diff --git a/.github/actions/prepare-build/action.yml b/.github/actions/prepare-build/action.yml new file mode 100644 index 0000000000..d64d579520 --- /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.2.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 60bd0cb729..e2ea1072fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,17 +22,58 @@ on: - "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 @@ -42,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 @@ -52,129 +94,118 @@ 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 }}-${{ matrix.arch }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" + needs: [maybe-release, variables] + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - - os: ubuntu-20.04 + - os: 22.04 + os_underscore: 22_04 + arch: x86_64 + runner: "ubuntu-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 - 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: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} + - os: 22.04 + os_underscore: 22_04 + arch: x86_64 + runner: "ubuntu-22.04" + should_run: true + ghc: ${{ needs.variables.outputs.GHC_VER }} + - os: 24.04 + os_underscore: 24_04 + arch: x86_64 + runner: "ubuntu-24.04" + should_run: true + ghc: ${{ needs.variables.outputs.GHC_VER }} + - os: 22.04 + os_underscore: 22_04 + arch: aarch64 + runner: "ubuntu-22.04-arm" + should_run: true + ghc: ${{ needs.variables.outputs.GHC_VER }} + - os: 24.04 + os_underscore: 24_04 + arch: aarch64 + runner: "ubuntu-24.04-arm" + should_run: true + ghc: ${{ needs.variables.outputs.GHC_VER }} steps: - - name: Skip unreliable ghc 8.10.7 build on stable branch - if: matrix.ghc == '8.10.7' && github.ref == 'refs/heads/stable' - run: exit 0 - - - 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 + + - name: Get UID and GID + id: ids + run: | + echo "uid=$(id -u)" >> $GITHUB_OUTPUT + echo "gid=$(id -g)" >> $GITHUB_OUTPUT + + # 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 }}-${{ matrix.arch }}-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 }} + USER_UID=${{ steps.ids.outputs.uid }} + USER_GID=${{ steps.ids.outputs.gid }} + + # 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@3.0/include" >> cabal.project.local - echo " extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib" >> cabal.project.local - echo "" >> cabal.project.local - echo "package direct-sqlcipher" >> cabal.project.local - echo " extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include" >> cabal.project.local - echo " extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib" >> cabal.project.local - echo " flags: +openssl" >> cabal.project.local - echo "" >> cabal.project.local - echo "package jpeg-turbo" >> cabal.project.local - echo " extra-include-dirs: /opt/homebrew/opt/libjpeg-turbo/include" >> cabal.project.local - echo " extra-lib-dirs: /opt/homebrew/opt/libjpeg-turbo/lib" >> cabal.project.local - echo " flags: +static" >> 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@3.0/include" >> cabal.project.local - echo " extra-lib-dirs: /usr/local/opt/openssl@3.0/lib" >> cabal.project.local - echo "" >> cabal.project.local - echo "package direct-sqlcipher" >> cabal.project.local - echo " extra-include-dirs: /usr/local/opt/openssl@3.0/include" >> cabal.project.local - echo " extra-lib-dirs: /usr/local/opt/openssl@3.0/lib" >> cabal.project.local - echo " flags: +openssl" >> cabal.project.local - echo "" >> cabal.project.local - echo "package jpeg-turbo" >> cabal.project.local - echo " extra-include-dirs: /usr/local/opt/libjpeg-turbo/include" >> cabal.project.local - echo " extra-lib-dirs: /usr/local/opt/libjpeg-turbo/lib" >> cabal.project.local - echo " flags: +static" >> 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 Linux dependencies - if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04' - run: sudo apt install -y libturbojpeg0-dev - - - name: Install pkg-config for Mac - if: matrix.os == 'macos-latest' || matrix.os == 'macos-13' - run: brew install openssl@3.0 pkg-config jpeg-turbo - - - 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 @@ -184,68 +215,234 @@ jobs: echo "package jpeg-turbo" >> cabal.project.local echo " flags: +static-gcc" >> 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: | + 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: Build CLI deb + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + shell: docker exec -t builder sh -eu {0} + run: | + version=${{ github.ref }} + version=${version#refs/tags/v} + version=${version%-*} + + ./scripts/desktop/build-cli-deb.sh "$version" + + - 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 + - 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: | + cli_name="simplex-chat-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}" + cli_deb_name="${cli_name}.deb" + cli_path="${{ github.workspace }}" + + docker cp builder:/out/simplex-chat "./${cli_name}" + docker cp builder:/out/deb-build/simplex-chat.deb "./${cli_deb_name}" + + echo "bin_name=${cli_name}" >> $GITHUB_OUTPUT + echo "bin_path=${cli_path}/${cli_name}" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-256\(${cli_name}\)= $(openssl sha256 "${cli_path}/${cli_name}" | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + echo "deb_name=${cli_deb_name}" >> $GITHUB_OUTPUT + echo "deb_path=${cli_path}/${cli_deb_name}" >> $GITHUB_OUTPUT + echo "deb_hash=$(echo SHA2-256\(${cli_deb_name}\)= $(openssl sha256 "${cli_path}/${cli_deb_name}" | 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: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.unix_cli_build.outputs.bin_path }} - asset_name: ${{ matrix.asset_name }} - tag: ${{ github.ref }} + bin_name: ${{ steps.linux_cli_prepare.outputs.bin_name }} + bin_path: ${{ steps.linux_cli_prepare.outputs.bin_path }} + bin_hash: ${{ steps.linux_cli_prepare.outputs.bin_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - 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 }} + - name: Upload CLI deb + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + uses: ./.github/actions/prepare-release with: - append_body: true - body: | - ${{ steps.unix_cli_build.outputs.bin_hash }} + bin_name: ${{ steps.linux_cli_prepare.outputs.deb_name }} + bin_path: ${{ steps.linux_cli_prepare.outputs.deb_path }} + bin_hash: ${{ steps.linux_cli_prepare.outputs.deb_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - 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: Build Desktop + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + shell: docker exec -t builder sh -eu {0} + run: | + scripts/desktop/make-deb-linux.sh - - name: Linux build desktop + - name: Prepare 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') + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: bash run: | - scripts/desktop/build-lib-linux.sh - cd apps/multiplatform - ./gradlew packageDeb - path=$(echo $PWD/release/main/deb/simplex_*_amd64.deb) + path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/deb/simplex_${{ matrix.arch }}.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 + echo "package_hash=$(echo SHA2-256\(simplex-desktop-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}.deb\)= $(openssl sha256 $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' - shell: bash + - 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: simplex-desktop-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}.deb + 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 - path=$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage) - echo "appimage_path=$path" >> $GITHUB_OUTPUT - echo "appimage_hash=$(echo SHA2-512\(simplex-desktop-x86_64.AppImage\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Mac build desktop + - 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-${{ matrix.arch }}.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-${{ matrix.arch }}.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 && matrix.arch == 'x86_64' + 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-15-intel + 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 + echo "" >> cabal.project.local + echo "package jpeg-turbo" >> cabal.project.local + echo " extra-include-dirs: /opt/homebrew/opt/libjpeg-turbo/include" >> cabal.project.local + echo " extra-lib-dirs: /opt/homebrew/opt/libjpeg-turbo/lib" >> cabal.project.local + echo " flags: +static" >> 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 }} @@ -255,88 +452,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 @@ -349,10 +535,9 @@ 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) @@ -369,70 +554,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..0febed4c87 --- /dev/null +++ b/.github/workflows/reproduce-schedule.yml @@ -0,0 +1,52 @@ +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: 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: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ env.TAG }} + repository: simplex-chat/simplex-chat + + # Otherwise we run out of disk space with Docker build + - name: Free disk space + shell: bash + run: ./scripts/ci/linux_util_free_space.sh + - name: Execute reproduce script + run: | + ${GITHUB_WORKSPACE}/scripts/simplex-chat-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}-simplex-chat/_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 6839d48aeb..5fbe8293bc 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -33,7 +33,7 @@ jobs: ./website/web.sh - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + uses: simplex-chat/actions-gh-pages@v3 with: publish_dir: ./website/_site github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 645b55ec9d..bf565453a5 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ website/translations.json website/src/img/images/ website/src/images/ website/src/js/lottie.min.js +website/src/js/ethers* website/src/privacy.md # Generated files website/package/generated* @@ -79,3 +80,4 @@ website/package-lock.json website/.cache website/test/stubs-layout-cache/_includes/*.js apps/android/app/release +apps/multiplatform/.kotlin/sessions diff --git a/Dockerfile b/Dockerfile index 7b9641777a..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 --constraint 'simplexmq +client_library' +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..3ddff59d12 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,134 @@ +# 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.2.0 +ARG JAVA_VER=17.0.17.10.1 +ARG JAVA_HASH_AMD64=e3e11daa5c22a45153bbeff1a0c21bf08631791e4e8d8ed14deba31c7cf9af1a +ARG JAVA_HASH_ARM64=2b460859b681757b33a7591b6238ecaf51569d05d2684984e5f0a89c6514acbc + +ENV TZ=Etc/UTC \ + DEBIAN_FRONTEND=noninteractive + +ARG USER_UID=1000 +ARG USER_GID=1000 +ARG USER_NAME=builder + +# Install curl, git and and simplex-chat dependencies +RUN apt-get update && \ + apt-get install -y curl \ + libpq-dev \ + git \ + strip-nondeterminism \ + 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 \ + zipalign \ + apksigner \ + python3 \ + python3-venv \ + xz-utils \ + 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 export JAVA_FILENAME='java-corretto.deb' \ + JAVA_VER_MAJOR=$(printf "${JAVA_VER}" | awk -F. '{print $1}') \ + JAVA_VER_DEB=$(printf "${JAVA_VER}" | sed 's/\.1$/-1/') && \ + case "$(uname -m)" in \ + x86_64) export JAVA_ARCH='amd64' JAVA_HASH="$JAVA_HASH_AMD64" ;; \ + aarch64) export JAVA_ARCH='arm64' JAVA_HASH="$JAVA_HASH_ARM64" ;; \ + *) echo "unknown arch $(uname -m)" && exit 1 ;; \ + esac && \ + curl --proto '=https' --tlsv1.2 -sSf \ + "https://corretto.aws/downloads/resources/${JAVA_VER}/java-${JAVA_VER_MAJOR}-amazon-corretto-jdk_${JAVA_VER_DEB}_${JAVA_ARCH}.deb" \ + -o "${JAVA_FILENAME}" && \ + if echo "${JAVA_HASH} ${JAVA_FILENAME}" | sha256sum -c -; then \ + if apt install -y ./"${JAVA_FILENAME}"; then \ + rm ./"${JAVA_FILENAME}"; \ + else \ + echo "Failed to install Java Corretto" && exit 1; \ + fi \ + else \ + echo "Checksum mismatch" && exit 1; \ + fi + +RUN userdel -r ubuntu || : +RUN groupadd -g ${USER_GID} ${USER_NAME} || :; useradd -u ${USER_UID} -g ${USER_GID} --create-home --shell /bin/bash ${USER_NAME} || : +RUN mkdir /nix /out && chown ${USER_NAME}:${USER_NAME} /nix /out +USER ${USER_NAME} +WORKDIR /home/${USER_NAME} + +# 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 + +# Setup basic env variables (required) +ENV HOME="/home/${USER_NAME}" USER="${USER_NAME}" +# Adjust PATH +ENV PATH="$HOME/.cabal/bin:$HOME/.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="$HOME" + +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 "$HOME/.android" "$HOME/.gradle" && \ + touch "$HOME/.android/repositories.cfg" && \ + echo 'org.gradle.console=plain' > "$HOME/.gradle/gradle.properties" &&\ + yes | sdkmanager --licenses >/dev/null + +ENV PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools" + +# Android reproducibility scripts +RUN python3 -m venv "$HOME/.venv" +RUN "$HOME/.venv/bin/pip" install apksigcopier repro-apk + +ENV PATH="$HOME/.venv/bin:$PATH" + +WORKDIR /project diff --git a/PRIVACY.md b/PRIVACY.md index 7c4bfbf660..18e5539726 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -123,6 +123,16 @@ This section applies only to the experimental group directory operated by Simple [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 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. @@ -131,9 +141,9 @@ The app includes support contact operated by SimpleX Chat Ltd. If you contact su Preset server operators will not share the information on their servers with each other, other than aggregate usage statistics. -Preset server operators will not provide general access to their servers or the data on their servers to each other. +Preset server operators must not provide general access to their servers or the data on their servers to each other. -Preset server operators will provide non-administrative access to control port of preset servers to SimpleX Chat Ltd, for the purposes of removing identified illegal content. This control port access only allows deleting known links and files, and access to aggregate statistics, but does NOT allow enumerating any information 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 @@ -148,7 +158,7 @@ The cases when the preset server operators may share the data temporarily stored - To detect, prevent, or otherwise address fraud, security, or technical issues. - To protect against harm to the rights, property, or safety of software users, operators of preset servers, or the public as required or permitted by law. -At the time of updating this document, the preset server operators 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 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. +By the time of updating this document, the preset server operators were not served with any enforceable requests and did not provide any information from the servers to any third parties. If the preset server operators are ever requested to provide such access or information, they will follow the due legal process to limit any information shared with the third parties to the minimally required by law. Preset server operators will publish information they are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md). @@ -190,7 +200,18 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **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 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; 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. +**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. + +**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. @@ -222,4 +243,4 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd and preset server operators at any time by deleting the Applications from your devices and discontinuing use of the Infrastructure of SimpleX Chat Ltd and preset server operators. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd and/or preset server operators. -Updated November 14, 2024 +Updated March 3, 2025 diff --git a/README.md b/README.md index 936667da8c..b1d2556072 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! @@ -24,15 +24,15 @@ ## Install the app -[iOS app](https://apps.apple.com/us/app/simplex-chat/id1605771084) +[iOS app](https://apps.apple.com/us/app/simplex-chat/id1605771084)   -[![Android app](https://github.com/simplex-chat/.github/blob/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app) +[![Android app](https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app)   -[F-Droid](https://app.simplex.chat) +[F-Droid](https://app.simplex.chat)   -[iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu) +[iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) - 🖲 Protects your messages and metadata - who you talk to and when. - 🔐 Double ratchet end-to-end encryption, with additional encryption layer. @@ -54,38 +54,20 @@ If you are interested in helping us to integrate open-source language models, an ## Join user groups -You can join the groups created by other users via the new [directory service](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). We are not responsible for the content shared in these groups. +You can find the groups created by users in [SimpleX Directory](https://simplex.chat/directory/). It is also available as [SimpleX bot](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) that allows to add your own groups and communities to the directory. We are not responsible for the content shared in these groups. **Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only. -You also can: -- criticize the app, and make comparisons with other messengers. -- share new messengers you think could be interesting for privacy, as long as you don't spam. -- share some privacy related publications, infrequently. -- having preliminary approved with the admin in direct message, share the link to a group you created, but only once. Once the group has more than 10 members it can be submitted to [SimpleX Directory Service](./docs/DIRECTORY.md) where the new users will be able to discover it. +You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://smp4.simplex.im/g#hr4lvFeBmndWMKTwqiodPz3VBo_6UmdGWocXd1SupsM) -You must: -- be polite to other users -- avoid spam (too frequent messages, even if they are relevant) -- avoid any personal attacks or hostility. -- avoid sharing any content that is not relevant to the above (that includes, but is not limited to, discussing politics or any aspects of society other than privacy, security, technology and communications, sharing any content that may be found offensive by other users, etc.). - -Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment. - -You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-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) - -There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform: +There is also a group [#simplex-devs](https://smp6.simplex.im/g#Drx3efC-n418AuSpzTspw9SER0iJwrQTmKBafQHwkKM) for developers who build on SimpleX platform: - chat bots and automations - integrations with other apps - social apps and services - etc. -There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users: - -[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmfiivxDKWFuowXrQOp11jsY8TuP__rBL%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAiz3pKNwvKudckFYMUfgoT0s96B0jfZ7ALHAu7rtE9HQ%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22jZeJpXGrRXQJU_-MSJ_v2A%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FJ5ES83pJimY2BRklS8fvy_iQwIU37xra%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0F0STP6UqN_12_k2cjjTrIjFgBGeWhOAmbY1qlk3pnM%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22VmUU0fqmYdCRmVCyvStvHA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FxCHBE_6PBRMqNEpm4UQDHXb9cz-mN7dd%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAetqlcM7zTCRw-iatnwCrvpJSto7lq5Yv6AsBMWv7GSM%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22foO5Xw4hhjOa_x7zET7otw%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FVXQTB0J2lLjYkgjWByhl6-1qmb5fgZHh%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAI6JaEWezfSwvcoTEkk6au-gkjrXR2ew2OqZYMYBvayk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22ORH9OEe8Duissh-hslfeVg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FqpHu0psOUdYfc11yQCzSyq5JhijrBzZT%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEACZ_7fbwlM45wl6cGif8cY47oPQ_AMdP0ATqOYLA6zHY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%229uRQRTir3ealdcSfB0zsrw%3D%3D%22%7D) (Italian-speaking). - -You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code. +You can join these and other groups by opening these links in the app or by opening them in a desktop browser and scanning the QR code. ## Follow our updates @@ -102,7 +84,7 @@ You need to share a link with your friend or scan a QR code from their phone, in The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established. -Make a private connection Conversation Video call +Make a private connection Conversation Video call After you connect, you can [verify connection security code](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification). @@ -110,6 +92,14 @@ 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: + +- [develop a chat bot](#develop-a-chat-bot) for SimpleX Chat! +- writing a tutorial or recipes about hosting servers, chat bots, etc. +- 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,15 +131,6 @@ 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. - ## Please support us with your donations Huge thank you to everybody who donated to SimpleX Chat! @@ -166,9 +147,9 @@ It is possible to donate via: - BTC: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u - XMR: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt - BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg -- ETH: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692 -- USDT (Ethereum): 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692 +- ETH/USDT (Ethereum, Arbitrum One): 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a - ZEC: t1fwjQW5gpFhDqXNhxqDWyF9j9WeKvVS5Jg +- ZEC shielded: u16rnvkflumf5uw9frngc2lymvmzgdr2mmc9unyu0l44unwfmdcpfm0axujd2w34ct3ye709azxsqge45705lpvvqu264ltzvfay55ygyq - DOGE: D99pV4n9TrPxBPCkQGx4w4SMSa6QjRBxPf - SOL: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu - please ask if you want to donate any other coins. @@ -193,6 +174,7 @@ SimpleX Chat founder - [SimpleX Platform design](#simplex-platform-design) - [Privacy and security: technical details and limitations](#privacy-and-security-technical-details-and-limitations) - [For developers](#for-developers) +- [Develop a chat bot](#develop-a-chat-bot) - [Roadmap](#roadmap) - [Disclaimers, Security contact, License](#disclaimers) @@ -234,6 +216,14 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Jul 29, 2025 SimpleX Chat v6.4.1: welcome your contacts, review members to protect groups, and more.](./blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md) + +[Jul 3, 2025 SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4](./blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.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) + +[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) @@ -305,27 +295,36 @@ What is already implemented: 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. 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 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. +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 You can: +- [create chat bots and services](#develop-a-chat-bot). +- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution. - use SimpleX Chat library to integrate chat functionality into your mobile apps. - create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/). -- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). -- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution. If you are considering developing with SimpleX platform please get in touch for any advice and support. Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) group to ask any questions and share your success stories. +## Develop a chat bot + +You can create a chat bot or any chat-based service in any language running SimpleX Chat terminal CLI as a local WebSocket server. + +See [our new bot API reference](./bots/README.md). Most of it is automatically generated from core library types, so it stays up to date. + +Also see [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). + ## Roadmap - ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies. @@ -424,14 +423,16 @@ Please do NOT report security vulnerabilities via GitHub issues. ## License -[AGPL v3](./LICENSE) +This software is licensed under the GNU Affero General Public License version 3 (AGPLv3). See the [LICENSE](./LICENSE) file for details. The SimpleX and SimpleX Chat name, logo, and associated branding materials are not covered by this license and are subject to the terms outlined in the [TRADEMARK](./docs/TRADEMARK.md) file. -[iOS app](https://apps.apple.com/us/app/simplex-chat/id1605771084) +Graphic designs, artworks and layouts are not licensed for re-use. If you want to use them in your publications, please ask for permission. Texts can be used as direct quotes, referencing the source. + +[iOS app](https://apps.apple.com/us/app/simplex-chat/id1605771084)   -[![Android app](https://github.com/simplex-chat/.github/blob/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app) +[![Android app](https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app)   -[F-Droid](https://app.simplex.chat) +[F-Droid](https://app.simplex.chat)   -[iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu) +[iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index ad8c661e1c..3f6998c9ec 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -54,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))") 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/ContentView.swift b/apps/ios/Shared/ContentView.swift index 652258415e..7adf7a0435 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -11,12 +11,10 @@ import SimpleXChat private enum NoticesSheet: Identifiable { case whatsNew(updatedConditions: Bool) - case updatedConditions var id: String { switch self { case .whatsNew: return "whatsNew" - case .updatedConditions: return "updatedConditions" } } } @@ -47,21 +45,10 @@ struct ContentView: View { @State private var showChooseLAMode = false @State private var showSetPasscode = false @State private var waitingForOrPassedAuth = true - @State private var chatListActionSheet: ChatListActionSheet? = nil @State private var chatListUserPickerSheet: UserPickerSheet? = nil private let callTopPadding: CGFloat = 40 - private enum ChatListActionSheet: Identifiable { - case planAndConnectSheet(sheet: PlanAndConnectActionSheet) - - var id: String { - switch self { - case let .planAndConnectSheet(sheet): return sheet.id - } - } - } - private var accessAuthenticated: Bool { chatModel.contentViewAccessAuthenticated || contentAccessAuthenticationExtended } @@ -76,7 +63,7 @@ struct ContentView: View { } } - @ViewBuilder func allViews() -> some View { + 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. @@ -183,11 +170,6 @@ struct ContentView: View { if case .onboardingComplete = step, chatModel.currentUser != nil { mainView() - .actionSheet(item: $chatListActionSheet) { sheet in - switch sheet { - case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false) - } - } } else { OnboardingView(onboarding: step) } @@ -211,7 +193,7 @@ 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() @@ -278,18 +260,18 @@ struct ContentView: View { let showWhatsNew = shouldShowWhatsNew() let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false noticesShown = showWhatsNew || showUpdatedConditions - if showWhatsNew { + if showWhatsNew || showUpdatedConditions { noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions) - } else if showUpdatedConditions { - noticesSheetItem = .updatedConditions } } } } prefShowLANotice = true connectViaUrl() + showReRegisterTokenAlert() } .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } + .onChange(of: chatModel.reRegisterTknStatus) { _ in showReRegisterTokenAlert() } .sheet(item: $noticesSheetItem) { item in switch item { case let .whatsNew(updatedConditions): @@ -298,13 +280,6 @@ struct ContentView: View { .if(updatedConditions) { v in v.task { await setConditionsNotified_() } } - case .updatedConditions: - UsageConditionsView( - currUserServers: Binding.constant([]), - userServers: Binding.constant([]) - ) - .modifier(ThemedBackground(grouped: true)) - .task { await setConditionsNotified_() } } } if chatModel.setDeliveryReceipts { @@ -315,6 +290,12 @@ 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 { @@ -446,30 +427,47 @@ struct ContentView: View { } func connectViaUrl() { + let m = ChatModel.shared + if let url = m.appOpenUrl { + m.appOpenUrl = nil + connectViaUrl_(url) + } else if let url = m.appOpenUrlLater, AppChatState.shared.value == .active, scenePhase == .active { + // correcting branch in case .onChange(of: scenePhase) in SimpleXApp doesn't trigger and transfer appOpenUrlLater into appOpenUrl + m.appOpenUrlLater = nil + connectViaUrl_(url) + } + } + + func connectViaUrl_(_ url: URL) { dismissAllSheets() { - let m = ChatModel.shared - if let url = m.appOpenUrl { - m.appOpenUrl = nil - var path = url.path - if (path == "/contact" || path == "/invitation") { - path.removeFirst() - let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") - planAndConnect( - link, - showAlert: showPlanAndConnectAlert, - showActionSheet: { chatListActionSheet = .planAndConnectSheet(sheet: $0) }, - dismiss: false, - incognito: nil - ) - } else { - AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid"))) - } + var path = url.path + 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( + link, + theme: theme, + dismiss: false + ) + } else { + AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid"))) } } } - private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) { - AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false)) + 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() + )) + } + } } } diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift new file mode 100644 index 0000000000..193b675a57 --- /dev/null +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -0,0 +1,2346 @@ +// +// 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 apiSetUserAutoAcceptMemberContacts(userId: Int64, enable: Bool) + 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, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String) + case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) + case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, 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, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool) + case apiDeleteChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, 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, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction) + case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction) + case apiPlanForwardChatItems(fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64]) + case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, 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 apiAcceptMember(groupId: Int64, groupMemberId: Int64, memberRole: GroupMemberRole) + case apiDeleteMemberSupportChat(groupId: Int64, groupMemberId: 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) + case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) + case apiDeleteGroupLink(groupId: Int64) + case apiGetGroupLink(groupId: Int64) + case apiAddGroupShortLink(groupId: Int64) + case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) + case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) + case apiAcceptMemberContact(contactId: Int64) + 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, incognito: Bool) + case apiSetConnectionIncognito(connId: Int64, incognito: Bool) + case apiChangeConnectionUser(connId: Int64, userId: Int64) + case apiConnectPlan(userId: Int64, connLink: String) + case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) + case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData) + case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) + case apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) + case apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) + case apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) + 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) + case apiDeleteMyAddress(userId: Int64) + case apiShowMyAddress(userId: Int64) + case apiAddMyAddressShortLink(userId: Int64) + case apiSetProfileAddress(userId: Int64, on: Bool) + case apiSetAddressSettings(userId: Int64, addressSettings: AddressSettings) + 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 apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?) + case apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, 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 .apiSetUserAutoAcceptMemberContacts(userId, enable): + return "/_set accept member contacts \(userId) \(onOff(enable))" + 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, scope, contentTag, pagination, search): + let tag = contentTag != nil ? " content=\(contentTag!.rawValue)" : "" + return "/_get chat \(chatId)\(scopeRef(scope: scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") + case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)" + case let .apiSendMessages(type, id, scope, live, ttl, composedMessages): + let msgs = encodeJSON(composedMessages) + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_send \(ref(type, id, scope: scope)) 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, scope: nil)) \(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, scope, itemId, um, live): return "/_update item \(ref(type, id, scope: scope)) \(itemId) live=\(onOff(live)) \(um.cmdString)" + case let .apiDeleteChatItem(type, id, scope, itemIds, mode): return "/_delete item \(ref(type, id, scope: scope)) \(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, scope, itemId, add, reaction): return "/_reaction \(ref(type, id, scope: scope)) \(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, scope, itemIds): return "/_forward plan \(ref(type, id, scope: scope)) \(itemIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl): + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_forward \(ref(toChatType, toChatId, scope: toScope)) \(ref(fromChatType, fromChatId, scope: fromScope)) \(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 .apiAcceptMember(groupId, groupMemberId, memberRole): return "/_accept member #\(groupId) \(groupMemberId) \(memberRole.rawValue)" + case let .apiDeleteMemberSupportChat(groupId, groupMemberId): return "/_delete member chat #\(groupId) \(groupMemberId)" + 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): 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 .apiAddGroupShortLink(groupId): return "/_short 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 .apiAcceptMemberContact(contactId): return "/_accept member contact @\(contactId)" + 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, scope: nil)) \(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, scope: nil)) \(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, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" + case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" + case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" + case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)" + case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))" + case let .apiPrepareGroup(userId, connLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(groupShortLinkData))" + case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)" + case let .apiChangePreparedGroupUser(groupId, newUserId): return "/_set group user #\(groupId) \(newUserId)" + case let .apiConnectPreparedContact(contactId, incognito, mc): return "/_connect contact @\(contactId) incognito=\(onOff(incognito))\(maybeContent(mc))" + case let .apiConnectPreparedGroup(groupId, incognito, mc): return "/_connect group #\(groupId) incognito=\(onOff(incognito))\(maybeContent(mc))" + 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, scope: nil)) \(chatDeleteMode.cmdString)" + case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id, scope: nil))" + 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): return "/_address \(userId)" + case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" + case let .apiShowMyAddress(userId): return "/_show_address \(userId)" + case let .apiAddMyAddressShortLink(userId): return "/_short_link_address \(userId)" + case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" + case let .apiSetAddressSettings(userId, addressSettings): return "/_address_settings \(userId) \(encodeJSON(addressSettings))" + 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 let .apiChatRead(type, id, scope): return "/_read chat \(ref(type, id, scope: scope))" + case let .apiChatItemsRead(type, id, scope, itemIds): return "/_read chat items \(ref(type, id, scope: scope)) \(joinedIds(itemIds))" + case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id, scope: nil)) \(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 .apiSetUserAutoAcceptMemberContacts: return "apiSetUserAutoAcceptMemberContacts" + 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 .apiAcceptMember: return "apiAcceptMember" + case .apiDeleteMemberSupportChat: return "apiDeleteMemberSupportChat" + 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 .apiAddGroupShortLink: return "apiAddGroupShortLink" + case .apiCreateMemberContact: return "apiCreateMemberContact" + case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" + case .apiAcceptMemberContact: return "apiAcceptMemberContact" + 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 .apiPrepareContact: return "apiPrepareContact" + case .apiPrepareGroup: return "apiPrepareGroup" + case .apiChangePreparedContactUser: return "apiChangePreparedContactUser" + case .apiChangePreparedGroupUser: return "apiChangePreparedGroupUser" + case .apiConnectPreparedContact: return "apiConnectPreparedContact" + case .apiConnectPreparedGroup: return "apiConnectPreparedGroup" + 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 .apiAddMyAddressShortLink: return "apiAddMyAddressShortLink" + case .apiSetProfileAddress: return "apiSetProfileAddress" + case .apiSetAddressSettings: return "apiSetAddressSettings" + 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 .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, scope: GroupChatScope?) -> String { + "\(type.rawValue)\(id)\(scopeRef(scope: scope))" + } + + func scopeRef(scope: GroupChatScope?) -> String { + switch (scope) { + case .none: "" + case let .memberSupport(groupMemberId_): + if let groupMemberId = groupMemberId_ { + "(_support:\(groupMemberId))" + } else { + "(_support)" + } + case .reports: + "(reports, prohibited)" // can't use surrogate Reports scope + } + } + + 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) + } + + private func maybeContent(_ mc: MsgContent?) -> String { + if case let .text(s) = mc, s.isEmpty { + "" + } else if let mc { + " " + mc.cmdString + } else { + "" + } + } +} + +// 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 newPreparedChat(user: UserRef, chat: ChatData) + case contactUserChanged(user: UserRef, fromContact: Contact, newUser: UserRef, toContact: Contact) + case groupUserChanged(user: UserRef, fromGroup: GroupInfo, newUser: UserRef, toGroup: GroupInfo) + case sentConfirmation(user: UserRef, connection: PendingContactConnection) + case sentInvitation(user: UserRef, connection: PendingContactConnection) + case startedConnectionToContact(user: UserRef, contact: Contact) + case startedConnectionToGroup(user: UserRef, groupInfo: GroupInfo) + 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 itemsReadForChat(user: UserRef, chatInfo: ChatInfo) + 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, contactRequest: UserContactRequest, contact_: Contact?) + 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 .newPreparedChat: "newPreparedChat" + case .contactUserChanged: "contactUserChanged" + case .groupUserChanged: "groupUserChanged" + case .sentConfirmation: "sentConfirmation" + case .sentInvitation: "sentInvitation" + case .startedConnectionToContact: "startedConnectionToContact" + case .startedConnectionToGroup: "startedConnectionToGroup" + case .sentInvitationToContact: "sentInvitationToContact" + case .contactAlreadyExists: "contactAlreadyExists" + case .contactDeleted: "contactDeleted" + case .contactConnectionDeleted: "contactConnectionDeleted" + case .groupDeletedUser: "groupDeletedUser" + case .itemsReadForChat: "itemsReadForChat" + 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 .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 .itemsReadForChat(u, chatInfo): return withUser(u, String(describing: chatInfo)) + 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, String(describing: contactLink)) + case let .userContactLinkUpdated(u, contactLink): return withUser(u, String(describing: contactLink)) + 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 let .contactRequestRejected(u, contactRequest, contact_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\ncontact_: \(String(describing: contact_))") + 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 .newPreparedChat(u, chat): return withUser(u, String(describing: chat)) + case let .contactUserChanged(u, fromContact, newUser, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\nnewUserId: \(String(describing: newUser.userId))\ntoContact: \(String(describing: toContact))") + case let .groupUserChanged(u, fromGroup, newUser, toGroup): return withUser(u, "fromGroup: \(String(describing: fromGroup))\nnewUserId: \(String(describing: newUser.userId))\ntoGroup: \(String(describing: toGroup))") + case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection)) + case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) + case let .startedConnectionToContact(u, contact): return withUser(u, String(describing: contact)) + case let .startedConnectionToGroup(u, groupInfo): return withUser(u, String(describing: groupInfo)) + 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 memberAccepted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case memberSupportChatRead(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case memberSupportChatDeleted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + 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, groupLink: GroupLink) + case groupLink(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink) + 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 memberContactAccepted(user: UserRef, contact: Contact) + // 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 + // 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 .memberAccepted: "memberAccepted" + case .memberSupportChatRead: "memberSupportChatRead" + case .memberSupportChatDeleted: "memberSupportChatDeleted" + 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 .memberContactAccepted: "memberContactAccepted" + case .rcvFileAccepted: "rcvFileAccepted" + case .rcvFileAcceptedSndCancelled: "rcvFileAcceptedSndCancelled" + case .standaloneFileInfo: "standaloneFileInfo" + case .rcvStandaloneFileCreated: "rcvStandaloneFileCreated" + case .rcvFileCancelled: "rcvFileCancelled" + case .sndFileCancelled: "sndFileCancelled" + case .sndStandaloneFileCreated: "sndStandaloneFileCreated" + 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 .memberAccepted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .memberSupportChatRead(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .memberSupportChatDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + 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, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)") + case let .groupLink(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)") + 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 .memberContactAccepted(u, contact): return withUser(u, "contact: \(contact)") + 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 .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, chat_: ChatData?) + case contactUpdated(user: UserRef, toContact: Contact) + case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember) + case subscriptionStatus(subscriptionStatus: SubscriptionStatus, connections: [String]) + case chatInfoUpdated(user: UserRef, chatInfo: ChatInfo) + 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 memberAcceptedByOther(user: UserRef, groupInfo: GroupInfo, acceptingMember: 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 .subscriptionStatus: "subscriptionStatus" + case .chatInfoUpdated: "chatInfoUpdated" + 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 .memberAcceptedByOther: "memberAcceptedByOther" + 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, chat_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\nchat_: \(String(describing: chat_))") + 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 .subscriptionStatus(status, conns): return "subscriptionStatus: \(String(describing: status))\nconnections: \(String(describing: conns))" + case let .chatInfoUpdated(u, chatInfo): return withUser(u, String(describing: chatInfo)) + 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 .memberAcceptedByOther(u, groupInfo, acceptingMember, member): return withUser(u, "groupInfo: \(groupInfo)\nacceptingMember: \(acceptingMember)\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(contactSLinkData_: ContactShortLinkData?) + case ownLink + case connecting(contact_: Contact?) + case known(contact: Contact) +} + +enum ContactAddressPlan: Decodable, Hashable { + case ok(contactSLinkData_: ContactShortLinkData?) + case ownLink + case connectingConfirmReconnect + case connectingProhibit(contact: Contact) + case known(contact: Contact) + case contactViaAddress(contact: Contact) +} + +enum GroupLinkPlan: Decodable, Hashable { + case ok(groupSLinkData_: GroupShortLinkData?) + 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 ForwardConfirmation: Decodable, Hashable { + case filesNotAccepted(fileIds: [Int64]) + case filesInProgress(filesCount: Int) + case filesMissing(filesCount: Int) + case filesFailed(filesCount: Int) +} + +struct UserMsgReceiptSettings: Codable { + var enable: Bool + var clearOverrides: Bool +} + +protocol SimplexAddress { + var connLinkContact: CreatedConnLink { get } + var shortLinkDataSet: Bool { get } + var shortLinkLargeDataSet: Bool { get } +} + +extension SimplexAddress { + var shouldBeUpgraded: Bool { + connLinkContact.connShortLink == nil || !shortLinkDataSet || !shortLinkLargeDataSet + } + + func shareAddress(short: Bool) { + showShareSheet(items: [simplexChatLink(connLinkContact.simplexChatUri(short: short))]) + } +} + +struct UserContactLink: Decodable, Hashable, SimplexAddress { + var connLinkContact: CreatedConnLink + var shortLinkDataSet: Bool + var shortLinkLargeDataSet: Bool + var addressSettings: AddressSettings + + init(_ ccLink: CreatedConnLink) { + connLinkContact = ccLink + let slDataSet = ccLink.connShortLink != nil + shortLinkDataSet = slDataSet + shortLinkLargeDataSet = slDataSet + addressSettings = AddressSettings(businessAddress: false) + } +} + +struct AddressSettings: Codable, Hashable { + var businessAddress: Bool + var autoAccept: AutoAccept? + var autoReply: MsgContent? +} + +struct AutoAccept: Codable, Hashable { + var acceptIncognito: Bool +} + +struct GroupLink: Decodable, Hashable, SimplexAddress { + var userContactLinkId: Int64 + var connLinkContact: CreatedConnLink + var shortLinkDataSet: Bool + var shortLinkLargeDataSet: Bool + var groupLinkId: String + var acceptMemberRole: GroupMemberRole +} + +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(.unknownCAError)): + return err + " " + NSLocalizedString("Fingerprint in server address does not match certificate.", comment: "server test error") + default: + return err + " " + String.localizedStringWithFormat(NSLocalizedString("Error: %@.", comment: "server test error"), String(describing: testError)) + } + } +} + +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 95cebcde10..f1f4e686bd 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,8 +52,26 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) { items.append(item) } +// analogue for SecondaryContextFilter in Kotlin +enum SecondaryItemsModelFilter { + case groupChatScopeContext(groupScopeInfo: GroupChatScopeInfo) + case msgContentTagContext(contentTag: MsgContentTag) + + func descr() -> String { + switch self { + case let .groupChatScopeContext(groupScopeInfo): + return "groupChatScopeContext \(groupScopeInfo.toChatScope())" + case let .msgContentTagContext(contentTag): + return "msgContentTagContext \(contentTag.rawValue)" + } + } +} + +// analogue for ChatsContext in Kotlin class ItemsModel: ObservableObject { - static let shared = ItemsModel() + static let shared = ItemsModel(secondaryIMFilter: nil) + public var secondaryIMFilter: SecondaryItemsModelFilter? + public var preloadState = PreloadState() private let publisher = ObservableObjectPublisher() private var bag = Set() var reversedChatItems: [ChatItem] = [] { @@ -53,61 +80,113 @@ class ItemsModel: ObservableObject { 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 = false + @Published var showLoadingProgress: ChatId? = nil - init() { + private var navigationTimeoutTask: Task? = nil + private var loadChatTask: Task? = nil + + var lastItemsLoaded: Bool { + chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id + } + + init(secondaryIMFilter: SecondaryItemsModelFilter? = nil) { + self.secondaryIMFilter = secondaryIMFilter publisher .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .sink { self.objectWillChange.send() } .store(in: &bag) } + static func loadSecondaryChat(_ chatId: ChatId, chatFilter: SecondaryItemsModelFilter, willNavigate: @escaping () -> Void = {}) { + let im = ItemsModel(secondaryIMFilter: chatFilter) + ChatModel.shared.secondaryIM = im + im.loadOpenChat(chatId, willNavigate: willNavigate) + } + func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) { - let navigationTimeout = Task { + navigationTimeoutTask?.cancel() + loadChatTask?.cancel() + navigationTimeoutTask = Task { do { try await Task.sleep(nanoseconds: 250_000000) await MainActor.run { - willNavigate() ChatModel.shared.chatId = chatId + willNavigate() } } catch {} } - let progressTimeout = Task { - do { - try await Task.sleep(nanoseconds: 1500_000000) - await MainActor.run { showLoadingProgress = true } - } catch {} - } - Task { - if let chat = ChatModel.shared.getChat(chatId) { - await MainActor.run { self.isLoading = true } -// try? await Task.sleep(nanoseconds: 5000_000000) - await loadChat(chat: chat) - navigationTimeout.cancel() - progressTimeout.cancel() + loadChatTask = Task { + await MainActor.run { self.isLoading = true } +// try? await Task.sleep(nanoseconds: 1000_000000) + await loadChat(chatId: chatId, im: self) + if !Task.isCancelled { await MainActor.run { self.isLoading = false - self.showLoadingProgress = false - willNavigate() - ChatModel.shared.chatId = chatId + 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, im: self, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil) + if !Task.isCancelled { + await MainActor.run { + if openAroundItemId == nil { + ChatModel.shared.chatId = chatId + } + } + } + } + } + + public var contentTag: MsgContentTag? { + switch secondaryIMFilter { + case nil: nil + case .groupChatScopeContext: nil + case let .msgContentTagContext(contentTag): contentTag + } + } + + public var groupScopeInfo: GroupChatScopeInfo? { + switch secondaryIMFilter { + case nil: nil + case let .groupChatScopeContext(scopeInfo): scopeInfo + case .msgContentTagContext: nil + } + } +} + +class PreloadState { + var prevFirstVisible: Int64 = Int64.min + var prevItemsCount: Int = 0 + var preloading: Bool = false + + func clear() { + prevFirstVisible = Int64.min + prevItemsCount = 0 + preloading = false + } } 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] = [:] @@ -124,11 +203,9 @@ class ChatTagsModel: ObservableObject { } } } - if case let .presetTag(tag) = tm.activeFilter, (newPresetTags[tag] ?? 0) == 0 { - activeFilter = nil - } presetTags = newPresetTags unreadTags = newUnreadTags + clearActiveChatFilterIfNeeded() } func updateChatFavorite(favorite: Bool, wasFavorite: Bool) { @@ -137,9 +214,7 @@ class ChatTagsModel: ObservableObject { presetTags[.favorites] = (count ?? 0) + 1 } else if !favorite && wasFavorite, let count { presetTags[.favorites] = max(0, count - 1) - if case .presetTag(.favorites) = activeFilter, (presetTags[.favorites] ?? 0) == 0 { - activeFilter = nil - } + clearActiveChatFilterIfNeeded() } } @@ -163,14 +238,15 @@ class ChatTagsModel: ObservableObject { } } } + 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 @@ -193,30 +269,17 @@ class ChatTagsModel: ObservableObject { func changeGroupReportsTag(_ by: Int = 0) { if by == 0 { return } - presetTags[.groupReports] = (presetTags[.groupReports] ?? 0) + by - } -} - -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 - } + presetTags[.groupReports] = max(0, (presetTags[.groupReports] ?? 0) + by) + clearActiveChatFilterIfNeeded() } - func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { - if let conn = contact.activeConn { - networkStatuses[conn.agentConnId] ?? .unknown - } else { - .unknown + 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 } } } @@ -228,6 +291,41 @@ class ChatItemDummyModel: ObservableObject { func sendUpdate() { objectWillChange.send() } } +class ConnectProgressManager: ObservableObject { + @Published private var connectInProgress: String? = nil + @Published private var connectProgressByTimeout: Bool = false + private var onCancel: (() -> Void)? + + static let shared = ConnectProgressManager() + + func startConnectProgress(_ text: String, onCancel: (() -> Void)? = nil) { + connectInProgress = text + self.onCancel = onCancel + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.connectProgressByTimeout = self.connectInProgress != nil + } + } + + func stopConnectProgress() { + connectInProgress = nil + onCancel = nil + connectProgressByTimeout = false + } + + func cancelConnectProgress() { + onCancel?() + stopConnectProgress() + } + + var showConnectProgress: String? { + connectProgressByTimeout ? connectInProgress : nil + } + + var isInProgress: Bool { + connectInProgress != nil + } +} + final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var setDeliveryReceipts = false @@ -253,7 +351,9 @@ final class ChatModel: ObservableObject { @Published var deletedChats: Set = [] // current chat @Published var chatId: String? - var chatItemStatuses: Dictionary = [:] + @Published var chatAgentConnId: String? + @Published var chatSubStatus: SubscriptionStatus? + @Published var openAroundItemId: ChatItem.ID? = nil @Published var chatToTop: String? @Published var groupMembers: [GMember] = [] @Published var groupMembersIndexes: Dictionary = [:] // groupMemberId to index in groupMembers list @@ -264,9 +364,11 @@ final class ChatModel: ObservableObject { @Published var userAddress: UserContactLink? @Published var chatItemTTL: ChatItemTTL = .none @Published var appOpenUrl: URL? + @Published var appOpenUrlLater: URL? @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? @@ -301,6 +403,10 @@ final class ChatModel: ObservableObject { let im = ItemsModel.shared + // ItemsModel for secondary chat view (such as support scope chat), as opposed to ItemsModel.shared used for primary chat + @Published var secondaryIM: ItemsModel? = nil + @Published var secondaryPendingInviteeChatOpened = false + static var ok: Bool { ChatModel.shared.chatDbStatus == .ok } let ntfEnableLocal = true @@ -313,6 +419,10 @@ final class ChatModel: ObservableObject { remoteCtrlSession?.active ?? false } + var addressShortLinkDataSet: Bool { + userAddress?.shortLinkDataSet ?? true + } + func getUser(_ userId: Int64) -> User? { currentUser?.userId == userId ? currentUser @@ -358,7 +468,7 @@ final class ChatModel: ObservableObject { func getGroupChat(_ groupId: Int64) -> Chat? { chats.first { chat in - if case let .group(groupInfo) = chat.chatInfo { + if case let .group(groupInfo, _) = chat.chatInfo { return groupInfo.groupId == groupId } else { return false @@ -411,7 +521,11 @@ final class ChatModel: ObservableObject { func updateChatInfo(_ cInfo: ChatInfo) { if let i = getChatIndex(cInfo.id) { - chats[i].chatInfo = cInfo + if case let .group(groupInfo, groupChatScope) = cInfo, groupChatScope != nil { + chats[i].chatInfo = .group(groupInfo: groupInfo, groupChatScope: nil) + } else { + chats[i].chatInfo = cInfo + } chats[i].created = Date.now } } @@ -433,7 +547,7 @@ final class ChatModel: ObservableObject { } func updateGroup(_ groupInfo: GroupInfo) { - updateChat(.group(groupInfo: groupInfo)) + updateChat(.group(groupInfo: groupInfo, groupChatScope: nil)) } private func updateChat(_ cInfo: ChatInfo, addMissing: Bool = true) { @@ -465,8 +579,15 @@ final class ChatModel: ObservableObject { } } - func updateChats(_ newChats: [ChatData]) { - chats = newChats.map { Chat($0) } + func updateChats(_ newChats: [ChatData], keepingChatId: String? = nil) { + if let keepingChatId, + let chatToKeep = getChat(keepingChatId), + let i = newChats.firstIndex(where: { $0.id == keepingChatId }) { + let remainingNewChats = Array(newChats[..= currentPreviewItem.meta.itemTs { - [cItem] + // update preview + if cInfo.groupChatScope() == nil || cInfo.groupInfo?.membership.memberPending ?? false { + chats[i].chatItems = switch cInfo { + case .group: + if let currentPreviewItem = chats[i].chatItems.first { + if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs { + [cItem] + } else { + [currentPreviewItem] + } } else { - [currentPreviewItem] + [cItem] } - } else { + default: [cItem] } - default: - [cItem] - } - if case .rcvNew = cItem.meta.itemStatus { - unreadCollector.changeUnreadCounter(cInfo.id, by: 1) + if case .rcvNew = cItem.meta.itemStatus { + unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0) + } } + // pop chat popChatCollector.throttlePopChat(cInfo.id, currentPosition: i) } else { - addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) + if cInfo.groupChatScope() == nil { + addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) + } else { + addChat(Chat(chatInfo: cInfo, chatItems: [])) + } } - // add to current chat - if chatId == cInfo.id { - _ = _upsertChatItem(cInfo, cItem) + // add to current scope + if let ciIM = getCIItemsModel(cInfo, cItem) { + _ = _upsertChatItem(ciIM, cInfo, cItem) + } + } + + func getCIItemsModel(_ cInfo: ChatInfo, _ ci: ChatItem) -> ItemsModel? { + let cInfoScope = cInfo.groupChatScope() + return if let cInfoScope = cInfoScope { + switch (cInfoScope, secondaryIM?.secondaryIMFilter) { + case let (.memberSupport, .some(.groupChatScopeContext(groupScopeInfo))): + // Chat with member or Chat with admins opened (secondaryIM has .groupChatScopeContext filter), cInfo has matching scope + (cInfo.id == chatId && sameChatScope(cInfoScope, groupScopeInfo.toChatScope())) ? secondaryIM : nil + + case let (.memberSupport, .some(.msgContentTagContext(contentTag))): + // Reports view opened (secondaryIM has .msgContentTagContext(.report) filter), we process event (cInfo has proper .memberSupport scope) + (cInfo.id == chatId && ci.isReport && contentTag == .report) ? secondaryIM : nil + + case let (.reports, .some(.msgContentTagContext(contentTag))): + // Reports view opened (secondaryIM has .msgContentTagContext(.report) filter), we process user action (cInfo has surrogate .reports scope) + (cInfo.id == chatId && ci.isReport && contentTag == .report) ? secondaryIM : nil + default: + nil + } + } else { + cInfo.id == chatId ? im : nil } } func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { - // update previews - var res: Bool - if let chat = getChat(cInfo.id) { - if let pItem = chat.chatItems.last { - if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { + // update chat list + var itemAdded: Bool = false + if cInfo.groupChatScope() == nil { + if let chat = getChat(cInfo.id) { + if let pItem = chat.chatItems.last { + if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { + chat.chatItems = [cItem] + } + } else { chat.chatItems = [cItem] } } else { - chat.chatItems = [cItem] + addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) + itemAdded = true + } + if cItem.isDeletedContent || cItem.meta.itemDeleted != nil { + VoiceItemState.stopVoiceInChatView(cInfo, cItem) } - res = false - } else { - addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) - res = true } - if cItem.isDeletedContent || cItem.meta.itemDeleted != nil { - VoiceItemState.stopVoiceInChatView(cInfo, cItem) + // update current scope + if let ciIM = getCIItemsModel(cInfo, cItem) { + itemAdded = _upsertChatItem(ciIM, cInfo, cItem) } - // update current chat - return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res + return itemAdded } - private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { - if let i = getChatItemIndex(cItem) { - _updateChatItem(at: i, with: cItem) - ChatItemDummyModel.shared.sendUpdate() + private func _upsertChatItem(_ ciIM: ItemsModel, _ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { + if let i = getChatItemIndex(ciIM, cItem) { + let oldStatus = ciIM.reversedChatItems[i].meta.itemStatus + let newStatus = cItem.meta.itemStatus + var ci = cItem + if shouldKeepOldSndCIStatus(oldStatus: oldStatus, newStatus: newStatus) { + ci.meta.itemStatus = oldStatus + } + _updateChatItem(ciIM: ciIM, at: i, with: ci) + ChatItemDummyModel.shared.sendUpdate() // TODO [knocking] review what's this return false } else { - 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.itemAdded = true + ciIM.reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0) + ciIM.chatState.itemAdded((cItem.id, cItem.isRcvNew), hasLiveDummy ? 1 : 0) + ciIM.itemAdded = true ChatItemDummyModel.shared.sendUpdate() return true } @@ -559,45 +723,82 @@ final class ChatModel: ObservableObject { } func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) { - if chatId == cInfo.id, let i = getChatItemIndex(cItem) { + if let ciIM = getCIItemsModel(cInfo, cItem), + let i = getChatItemIndex(ciIM, cItem) { withConditionalAnimation { - _updateChatItem(at: i, with: cItem) + _updateChatItem(ciIM: ciIM, at: i, with: cItem) } - } else if let status = status { - chatItemStatuses.updateValue(status, forKey: cItem.id) } } - private func _updateChatItem(at i: Int, with cItem: ChatItem) { - im.reversedChatItems[i] = cItem - im.reversedChatItems[i].viewTimestamp = .now + private func _updateChatItem(ciIM: ItemsModel, at i: Int, with cItem: ChatItem) { + ciIM.reversedChatItems[i] = cItem + ciIM.reversedChatItems[i].viewTimestamp = .now } - func getChatItemIndex(_ cItem: ChatItem) -> Int? { - im.reversedChatItems.firstIndex(where: { $0.id == cItem.id }) + func getChatItemIndex(_ ciIM: ItemsModel, _ cItem: ChatItem) -> Int? { + ciIM.reversedChatItems.firstIndex(where: { $0.id == cItem.id }) } func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { - if cItem.isRcvNew { - unreadCollector.changeUnreadCounter(cInfo.id, by: -1) - } - // update previews - if let chat = getChat(cInfo.id) { - if let pItem = chat.chatItems.last, pItem.id == cItem.id { - chat.chatItems = [ChatItem.deletedItemDummy()] + // update chat list + if cInfo.groupChatScope() == nil { + if cItem.isRcvNew { + unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0) + } + // update previews + if let chat = getChat(cInfo.id) { + if let pItem = chat.chatItems.last, pItem.id == cItem.id { + chat.chatItems = [ChatItem.deletedItemDummy()] + } } } - // remove from current chat - if chatId == cInfo.id { - if let i = getChatItemIndex(cItem) { - _ = withAnimation { - im.reversedChatItems.remove(at: i) + // remove from current scope + if let ciIM = getCIItemsModel(cInfo, cItem) { + if let i = getChatItemIndex(ciIM, cItem) { + withAnimation { + let item = ciIM.reversedChatItems.remove(at: i) + ciIM.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed()) } } } VoiceItemState.stopVoiceInChatView(cInfo, cItem) } + func removeMemberItems(_ removedMember: GroupMember, byMember: GroupMember, _ groupInfo: GroupInfo) { + 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 = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil } if previous { @@ -640,6 +841,7 @@ final class ChatModel: ObservableObject { let cItem = ChatItem.liveDummy(chatInfo.chatType) withAnimation { im.reversedChatItems.insert(cItem, at: 0) + im.chatState.itemAdded((cItem.id, cItem.isRcvNew), 0) im.itemAdded = true } return cItem @@ -659,63 +861,23 @@ final class ChatModel: ObservableObject { im.reversedChatItems.first?.isLiveDummy == true } - func markChatItemsRead(_ cInfo: ChatInfo) { + func markAllChatItemsRead(_ chatIM: ItemsModel, _ cInfo: ChatInfo) { // update preview _updateChat(cInfo.id) { chat in - self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount) - self.updateFloatingButtons(unreadCount: 0) + 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 < im.reversedChatItems.count { - markChatItemRead_(j) - j += 1 - } - } - - private func updateFloatingButtons(unreadCount: Int) { - let fbm = ChatView.FloatingButtonModel.shared - fbm.totalUnread = unreadCount - fbm.objectWillChange.send() - } - - 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.im.reversedChatItems[j].meta.itemStatus { - unreadBelow += 1 - } - j -= 1 - } - // update preview - let markedCount = chat.chatStats.unreadCount - unreadBelow - if markedCount > 0 { - let wasUnread = chat.unreadTag - chat.chatStats.unreadCount -= markedCount - ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: wasUnread) - self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount) - self.updateFloatingButtons(unreadCount: chat.chatStats.unreadCount) - } - } + var i = 0 + while i < im.reversedChatItems.count { + markChatItemRead_(chatIM, 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 @@ -727,7 +889,7 @@ final class ChatModel: ObservableObject { 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() @@ -735,20 +897,28 @@ final class ChatModel: ObservableObject { } // clear current chat if chatId == cInfo.id { - chatItemStatuses = [:] im.reversedChatItems = [] + im.chatState.clear() } } - func markChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) { + func markChatItemsRead(_ chatIM: ItemsModel, _ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], _ mentionsRead: Int) { if self.chatId == cInfo.id { - for itemId in itemIds { - if let i = im.reversedChatItems.firstIndex(where: { $0.id == itemId }) { - markChatItemRead_(i) + var unreadItemIds: Set = [] + var i = 0 + var ids = Set(itemIds) + while i < chatIM.reversedChatItems.count && !ids.isEmpty { + let item = chatIM.reversedChatItems[i] + if ids.contains(item.id) && item.isRcvNew { + markChatItemRead_(chatIM, i) + unreadItemIds.insert(item.id) + ids.remove(item.id) } + i += 1 } + chatIM.chatState.itemsRead(unreadItemIds, chatIM.reversedChatItems.reversed()) } - self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count) + self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead) } private let unreadCollector = UnreadCollector() @@ -756,16 +926,16 @@ final class ChatModel: ObservableObject { class UnreadCollector { private let subject = PassthroughSubject() private var bag = Set() - private var unreadCounts: [ChatId: Int] = [:] + private var unreadCounts: [ChatId: (unread: Int, mentions: Int)] = [:] init() { subject .debounce(for: 1, scheduler: DispatchQueue.main) .sink { let m = ChatModel.shared - for (chatId, count) in self.unreadCounts { - if let i = m.getChatIndex(chatId) { - m.changeUnreadCounter(i, by: count) + 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 = [:] @@ -773,17 +943,15 @@ final class ChatModel: ObservableObject { .store(in: &bag) } - func changeUnreadCounter(_ chatId: ChatId, by count: Int) { - if chatId == ChatModel.shared.chatId { - ChatView.FloatingButtonModel.shared.totalUnread += count - } - self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count + 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() @@ -796,7 +964,7 @@ final class ChatModel: ObservableObject { .sink { self.popCollectedChats() } .store(in: &bag) } - + func throttlePopChat(_ chatId: ChatId, currentPosition: Int) { let m = ChatModel.shared if currentPosition > 0 && m.chatId == chatId { @@ -807,7 +975,7 @@ final class ChatModel: ObservableObject { subject.send() } } - + func clear() { chatsToPop = [:] } @@ -844,20 +1012,22 @@ final class ChatModel: ObservableObject { } } - private func markChatItemRead_(_ i: Int) { - let meta = im.reversedChatItems[i].meta + private func markChatItemRead_(_ chatIM: ItemsModel, _ i: Int) { + let meta = chatIM.reversedChatItems[i].meta if case .rcvNew = meta.itemStatus { - im.reversedChatItems[i].meta.itemStatus = .rcvRead - im.reversedChatItems[i].viewTimestamp = .now + chatIM.reversedChatItems[i].meta.itemStatus = .rcvRead + chatIM.reversedChatItems[i].viewTimestamp = .now if meta.itemLive != true, let ttl = meta.itemTimed?.ttl { - im.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl) + chatIM.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl) } } } - func changeUnreadCounter(_ chatIndex: Int, by count: Int) { + func changeUnreadCounter(_ chatIndex: Int, by count: Int, unreadMentions: Int) { let wasUnread = chats[chatIndex].unreadTag - chats[chatIndex].chatStats.unreadCount = chats[chatIndex].chatStats.unreadCount + count + 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) } @@ -866,6 +1036,13 @@ final class ChatModel: ObservableObject { changeUnreadCounter(user: user, by: 1) } + 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) } @@ -878,8 +1055,20 @@ final class ChatModel: ObservableObject { } 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) { @@ -887,7 +1076,7 @@ final class ChatModel: ObservableObject { } func decreaseGroupReportsCounter(_ chatId: ChatId, by: Int = 1) { - changeGroupReportsCounter(chatId, -1) + changeGroupReportsCounter(chatId, -by) } private func changeGroupReportsCounter(_ chatId: ChatId, _ by: Int = 0) { @@ -908,7 +1097,7 @@ final class ChatModel: ObservableObject { var count = 0 var ns: [String] = [] if let ciCategory = chatItem.mergeCategory, - var i = getChatItemIndex(chatItem) { + var i = getChatItemIndex(im, chatItem) { // TODO [knocking] review: use getCIItemsModel? while i < im.reversedChatItems.count { let ci = im.reversedChatItems[i] if ci.mergeCategory != ciCategory { break } @@ -924,7 +1113,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) { + if let i = getChatItemIndex(im, ci) { // TODO [knocking] review: use getCIItemsModel? (i, i > 0 ? im.reversedChatItems[i - 1] : nil) } else { (nil, nil) @@ -948,12 +1137,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) = im.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) @@ -1030,7 +1224,7 @@ final class ChatModel: ObservableObject { func removeWallpaperFilesFromChat(_ chat: Chat) { if case let .direct(contact) = chat.chatInfo { removeWallpaperFilesFromTheme(contact.uiThemes) - } else if case let .group(groupInfo) = chat.chatInfo { + } else if case let .group(groupInfo, _) = chat.chatInfo { removeWallpaperFilesFromTheme(groupInfo.uiThemes) } } @@ -1051,7 +1245,6 @@ struct ShowingInvitation { } struct NTFContactRequest { - var incognito: Bool var chatId: String } @@ -1082,35 +1275,30 @@ final class Chat: ObservableObject, Identifiable, ChatLike { ) } - 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 userIsObserver: Bool { - switch chatInfo { - case let .group(groupInfo): - let m = groupInfo.membership - return m.memberActive && m.memberRole == .observer - default: return false - } - } - var unreadTag: Bool { - chatInfo.ntfsEnabled && (chatStats.unreadCount > 0 || chatStats.unreadChat) + switch chatInfo.chatSettings?.enableNtfs { + case .all: chatStats.unreadChat || chatStats.unreadCount > 0 + case .mentions: chatStats.unreadChat || chatStats.unreadMentions > 0 + default: chatStats.unreadChat + } } - + var id: ChatId { get { chatInfo.id } } var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } } + var supportUnreadCount: Int { + switch chatInfo { + case let .group(groupInfo, _): + if groupInfo.canModerate { + return groupInfo.membersRequireAttention + } else { + return groupInfo.membership.supportChat?.unread ?? 0 + } + default: return 0 + } + } + 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 6c33031eeb..79f4ef2f09 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -12,7 +12,6 @@ import UIKit import SimpleXChat let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT" -let ntfActionAcceptContactIncognito = "NTF_ACT_ACCEPT_CONTACT_INCOGNITO" let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL" let ntfActionRejectCall = "NTF_ACT_REJECT_CALL" @@ -59,13 +58,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { logger.debug("NtfManager.processNotificationResponse changeActiveUser") changeActiveUser(userId, viewPwd: nil) } - if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito), + if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact, let chatId = content.userInfo["chatId"] as? String { - let incognito = action == ntfActionAcceptContactIncognito if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo { - Task { await acceptContactRequest(incognito: incognito, contactRequest: contactRequest) } + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } } else { - chatModel.ntfContactRequest = NTFContactRequest(incognito: incognito, chatId: chatId) + chatModel.ntfContactRequest = NTFContactRequest(chatId: chatId) } } else if let (chatId, ntfAction) = ntfCallAction(content, action) { if let invitation = chatModel.callInvitations.removeValue(forKey: chatId) { @@ -161,10 +159,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { identifier: ntfActionAcceptContact, title: NSLocalizedString("Accept", comment: "accept contact request via notification"), options: .foreground - ), UNNotificationAction( - identifier: ntfActionAcceptContactIncognito, - title: NSLocalizedString("Accept incognito", comment: "accept contact request via notification"), - options: .foreground ) ], intentIdentifiers: [], @@ -248,7 +242,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) { logger.debug("NtfManager.notifyMessageReceived") - if cInfo.ntfsEnabled { + if cInfo.ntfsEnabled(chatItem: cItem) { addNotification(createMessageReceivedNtf(user, cInfo, cItem, 0)) } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 2380f79d59..5a042a6252 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -11,40 +11,40 @@ import UIKit import Dispatch import BackgroundTasks import SwiftUI -import SimpleXChat +@preconcurrency import SimpleXChat private var chatController: chat_ctrl? -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) } } } @@ -86,18 +86,24 @@ 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, log: Bool = true) -> ChatResponse { +@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, retryNum: Int32 = 0, log: Bool = true) -> APIResult { if log { logger.debug("chatSendCmd \(cmd.cmdType)") } let start = Date.now - let resp = bgTask - ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) } - : sendSimpleXCmd(cmd, ctrl) + let resp: APIResult = bgTask + ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl, retryNum: retryNum) } + : sendSimpleXCmd(cmd, ctrl, retryNum: retryNum) if log { logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") - if case let .response(_, json) = resp { - logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") + if case let .invalid(_, json) = resp { + logger.debug("chatSendCmd \(cmd.cmdType) response: \(dataToString(json))") } Task { await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp) @@ -106,35 +112,143 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = return resp } -func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil, log: Bool = true) async -> ChatResponse { - await withCheckedContinuation { cont in - cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl, log: log)) +@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) +} + +func chatApiSendCmdWithRetry(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, inProgress: BoxedValue? = nil, retryNum: Int32 = 0) async -> APIResult? { + let r: APIResult = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum) + if inProgress == nil || inProgress?.boxedValue == true, + case let .error(e) = r, let alert = retryableNetworkErrorAlert(e) { + return await withCheckedContinuation { cont in + showRetryAlert( + alert, + onCancel: { _ in + cont.resume(returning: nil) + }, + onRetry: { + let r1: APIResult? = await chatApiSendCmdWithRetry(cmd, bgTask: bgTask, bgDelay: bgDelay, inProgress: inProgress, retryNum: retryNum + 1) + cont.resume(returning: r1) + } + ) + } + } else { + return r } } -func chatRecvMsg(_ ctrl: chat_ctrl? = nil) async -> ChatResponse? { +@inline(__always) +func showRetryAlert(_ alert: (title: String, message: String), onCancel: @escaping (UIAlertAction) -> Void, onRetry: @escaping () async -> Void) { + DispatchQueue.main.async { + showAlert( + alert.title, + message: alert.message, + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .cancel, + handler: onCancel + ), + UIAlertAction( + title: NSLocalizedString("Retry", comment: "alert action"), + style: .default, + handler: { _ in Task(operation: onRetry) } + ) + ]} + ) + } +} + +func retryableNetworkErrorAlert(_ e: ChatError) -> (title: String, message: String)? { + switch e { + case let .errorAgent(.BROKER(addr, .TIMEOUT)): ( + title: NSLocalizedString("Connection timeout", comment: "alert title"), + message: serverErrorAlertMessage(addr) + ) + case let .errorAgent(.BROKER(addr, .NETWORK(.unknownCAError))): nil + case let .errorAgent(.BROKER(addr, .NETWORK)): ( + title: NSLocalizedString("Connection error", comment: "alert title"), + message: serverErrorAlertMessage(addr) + ) + case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.TIMEOUT)))): ( + title: NSLocalizedString("Private routing timeout", comment: "alert title"), + message: proxyErrorAlertMessage(serverAddress) + ) + case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.NETWORK(.unknownCAError))))): nil + case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.NETWORK)))): ( + title: NSLocalizedString("Private routing error", comment: "alert title"), + message: proxyErrorAlertMessage(serverAddress) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.TIMEOUT))))): ( + title: NSLocalizedString("Private routing timeout", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.NETWORK(.unknownCAError)))))): nil + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.NETWORK))))): ( + title: NSLocalizedString("Private routing error", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.NO_SESSION)))): ( + title: NSLocalizedString("No private routing session", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + default: nil + } +} + +func serverErrorAlertMessage(_ addr: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: "alert message"), serverHostname(addr)) +} + +func proxyErrorAlertMessage(_ addr: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: "alert message"), serverHostname(addr)) +} + +func proxyDestinationErrorAlertMessage(proxyServer: String, destServer: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: "alert message"), serverHostname(proxyServer), serverHostname(destServer)) +} + +@inline(__always) +func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) async -> APIResult { await withCheckedContinuation { cont in - _ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in - let resp = recvSimpleXMsg(ctrl) - cont.resume(returning: resp) - return resp + cont.resume(returning: chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, retryNum: retryNum, log: log)) + } +} + +@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) { () -> 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?, pastTimestamp: Bool = false, ctrl: chat_ctrl? = nil) throws -> User { - let r = chatSendCmdSync(.createActiveUser(profile: p, pastTimestamp: pastTimestamp), ctrl) + let r: ChatResponse0 = try chatSendCmdSync(.createActiveUser(profile: p, pastTimestamp: pastTimestamp), ctrl: ctrl) if case let .activeUser(user) = r { return user } - throw r + throw r.unexpected } func listUsers() throws -> [UserInfo] { @@ -145,41 +259,39 @@ 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 apiSetUserAutoAcceptMemberContacts(_ userId: Int64, enable: Bool) async throws { + try await sendCommandOkResp(.apiSetUserAutoAcceptMemberContacts(userId: userId, enable: enable)) } func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User { @@ -199,90 +311,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, enableSndFiles: 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 = chatSendCmdSync(.checkChatRunning) + let r: ChatResponse0 = try chatSendCmdSync(.checkChatRunning) switch r { case .chatRunning: return true case .chatStopped: return false - default: throw r + 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 apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, ctrl: chat_ctrl? = nil) throws { - let r = chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl) + let r: ChatResponse2 = try chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl: 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 -> [ArchiveError] { - let r = await chatSendCmd(.apiExportArchive(config: config)) + let r: ChatResponse2 = try await chatSendCmd(.apiExportArchive(config: config)) if case let .archiveExported(archiveErrors) = r { return archiveErrors } - throw r + 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 { @@ -293,8 +403,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] { @@ -307,92 +417,92 @@ 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 apiGetChatTags() throws -> [ChatTag] { let userId = try currentUserId("apiGetChatTags") - let r = chatSendCmdSync(.apiGetChatTags(userId: userId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiGetChatTags(userId: userId)) if case let .chatTags(_, tags) = r { return tags } - throw r + throw r.unexpected } func apiGetChatTagsAsync() async throws -> [ChatTag] { let userId = try currentUserId("apiGetChatTags") - let r = await chatSendCmd(.apiGetChatTags(userId: userId)) + let r: ChatResponse0 = try await chatSendCmd(.apiGetChatTags(userId: userId)) if case let .chatTags(_, tags) = r { return tags } - throw r + throw r.unexpected } let loadItemsPerPage = 50 -func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws -> Chat { - let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search)) - if case let .apiChat(_, chat) = r { return Chat.init(chat) } - throw r +func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag? = nil, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) { + let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, scope: scope, contentTag: contentTag, pagination: pagination, search: search)) + if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) } + 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 loadChat(chat: Chat, im: ItemsModel, search: String = "", clearItems: Bool = true) async { + await loadChat(chatId: chat.chatInfo.id, im: im, search: search, clearItems: clearItems) } -func loadChat(chat: Chat, search: String = "", clearItems: Bool = true, replaceChat: Bool = false) async { - do { - let cInfo = chat.chatInfo - let m = ChatModel.shared - let im = ItemsModel.shared - m.chatItemStatuses = [:] +func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async { + await MainActor.run { if clearItems { - await MainActor.run { im.reversedChatItems = [] } + im.reversedChatItems = [] + im.chatState.clear() } - let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) - await MainActor.run { - im.reversedChatItems = chat.chatItems.reversed() - m.updateChatInfo(chat.chatInfo) - if (replaceChat) { - m.replaceChat(chat.chatInfo.id, chat) - } - } - } catch let error { - logger.error("loadChat error: \(responseError(error))") } + await apiLoadMessages( + chatId, + im, + ( // pagination + openAroundItemId != nil + ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) + : ( + search == "" + ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage) + ) + ), + 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)) +func apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) async throws -> ChatItemInfo { + let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, scope: scope, itemId: itemId)) if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo } - throw r + throw r.unexpected } -func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) { - let r = await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds)) +func apiPlanForwardChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) { + let r: ChatResponse1 = try await chatSendCmd(.apiPlanForwardChatItems(fromChatType: type, fromChatId: id, fromScope: scope, itemIds: itemIds)) if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) } - throw r + 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) +func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { + let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, toScope: toScope, fromChatType: fromChatType, fromChatId: fromChatId, fromScope: fromScope, itemIds: itemIds, ttl: ttl) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) } func apiCreateChatTag(tag: ChatTagData) async throws -> [ChatTag] { - let r = await chatSendCmd(.apiCreateChatTag(tag: tag)) + let r: ChatResponse0 = try await chatSendCmd(.apiCreateChatTag(tag: tag)) if case let .chatTags(_, userTags) = r { return userTags } - throw r + throw r.unexpected } func apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64]) async throws -> ([ChatTag], [Int64]) { - let r = await chatSendCmd(.apiSetChatTags(type: type, id: id, tagIds: tagIds)) + 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 + throw r.unexpected } func apiDeleteChatTag(tagId: Int64) async throws { @@ -407,14 +517,14 @@ 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) +func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? { + let cmd: ChatCommand = .apiSendMessages(type: type, id: id, scope: scope, live: live, ttl: ttl, composedMessages: composedMessages) return await processSendMessageCmd(toChatType: type, cmd: cmd) } private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> [ChatItem]? { let chatModel = ChatModel.shared - let r: ChatResponse + let r: APIResult if toChatType == .direct { var cItem: ChatItem? = nil let endTask = beginBGTask({ @@ -424,8 +534,8 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async } } }) - r = await chatSendCmd(cmd, bgTask: false) - if case let .newChatItems(_, aChatItems) = r { + 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 @@ -436,40 +546,40 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async 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 .newChatItems(_, aChatItems) = r { + 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 apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) async -> [ChatItem]? { - let r = await chatSendCmd(.apiCreateChatItems(noteFolderId: noteFolderId, composedMessages: composedMessages)) - if case let .newChatItems(_, aChatItems) = r { return aChatItems.map { $0.chatItem } } - createChatItemsErrorAlert(r) + let r: APIResult = await chatApiSendCmd(.apiCreateChatItems(noteFolderId: noteFolderId, composedMessages: composedMessages)) + if case let .result(.newChatItems(_, aChatItems)) = r { return aChatItems.map { $0.chatItem } } + createChatItemsErrorAlert(r.unexpected) return nil } func apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String) async -> [ChatItem]? { - let r = await chatSendCmd(.apiReportMessage(groupId: groupId, chatItemId: chatItemId, reportReason: reportReason, reportText: reportText)) - if case let .newChatItems(_, aChatItems) = r { return aChatItems.map { $0.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))" + message: "Error: \(responseError(r.unexpected))" ) return nil } -private func sendMessageErrorAlert(_ r: ChatResponse) { +private func sendMessageErrorAlert(_ r: ChatError) { logger.error("send message error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error sending message", @@ -477,7 +587,7 @@ private func sendMessageErrorAlert(_ r: ChatResponse) { ) } -private func createChatItemsErrorAlert(_ r: ChatResponse) { +private func createChatItemsErrorAlert(_ r: ChatError) { logger.error("apiCreateChatItems error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error creating message", @@ -485,42 +595,57 @@ private func createChatItemsErrorAlert(_ r: ChatResponse) { ) } -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, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem { + let r: ChatResponse1 = try await chatSendCmd(.apiUpdateChatItem(type: type, id: id, scope: scope, 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) +func apiChatItemReaction(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem { + let r: ChatResponse1 = try await chatSendCmd(.apiChatItemReaction(type: type, id: id, scope: scope, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay) if case let .chatItemReaction(_, _, reaction) = r { return reaction.chatReaction.chatItem } - throw r + throw r.unexpected } func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction) async throws -> [MemberReaction] { let userId = try currentUserId("apiGetReactionMemebers") - let r = await chatSendCmd(.apiGetReactionMembers(userId: userId, groupId: groupId, itemId: itemId, reaction: reaction )) + 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 + throw r.unexpected } -func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { - let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay) +func apiDeleteChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChatItem(type: type, id: id, scope: scope, itemIds: itemIds, mode: mode), bgDelay: msgDelay) if case let .chatItemsDeleted(_, items, _) = r { return items } - throw r + throw r.unexpected } func apiDeleteMemberChatItems(groupId: Int64, itemIds: [Int64]) async throws -> [ChatItemDeletion] { - let r = await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, itemIds: itemIds), bgDelay: msgDelay) + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, itemIds: itemIds), bgDelay: msgDelay) if case let .chatItemsDeleted(_, items, _) = r { return items } - throw r + 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) @@ -528,9 +653,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) { @@ -542,7 +667,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))") } @@ -550,90 +680,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 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 = await chatSendCmd(.apiGetServerOperators) + let r: ChatResponse0 = try await chatSendCmd(.apiGetServerOperators) if case let .serverOperatorConditions(conditions) = r { return conditions } logger.error("getServerOperators error: \(String(describing: r))") - throw r + throw r.unexpected } func getServerOperatorsSync() throws -> ServerOperatorConditions { - let r = chatSendCmdSync(.apiGetServerOperators) + let r: ChatResponse0 = try chatSendCmdSync(.apiGetServerOperators) if case let .serverOperatorConditions(conditions) = r { return conditions } logger.error("getServerOperators error: \(String(describing: r))") - throw r + throw r.unexpected } func setServerOperators(operators: [ServerOperator]) async throws -> ServerOperatorConditions { - let r = await chatSendCmd(.apiSetServerOperators(operators: operators)) + 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 + throw r.unexpected } func getUserServers() async throws -> [UserOperatorServers] { let userId = try currentUserId("getUserServers") - let r = await chatSendCmd(.apiGetUserServers(userId: userId)) + 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 + throw r.unexpected } func setUserServers(userServers: [UserOperatorServers]) async throws { let userId = try currentUserId("setUserServers") - let r = await chatSendCmd(.apiSetUserServers(userId: userId, userServers: userServers)) + 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 + throw r.unexpected } func validateServers(userServers: [UserOperatorServers]) async throws -> [UserServersError] { let userId = try currentUserId("validateServers") - let r = await chatSendCmd(.apiValidateServers(userId: userId, userServers: userServers)) + 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 + throw r.unexpected } func getUsageConditions() async throws -> (UsageConditions, String?, UsageConditions?) { - let r = await chatSendCmd(.apiGetUsageConditions) + 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 + throw r.unexpected } func setConditionsNotified(conditionsId: Int64) async throws { - let r = await chatSendCmd(.apiSetConditionsNotified(conditionsId: conditionsId)) + let r: ChatResponse2 = try await chatSendCmd(.apiSetConditionsNotified(conditionsId: conditionsId)) if case .cmdOk = r { return } logger.error("setConditionsNotified error: \(String(describing: r))") - throw r + throw r.unexpected } func acceptConditions(conditionsId: Int64, operatorIds: [Int64]) async throws -> ServerOperatorConditions { - let r = await chatSendCmd(.apiAcceptConditions(conditionsId: conditionsId, operatorIds: operatorIds)) + 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 + throw r.unexpected } func getChatItemTTL() throws -> ChatItemTTL { @@ -646,7 +815,7 @@ func getChatItemTTLAsync() async throws -> ChatItemTTL { return try chatItemTTLResponse(await chatSendCmd(.apiGetChatItemTTL(userId: userId))) } -private func chatItemTTLResponse(_ r: ChatResponse) throws -> ChatItemTTL { +private func chatItemTTLResponse(_ r: ChatResponse0) throws -> ChatItemTTL { if case let .chatItemTTL(_, chatItemTTL) = r { if let ttl = chatItemTTL { return ChatItemTTL(ttl) @@ -654,7 +823,7 @@ private func chatItemTTLResponse(_ r: ChatResponse) throws -> ChatItemTTL { throw RuntimeError("chatItemTTLResponse: invalid ttl") } } - throw r + throw r.unexpected } func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws { @@ -668,21 +837,21 @@ func setChatTTL(chatType: ChatType, id: Int64, _ chatItemTTL: ChatTTL) async thr } 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 { @@ -703,131 +872,133 @@ 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 apiGroupMemberInfoSync(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) { - let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) + 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 = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) + let r: ChatResponse0 = try await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) } - throw r + throw r.unexpected } -func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { - let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId)) - if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } - throw r +func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo)? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiContactQueueInfo(contactId: contactId)) + if case let .result(.queueInfo(_, rcvMsgInfo, queueInfo)) = r { return (rcvMsgInfo, queueInfo) } + if let r { throw r.unexpected } else { return nil } } -func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { - let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) - if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } - throw r +func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo)? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) + if case let .result(.queueInfo(_, rcvMsgInfo, queueInfo)) = r { return (rcvMsgInfo, queueInfo) } + if let r { throw r.unexpected } else { return nil } } 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) async throws -> (GroupMember, String) { - let r = await chatSendCmd(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId)) + 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 alert = connectionErrorAlert(r) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) + if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) } + let alert: Alert? = if let r { connectionErrorAlert(r) } else { nil } 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 apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? { - let r = await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId)) - - if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection} - throw r + let r: APIResult? = await chatApiSendCmdWithRetry(.apiChangeConnectionUser(connId: connId, userId: userId)) + if case let .result(.connectionUserChanged(_, _, toConnection, _)) = r {return toConnection} + if let r { throw r.unexpected } else { return nil } } -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 apiConnectPlan(connLink: String, inProgress: BoxedValue) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { + guard let userId = ChatModel.shared.currentUser?.userId else { + logger.error("apiConnectPlan: no current user") + return (nil, nil) + } + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink), inProgress: inProgress) + if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } + let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } + return (nil, alert) } -func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, PendingContactConnection)? { - let (r, alert) = await apiConnect_(incognito: incognito, connReq: connReq) +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 @@ -836,38 +1007,49 @@ 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 chatApiSendCmdWithRetry(.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) { ItemsModel.shared.loadOpenChat(c.id) } let alert = contactAlreadyExistsAlert(contact) return (nil, alert) - case .chatCmdError(_, .error(.invalidConnReq)): - let alert = mkAlert( + default: () + } + let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } + return (nil, alert) +} + +private func apiConnectResponseAlert(_ r: APIResult) -> Alert { + switch r.unexpected { + 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(.SMP(_, .BLOCKED(info)))): - let alert = Alert( + 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")), @@ -877,25 +1059,22 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi } } ) - return (nil, alert) - case .chatCmdError(_, .errorAgent(.SMP(_, .QUOTA))): - let alert = mkAlert( + case .errorAgent(.SMP(_, .QUOTA)): + mkAlert( title: "Undelivered messages", message: "The connection reached the limit of undelivered messages, your contact may be offline." ) - return (nil, alert) - case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))): + 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)))." + message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(internalErr))." ) - return (nil, alert) + } else { + connectionErrorAlert(r) } - default: () + default: connectionErrorAlert(r) } - let alert = connectionErrorAlert(r) - return (nil, alert) } func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { @@ -905,38 +1084,81 @@ 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: \(responseError(r))" + message: "Error: \(responseError(r.unexpected))" ) } } +func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) async throws -> ChatData { + let userId = try currentUserId("apiPrepareContact") + let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData)) + if case let .newPreparedChat(_, chat) = r { return chat } + throw r.unexpected +} + +func apiPrepareGroup(connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData) async throws -> ChatData { + let userId = try currentUserId("apiPrepareGroup") + let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, groupShortLinkData: groupShortLinkData)) + if case let .newPreparedChat(_, chat) = r { return chat } + throw r.unexpected +} + +func apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) async throws -> Contact { + let r: ChatResponse1 = try await chatSendCmd(.apiChangePreparedContactUser(contactId: contactId, newUserId: newUserId)) + if case let .contactUserChanged(_, _, _, toContact) = r {return toContact} + throw r.unexpected +} + +func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws -> GroupInfo { + let r: ChatResponse1 = try await chatSendCmd(.apiChangePreparedGroupUser(groupId: groupId, newUserId: newUserId)) + if case let .groupUserChanged(_, _, _, toGroup) = r {return toGroup} + throw r.unexpected +} + +func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async -> Contact? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) + if case let .result(.startedConnectionToContact(_, contact)) = r { return contact } + if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + return nil +} + +func apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) async -> GroupInfo? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg)) + if case let .result(.startedConnectionToGroup(_, groupInfo)) = r { return groupInfo } + if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + return nil +} + func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Contact?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { 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 alert = connectionErrorAlert(r) - return (nil, alert) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) + if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) } + if let r { + logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") + return (nil, connectionErrorAlert(r)) + } else { + return (nil, nil) + } } 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, chatDeleteMode: chatDeleteMode), 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 apiDeleteContact(id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws -> Contact { @@ -950,9 +1172,9 @@ func apiDeleteContact(id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } } } - let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, chatDeleteMode: chatDeleteMode), bgTask: false) + 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 + throw r.unexpected } func deleteChat(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async { @@ -1003,9 +1225,9 @@ func deleteContactChat(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(noti 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 { @@ -1020,147 +1242,174 @@ 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 = await chatSendCmd(.apiSetGroupAlias(groupId: groupId, localAlias: localAlias)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetGroupAlias(groupId: groupId, localAlias: localAlias)) if case let .groupAliasUpdated(_, toGroup) = r { return toGroup } - throw r + 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 apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) async -> Bool { - let r = await chatSendCmd(.apiSetUserUIThemes(userId: userId, themes: themes)) - if case .cmdOk = r { return true } - logger.error("apiSetUserUIThemes bad response: \(String(describing: r))") - return false + 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 { - let r = await chatSendCmd(.apiSetChatUIThemes(chatId: chatId, themes: themes)) - if case .cmdOk = r { return true } - logger.error("apiSetChatUIThemes bad response: \(String(describing: r))") - return false + do { + try await sendCommandOkResp(.apiSetChatUIThemes(chatId: chatId, themes: themes)) + return true + } catch { + logger.error("apiSetChatUIThemes bad response: \(responseError(error))") + return false + } } -func apiCreateUserAddress() async throws -> String { +func apiCreateUserAddress() 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: APIResult? = await chatApiSendCmdWithRetry(.apiCreateMyAddress(userId: userId)) + if case let .result(.userContactLinkCreated(_, connLink)) = r { return connLink } + if case let .error(.errorAgent(.NOTICE(server, preset, expires))) = r { + showClientNotice(server, preset, expires) + return nil + } + if let r { throw r.unexpected } else { return nil } } func apiDeleteUserAddress() async throws -> User? { let userId = try currentUserId("apiDeleteUserAddress") - let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId)) - if case let .userContactLinkDeleted(user) = r { return user } - throw r + let r: APIResult? = await chatApiSendCmdWithRetry(.apiDeleteMyAddress(userId: userId)) + if case let .result(.userContactLinkDeleted(user)) = r { return user } + if let r { throw r.unexpected } else { return nil } } 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)) +func apiAddMyAddressShortLink() async throws -> UserContactLink? { + let userId = try currentUserId("apiAddMyAddressShortLink") + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddMyAddressShortLink(userId: userId)) + if case let .result(.userContactLink(_, contactLink)) = r { return contactLink } + if let r { throw r.unexpected } else { return nil } +} + +func apiSetUserAddressSettings(_ settings: AddressSettings) async throws -> UserContactLink? { + let userId = try currentUserId("apiSetUserAddressSettings") + let r: APIResult? = await chatApiSendCmdWithRetry(.apiSetAddressSettings(userId: userId, addressSettings: settings)) 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: if let r { throw r.unexpected } else { return nil } } } func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { - let r = await chatSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) + let r: APIResult? = await chatApiSendCmdWithRetry(.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." ) - } else if let networkErrorAlert = networkErrorAlert(r) { - am.showAlert(networkErrorAlert) - } else { - logger.error("apiAcceptContactRequest error: \(String(describing: r))") - am.showAlertMsg( - title: "Error accepting contact request", - message: "Error: \(responseError(r))" - ) + } else if let r { + if let networkErrorAlert = networkErrorAlert(r) { + am.showAlert(networkErrorAlert) + } else { + logger.error("apiAcceptContactRequest error: \(String(describing: r))") + am.showAlertMsg( + title: "Error accepting contact request", + message: "Error: \(responseError(r.unexpected))" + ) + } } return nil } -func apiRejectContactRequest(contactReqId: Int64) async throws { - let r = await chatSendCmd(.apiRejectContact(contactReqId: contactReqId)) - if case .contactRequestRejected = r { return } - throw r +func apiRejectContactRequest(contactReqId: Int64) async throws -> Contact? { + let r: ChatResponse1 = try await chatSendCmd(.apiRejectContact(contactReqId: contactReqId)) + if case let .contactRequestRejected(_, _, contact_) = r { return contact_ } + throw r.unexpected } func apiChatRead(type: ChatType, id: Int64) async throws { - try await sendCommandOkResp(.apiChatRead(type: type, id: id)) + try await sendCommandOkResp(.apiChatRead(type: type, id: id, scope: nil)) } -func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws { - try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds)) +func apiSupportChatRead(type: ChatType, id: Int64, scope: GroupChatScope) async throws -> (GroupInfo, GroupMember) { + let r: ChatResponse2 = try await chatSendCmd(.apiChatRead(type: type, id: id, scope: scope)) + if case let .memberSupportChatRead(_, groupInfo, member) = r { return (groupInfo, member) } + throw r.unexpected +} + +func apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> ChatInfo { + let r: ChatResponse1 = try await chatSendCmd(.apiChatItemsRead(type: type, id: id, scope: scope, itemIds: itemIds)) + if case let .itemsReadForChat(_, updatedChatInfo) = r { return updatedChatInfo } + throw r.unexpected } func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws { @@ -1168,31 +1417,33 @@ 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, responseError(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, responseError(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 } } @@ -1207,12 +1458,12 @@ func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = f } func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool = false, auto: Bool = false) async { - var fileIdsToApprove = [Int64]() - var srvsToApprove = Set() - var otherFileErrs = [ChatResponse]() + var fileIdsToApprove: [Int64] = [] + var srvsToApprove: Set = [] + var otherFileErrs: [APIResult] = [] for fileId in fileIds { - let r = await chatSendCmd( + let r: APIResult = await chatApiSendCmd( .receiveFile( fileId: fileId, userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(), @@ -1221,32 +1472,22 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool ) ) switch r { - case let .rcvFileAccepted(_, chatItem): + 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: - if let chatError = chatError(r) { - switch chatError { - case let .fileNotApproved(fileId, unknownServers): - fileIdsToApprove.append(fileId) - srvsToApprove.formUnion(unknownServers) - default: - otherFileErrs.append(r) - } - } + otherFileErrs.append(r) } } if !auto { - let otherErrsStr = if otherFileErrs.isEmpty { - "" - } else if otherFileErrs.count == 1 { - "\(otherFileErrs[0])" - } else if otherFileErrs.count == 2 { - "\(otherFileErrs[0])\n\(otherFileErrs[1])" - } else { - "\(otherFileErrs[0])\n\(otherFileErrs[1])\nand \(otherFileErrs.count - 2) other error(s)" - } - + 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 @@ -1282,7 +1523,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool } else if otherFileErrs.count == 1 { // If there is a single other error, we differentiate on it let errorResponse = otherFileErrs.first! switch errorResponse { - case let .rcvFileAcceptedSndCancelled(_, rcvFileTransfer): + case let .result(.rcvFileAcceptedSndCancelled(_, rcvFileTransfer)): logger.debug("receiveFiles error: sender cancelled file transfer \(rcvFileTransfer.fileId)") await MainActor.run { showAlert( @@ -1290,19 +1531,14 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool 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: - if let chatError = chatError(errorResponse) { - switch chatError { - case .fileCancelled, .fileAlreadyReceiving: - logger.debug("receiveFiles ignoring FileCancelled or FileAlreadyReceiving error") - default: - await MainActor.run { - showAlert( - NSLocalizedString("Error receiving file", comment: "alert title"), - message: responseError(errorResponse) - ) - } - } + 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 @@ -1314,6 +1550,20 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool } } } + + 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 { @@ -1324,12 +1574,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 } } @@ -1339,9 +1589,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 { @@ -1349,21 +1599,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 { @@ -1374,37 +1624,60 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws { try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId)) } -func networkErrorAlert(_ r: ChatResponse) -> Alert? { - if let alert = getNetworkErrorAlert(r) { +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 } } -func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) async { - if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequest.apiId) { +func acceptContactRequest(incognito: Bool, contactRequestId: Int64, inProgress: Binding? = nil) async { + await MainActor.run { inProgress?.wrappedValue = true } + if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequestId) { let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) await MainActor.run { - ChatModel.shared.replaceChat(contactRequest.id, chat) - NetworkModel.shared.setContactNetworkStatus(contact, .connected) + if contact.contactRequestId != nil { // means contact request was initially created with contact, so we don't need to replace it + ChatModel.shared.updateContact(contact) + } else { + ChatModel.shared.replaceChat(contactRequestChatId(contactRequestId), chat) + } + inProgress?.wrappedValue = false } if contact.sndReady { + let chatId = chat.id DispatchQueue.main.async { dismissAllSheets(animated: true) { - ItemsModel.shared.loadOpenChat(chat.id) + ItemsModel.shared.loadOpenChat(chatId) } } } + } else { + await MainActor.run { inProgress?.wrappedValue = false } } } -func rejectContactRequest(_ contactRequest: UserContactRequest) async { +func rejectContactRequest(_ contactRequestId: Int64, dismissToChatList: Bool = false) async { do { - try await apiRejectContactRequest(contactReqId: contactRequest.apiId) - DispatchQueue.main.async { ChatModel.shared.removeChat(contactRequest.id) } + let contact_ = try await apiRejectContactRequest(contactReqId: contactRequestId) + await MainActor.run { + if let contact = contact_ { // means contact request was initially created with contact, so we need to remove contact chat + ChatModel.shared.removeChat(contact.id) + } else { + ChatModel.shared.removeChat(contactRequestChatId(contactRequestId)) + } + if dismissToChatList { + ChatModel.shared.chatId = nil + } + } } catch let error { logger.error("rejectContactRequest: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error rejecting contact request", comment: "alert title"), + message: responseError(error) + ) + } } } @@ -1437,15 +1710,15 @@ func apiEndCall(_ contact: Contact) async throws { } func apiGetCallInvitationsSync() throws -> [RcvCallInvitation] { - let r = chatSendCmdSync(.apiGetCallInvitations) + 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 = await chatSendCmd(.apiGetCallInvitations) + let r: ChatResponse2 = try await chatSendCmd(.apiGetCallInvitations) if case let .callInvitations(invs) = r { return invs } - throw r + throw r.unexpected } func apiCallStatus(_ contact: Contact, _ status: String) async throws { @@ -1456,19 +1729,13 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws { } } -func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] { - let r = chatSendCmdSync(.apiGetNetworkStatuses) - if case let .networkStatuses(_, statuses) = r { return statuses } - throw r -} - -func markChatRead(_ chat: Chat) async { +func markChatRead(_ im: ItemsModel, _ chat: Chat) async { do { if chat.chatStats.unreadCount > 0 { let cInfo = chat.chatInfo try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId) await MainActor.run { - withAnimation { ChatModel.shared.markChatItemsRead(cInfo) } + withAnimation { ChatModel.shared.markAllChatItemsRead(im, cInfo) } } } if chat.chatStats.unreadChat { @@ -1491,40 +1758,55 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async { } } -func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) async { +func markSupportChatRead(_ groupInfo: GroupInfo, _ member: GroupMember) async { do { - try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds) - DispatchQueue.main.async { - ChatModel.shared.markChatItemsRead(cInfo, itemIds) + if member.supportChatNotRead { + let (updatedGroupInfo, updatedMember) = try await apiSupportChatRead(type: .group, id: groupInfo.apiId, scope: .memberSupport(groupMemberId_: member.groupMemberId)) + await MainActor.run { + _ = ChatModel.shared.upsertGroupMember(updatedGroupInfo, updatedMember) + ChatModel.shared.updateGroup(updatedGroupInfo) + } + } + } catch { + logger.error("markSupportChatRead apiChatRead error: \(responseError(error))") + } +} + +func apiMarkChatItemsRead(_ im: ItemsModel, _ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async { + do { + let updatedChatInfo = try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemIds: itemIds) + await MainActor.run { + ChatModel.shared.updateChatInfo(updatedChatInfo) + ChatModel.shared.markChatItemsRead(im, cInfo, itemIds, mentionsRead) } } catch { 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 { @@ -1533,32 +1815,44 @@ enum JoinGroupResult { case groupNotFound } -func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { - let r = await chatSendCmd(.apiJoinGroup(groupId: groupId)) +func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult? { + let r: APIResult? = await chatApiSendCmdWithRetry(.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: if let r { throw r.unexpected } else { return nil } } } -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 apiAcceptMember(_ groupId: Int64, _ groupMemberId: Int64, _ memberRole: GroupMemberRole) async throws -> (GroupInfo, GroupMember) { + let r: ChatResponse2 = try await chatSendCmd(.apiAcceptMember(groupId: groupId, groupMemberId: groupMemberId, memberRole: memberRole)) + if case let .memberAccepted(_, groupInfo, member) = r { return (groupInfo, member) } + 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 apiDeleteMemberSupportChat(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupInfo, GroupMember) { + let r: ChatResponse2 = try await chatSendCmd(.apiDeleteMemberSupportChat(groupId: groupId, groupMemberId: groupMemberId)) + if case let .memberSupportChatDeleted(_, groupInfo, member) = r { return (groupInfo, member) } + 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 apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool) async throws -> (GroupInfo, [GroupMember]) { + let r: ChatResponse2 = try await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds, withMessages: withMessages), bgTask: false) + if case let .userDeletedMembers(_, updatedGroupInfo, members, _withMessages) = r { return (updatedGroupInfo, members) } + throw r.unexpected +} + +func apiMembersRole(_ groupId: Int64, _ memberIds: [Int64], _ memberRole: GroupMemberRole) async throws -> [GroupMember] { + let r: ChatResponse2 = try await chatSendCmd(.apiMembersRole(groupId: groupId, memberIds: memberIds, memberRole: memberRole), bgTask: false) + if case let .membersRoleUser(_, _, members, _) = r { return members } + throw r.unexpected +} + +func apiBlockMembersForAll(_ groupId: Int64, _ memberIds: [Int64], _ blocked: Bool) async throws -> [GroupMember] { + let r: ChatResponse2 = try await chatSendCmd(.apiBlockMembersForAll(groupId: groupId, memberIds: memberIds, blocked: blocked), bgTask: false) + if case let .membersBlockedForAllUser(_, _, members, _) = r { return members } + throw r.unexpected } func leaveGroup(_ groupId: Int64) async { @@ -1571,92 +1865,129 @@ 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 [] } 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.sendMsgEnabled && !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) } + .compactMap{ c in c.chatInfo.sendMsgEnabled ? c.chatInfo.contact : nil } + .filter{ c in !c.sendMsgToConnect && !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 -> GroupLink? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) + if case let .result(.groupLinkCreated(_, _, groupLink)) = r { return groupLink } + if case let .error(.errorAgent(.NOTICE(server, preset, expires))) = r { + showClientNotice(server, preset, expires) + return nil + } + if let r { throw r.unexpected } else { return nil } } -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 -> GroupLink { + let r: ChatResponse2 = try await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole)) + if case let .groupLink(_, _, groupLink) = r { return groupLink } + throw r.unexpected } func apiDeleteGroupLink(_ groupId: Int64) async throws { - let r = await chatSendCmd(.apiDeleteGroupLink(groupId: groupId)) - if case .groupLinkDeleted = r { return } - throw r + let r: APIResult? = await chatApiSendCmdWithRetry(.apiDeleteGroupLink(groupId: groupId)) + if case .result(.groupLinkDeleted) = r { return } + if let r { throw r.unexpected } } -func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? { - let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId)) +func apiGetGroupLink(_ groupId: Int64) throws -> GroupLink? { + 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(_, _, groupLink)): + return groupLink + case .error(.errorStore(storeError: .groupLinkNotFound)): return nil - default: throw r + default: throw r.unexpected } } +func apiAddGroupShortLink(_ groupId: Int64) async throws -> GroupLink? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddGroupShortLink(groupId: groupId)) + if case let .result(.groupLink(_, _, groupLink)) = r { return groupLink } + if let r { throw r.unexpected } else { return nil } +} + 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 apiAcceptMemberContact(contactId: Int64) async -> Contact? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptMemberContact(contactId: contactId)) + if case let .result(.memberContactAccepted(_, contact)) = r { return contact } + if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + return nil +} + +func acceptMemberContact(contactId: Int64, inProgress: Binding? = nil) async { + await MainActor.run { inProgress?.wrappedValue = true } + if let contact = await apiAcceptMemberContact(contactId: contactId) { + await MainActor.run { + ChatModel.shared.updateContact(contact) + inProgress?.wrappedValue = false + } + if contact.sndReady { + DispatchQueue.main.async { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(contact.id) + } + } + } + } else { + await MainActor.run { inProgress?.wrappedValue = false } + } } 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 = await chatSendCmd(.getAgentSubsTotal(userId: userId), log: false) + 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 + throw r.unexpected } func getAgentServersSummary() throws -> PresentedServersSummary { let userId = try currentUserId("getAgentServersSummary") - let r = chatSendCmdSync(.getAgentServersSummary(userId: userId), log: false) + 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 + throw r.unexpected } func resetAgentServersStats() async throws { @@ -1800,7 +2131,7 @@ private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws { try getUserChatData() } -func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws { +func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?, keepingChatId: String? = nil) async throws { let currentUser = if let userId = userId { try await apiSetActiveUserAsync(userId, viewPwd: viewPwd) } else { @@ -1812,7 +2143,7 @@ func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws { m.currentUser = currentUser m.users = users } - try await getUserChatDataAsync() + try await getUserChatDataAsync(keepingChatId: keepingChatId) await MainActor.run { if let currentUser = currentUser, var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) { invitation.user = currentUser @@ -1834,7 +2165,7 @@ func getUserChatData() throws { tm.updateChatTags(m.chats) } -private func getUserChatDataAsync() async throws { +private func getUserChatDataAsync(keepingChatId: String?) async throws { let m = ChatModel.shared let tm = ChatTagsModel.shared if m.currentUser != nil { @@ -1845,7 +2176,7 @@ private func getUserChatDataAsync() async throws { await MainActor.run { m.userAddress = userAddress m.chatItemTTL = chatItemTTL - m.updateChats(chats) + m.updateChats(chats, keepingChatId: keepingChatId) tm.activeFilter = nil tm.userTags = tags tm.updateChatTags(m.chats) @@ -1866,7 +2197,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() @@ -1884,7 +2215,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) } @@ -1901,12 +2237,8 @@ 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): @@ -1923,14 +2255,15 @@ func processReceivedMsg(_ res: ChatResponse) async { m.dismissConnReqView(conn.id) m.removeChat(conn.id) } + if contact.id == m.chatId, let conn = contact.activeConn { + m.chatAgentConnId = conn.agentConnId + m.chatSubStatus = .active + } } } if contact.directOrUsed { NtfManager.shared.notifyContactConnected(user, contact) } - await MainActor.run { - n.setContactNetworkStatus(contact, .connected) - } case let .contactConnecting(user, contact): if active(user) && contact.directOrUsed { await MainActor.run { @@ -1951,20 +2284,27 @@ func processReceivedMsg(_ res: ChatResponse) async { } } } - await MainActor.run { - n.setContactNetworkStatus(contact, .connected) - } - case let .receivedContactRequest(user, contactRequest): + case let .receivedContactRequest(user, contactRequest, chat_): if active(user) { - let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) await MainActor.run { - if m.hasChat(contactRequest.id) { - m.updateChatInfo(cInfo) + if let chat = chat_ { // means contact request was created with contact, so we need to add/update contact chat + if !m.hasChat(chat.id) { + m.addChat(Chat(chat)) + } else if m.chatId == chat.id { + m.updateChatInfo(chat.chatInfo) + } else { + m.replaceChat(chat.id, Chat(chat)) + } } else { - m.addChat(Chat( - chatInfo: cInfo, - chatItems: [] - )) + let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) + if m.hasChat(contactRequest.id) { + m.updateChatInfo(cInfo) + } else { + m.addChat(Chat( + chatInfo: cInfo, + chatItems: [] + )) + } } } } @@ -1982,39 +2322,16 @@ func processReceivedMsg(_ res: ChatResponse) async { _ = m.upsertGroupMember(groupInfo, toMember) } } - case let .contactsMerged(user, intoContact, mergedContact): - if active(user) && m.hasChat(mergedContact.id) { + case let .subscriptionStatus(status, connections): + if let chatAgentConnId = m.chatAgentConnId, connections.contains(chatAgentConnId) { await MainActor.run { - if m.chatId == mergedContact.id { - ItemsModel.shared.loadOpenChat(mergedContact.id) - } - m.removeChat(mergedContact.id) + m.chatSubStatus = status } } - case let .networkStatus(status, connections): - // dispatch queue to synchronize access - networkStatusesLock.sync { - 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 { - n.networkStatuses = ns - } - } - case let .networkStatuses(_, statuses): () - // dispatch queue to synchronize access - networkStatusesLock.sync { - 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 { - n.networkStatuses = ns + case let .chatInfoUpdated(user, chatInfo): + if active(user) { + await MainActor.run { + m.updateChatInfo(chatInfo) } } case let .newChatItems(user, chatItems): @@ -2027,7 +2344,7 @@ func processReceivedMsg(_ res: ChatResponse) async { if cItem.isActiveReport { m.increaseGroupReportsCounter(cInfo.id) } - } else if cItem.isRcvNew && cInfo.ntfsEnabled { + } else if cItem.isRcvNew && cInfo.ntfsEnabled(chatItem: cItem) { m.increaseUnreadCounter(user: user) } } @@ -2045,7 +2362,7 @@ func processReceivedMsg(_ res: ChatResponse) async { let cInfo = chatItem.chatInfo let cItem = chatItem.chatItem if !cItem.isDeletedContent && active(user) { - await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) } + _ = await MainActor.run { m.upsertChatItem(cInfo, cItem) } } if let endTask = m.messageDelivery[cItem.id] { switch cItem.meta.itemStatus { @@ -2072,7 +2389,8 @@ func processReceivedMsg(_ res: ChatResponse) async { case let .chatItemsDeleted(user, items, _): if !active(user) { for item in items { - if item.toChatItem == nil && item.deletedChatItem.chatItem.isRcvNew && item.deletedChatItem.chatInfo.ntfsEnabled { + let d = item.deletedChatItem + if item.toChatItem == nil && d.chatItem.isRcvNew && d.chatInfo.ntfsEnabled(chatItem: d.chatItem) { await MainActor.run { m.decreaseUnreadCounter(user: user) } @@ -2088,42 +2406,16 @@ func processReceivedMsg(_ res: ChatResponse) async { } else { m.removeChatItem(item.deletedChatItem.chatInfo, item.deletedChatItem.chatItem) } + if item.deletedChatItem.chatItem.isActiveReport { + m.decreaseGroupReportsCounter(item.deletedChatItem.chatInfo.id) + } + } + if let updatedChatInfo = items.last?.deletedChatItem.chatInfo { + m.updateChatInfo(updatedChatInfo) } } case let .groupChatItemsDeleted(user, groupInfo, chatItemIDs, _, member_): - 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 } - } - } + await groupChatItemsDeleted(user, groupInfo, chatItemIDs, member_) case let .receivedGroupInvitation(user, groupInfo, _, _): if active(user) { await MainActor.run { @@ -2143,7 +2435,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 { @@ -2169,21 +2461,36 @@ func processReceivedMsg(_ res: ChatResponse) async { _ = m.upsertGroupMember(groupInfo, member) } } - case let .deletedMemberUser(user, groupInfo, _): // TODO update user member + case let .memberAcceptedByOther(user, groupInfo, _, member): if active(user) { await MainActor.run { + _ = m.upsertGroupMember(groupInfo, member) m.updateGroup(groupInfo) } } - case let .deletedMember(user, groupInfo, _, deletedMember): + 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, byMember, deletedMember, withMessages): + if active(user) { + await MainActor.run { + m.updateGroup(groupInfo) _ = m.upsertGroupMember(groupInfo, deletedMember) + if withMessages { + m.removeMemberItems(deletedMember, byMember: byMember, groupInfo) + } } } case let .leftMember(user, groupInfo, member): if active(user) { await MainActor.run { + m.updateGroup(groupInfo) _ = m.upsertGroupMember(groupInfo, member) } } @@ -2198,6 +2505,17 @@ func processReceivedMsg(_ res: ChatResponse) async { await MainActor.run { m.updateGroup(groupInfo) } + if m.chatId == groupInfo.id { + if groupInfo.membership.memberPending { + await MainActor.run { + m.secondaryPendingInviteeChatOpened = true + } + } else if case .memberSupport(nil) = m.secondaryIM?.groupScopeInfo { + await MainActor.run { + m.secondaryPendingInviteeChatOpened = false + } + } + } } case let .joinedGroupMember(user, groupInfo, member): if active(user) { @@ -2211,11 +2529,6 @@ func processReceivedMsg(_ res: ChatResponse) async { _ = m.upsertGroupMember(groupInfo, member) } } - if let contact = memberContact { - await MainActor.run { - n.setContactNetworkStatus(contact, .connected) - } - } case let .groupUpdated(user, toGroup): if active(user) { await MainActor.run { @@ -2244,6 +2557,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 legacy 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): @@ -2437,13 +2754,10 @@ 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) } - n.networkStatuses = Dictionary(uniqueKeysWithValues: statuses) } catch let error { logger.debug("error updating chat data: \(responseError(error))") } @@ -2466,6 +2780,43 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async { } } +func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemIDs: Set, _ member_: GroupMember?) async { + let m = ChatModel.shared + if !active(user) { + do { + let users = try listUsers() + await MainActor.run { + m.users = users + } + } catch { + logger.error("Error loading users: \(error)") + } + return + } + let im = ItemsModel.shared + let cInfo = ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil) + 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 refreshCallInvitations() async throws { let m = ChatModel.shared let callInvitations = try await apiGetCallInvitations() @@ -2514,3 +2865,26 @@ private struct UserResponse: Decodable { var user: User? var error: String? } + +private func showClientNotice(_ server: String, _ preset: Bool, _ expiresAt: Date?) { + DispatchQueue.main.async { + var message = "Server: \(server).\nConditions of use violation notice received from \(preset ? "preset" : "this") server.\nNo IDs shared, see How it works." + if let expiresAt { + message += "\n\nNew addresses can be created after \(expiresAt.formatted(date: .abbreviated, time: .shortened))." + } + showAlert("Not allowed", message: message) { + let howItWorks = UIAlertAction(title: NSLocalizedString("How it works", comment: "alert button"), style: .default, handler: { _ in + UIApplication.shared.open(contentModerationPostLink) + }) + return preset + ? [ + okAlertAction, + UIAlertAction(title: NSLocalizedString("Conditions of use", comment: "alert button"), style: .default, handler: { _ in + UIApplication.shared.open(conditionsURL) + }), + howItWorks + ] + : [okAlertAction, howItWorks] + } + } +} diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 10120db185..e1a6bb61e8 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -42,7 +42,11 @@ struct SimpleXApp: App { .environmentObject(AppTheme.shared) .onOpenURL { url in logger.debug("ContentView.onOpenURL: \(url)") - chatModel.appOpenUrl = url + if AppChatState.shared.value == .active { + chatModel.appOpenUrl = url + } else { + chatModel.appOpenUrlLater = url + } } .onAppear() { // Present screen for continue migration if it wasn't finished yet @@ -93,7 +97,16 @@ struct SimpleXApp: App { if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { await updateCallInvitations() } + if let url = chatModel.appOpenUrlLater { + await MainActor.run { + chatModel.appOpenUrlLater = nil + chatModel.appOpenUrl = url + } + } } + } else if let url = chatModel.appOpenUrlLater { + chatModel.appOpenUrlLater = nil + chatModel.appOpenUrl = url } } } @@ -145,12 +158,12 @@ struct SimpleXApp: App { if let id = chatModel.chatId, let chat = chatModel.getChat(id), !NtfManager.shared.navigatingToChat { - Task { await loadChat(chat: chat, clearItems: false) } + Task { await loadChat(chat: chat, im: ItemsModel.shared, clearItems: false) } } if let ncr = chatModel.ntfContactRequest { await MainActor.run { chatModel.ntfContactRequest = nil } if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo { - Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) } + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } } } } catch let error { diff --git a/apps/ios/Shared/Theme/Theme.swift b/apps/ios/Shared/Theme/Theme.swift index de67390026..3bd8f00c25 100644 --- a/apps/ios/Shared/Theme/Theme.swift +++ b/apps/ios/Shared/Theme/Theme.swift @@ -42,12 +42,14 @@ class AppTheme: ObservableObject, Equatable { } func updateFromCurrentColors() { - objectWillChange.send() - name = CurrentColors.name - base = CurrentColors.base - colors.updateColorsFrom(CurrentColors.colors) - appColors.updateColorsFrom(CurrentColors.appColors) - wallpaper.updateWallpaperFrom(CurrentColors.wallpaper) + DispatchQueue.main.async { + self.objectWillChange.send() + self.name = CurrentColors.name + self.base = CurrentColors.base + self.colors.updateColorsFrom(CurrentColors.colors) + self.appColors.updateColorsFrom(CurrentColors.appColors) + self.wallpaper.updateWallpaperFrom(CurrentColors.wallpaper) + } } } diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index 2f76f1f046..ab7a47b944 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -243,7 +243,7 @@ struct ActiveCallView: View { ChatReceiver.shared.messagesChannel = nil return } - if case let .chatItemsStatusesUpdated(_, chatItems) = msg, + if case let .result(.chatItemsStatusesUpdated(_, chatItems)) = msg, chatItems.contains(where: { ci in ci.chatInfo.id == call.contact.id && ci.chatItem.content.isSndCall && @@ -361,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: ")") } } } @@ -390,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: ")") } } } @@ -467,7 +467,7 @@ struct ActiveCallOverlay: View { .disabled(call.initialCallType == .audio && client.activeCall?.peerHasOldVersion == true) } - @ViewBuilder private func flipCameraButton() -> some View { + private func flipCameraButton() -> some View { controlButton(call, "arrow.triangle.2.circlepath", padding: 12) { Task { if await WebRTCClient.isAuthorized(for: .video) { @@ -477,11 +477,11 @@ struct ActiveCallOverlay: View { } } - @ViewBuilder private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View { + 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) } - @ViewBuilder private func audioDevicePickerButton() -> some View { + private func audioDevicePickerButton() -> some View { AudioDevicePicker() .opacity(0.8) .scaleEffect(2) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 62a41c504a..37f3b982a1 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -12,6 +12,7 @@ import SimpleXChat struct ChatInfoToolbar: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var theme: AppTheme + @EnvironmentObject var m: ChatModel @ObservedObject var chat: Chat var imageSize: CGFloat = 32 @@ -22,11 +23,28 @@ struct ChatInfoToolbar: View { Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo) Spacer().frame(width: 16) } - ChatInfoImage( - chat: chat, - size: imageSize, - color: Color(uiColor: .tertiaryLabel) - ) + ZStack(alignment: .bottomTrailing) { + ChatInfoImage( + chat: chat, + size: imageSize, + color: Color(uiColor: .tertiaryLabel) + ) + if chat.chatStats.reportsCount > 0 { + Image(systemName: "flag.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + } else if chat.supportUnreadCount > 0 { + Image(systemName: "flag.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, theme.colors.primary) + } + } .padding(.trailing, 4) let t = Text(cInfo.displayName).font(.headline) (cInfo.contact?.verified == true ? contactVerifiedShield + t : t) @@ -39,6 +57,13 @@ struct ChatInfoToolbar: View { .padding(.top, -2) } } + if let contact = chat.chatInfo.contact, + contact.ready && contact.active, + let chatSubStatus = m.chatSubStatus, + chatSubStatus != .active { + SubStatusView(status: chatSubStatus) + .padding(.leading, 4) + } } .foregroundColor(theme.colors.onBackground) .frame(width: 220) @@ -51,6 +76,30 @@ struct ChatInfoToolbar: View { .baselineOffset(1) .kerning(-2) } + + struct SubStatusView: View { + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @EnvironmentObject var theme: AppTheme + var status: SubscriptionStatus + + var body: some View { + switch status { + case .active: EmptyView() + case .pending: ProgressView() + case .removed: subStatusError() + case .noSub: subStatusError() + } + } + + @ViewBuilder private func subStatusError() -> some View { + let dynamicChatInfoSize = dynamicSize(userFont).chatInfoSize + Image(systemName: "exclamationmark.circle") + .resizable() + .scaledToFit() + .frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize) + .foregroundColor(theme.colors.secondary) + } + } } struct ChatInfoToolbar_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 7a5003c94d..ad82af05e2 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 { @@ -92,7 +92,6 @@ 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 @State var localAlias: String @@ -111,10 +110,11 @@ struct ChatInfoView: View { @State private var sendReceiptsUserDefault = true @State private var progressIndicator = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false - + @State private var showSecrets: Set = [] + enum ChatInfoViewAlert: Identifiable { case clearChatAlert - case networkStatusAlert + case subStatusAlert(status: SubscriptionStatus) case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert @@ -125,7 +125,7 @@ struct ChatInfoView: View { var id: String { switch self { case .clearChatAlert: return "clearChatAlert" - case .networkStatusAlert: return "networkStatusAlert" + case let .subStatusAlert(status): return "subStatusAlert \(status)" case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" @@ -135,7 +135,7 @@ struct ChatInfoView: View { } } } - + var body: some View { NavigationView { ZStack { @@ -146,19 +146,21 @@ struct ChatInfoView: View { .onTapGesture { aliasTextFieldFocused = false } - + localAliasTextEdit() .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) } - muteButton(width: buttonWidth) + if let nextNtfMode = chat.chatInfo.nextNtfMode { + muteButton(width: buttonWidth, nextNtfMode: nextNtfMode) + } } } .padding(.trailing) @@ -167,7 +169,7 @@ struct ChatInfoView: View { .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 { @@ -178,7 +180,7 @@ struct ChatInfoView: View { } } } - + Section { if let code = connectionCode { verifyCodeButton(code) } contactPreferencesButton() @@ -201,19 +203,19 @@ struct ChatInfoView: View { // } } .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) @@ -230,13 +232,15 @@ struct ChatInfoView: View { .foregroundColor(theme.colors.secondary) } } - + if contact.ready && contact.active { Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { - networkStatusRow() - .onTapGesture { - alert = .networkStatusAlert - } + if let chatSubStatus = chatModel.chatSubStatus { + SubStatusRow(status: chatSubStatus) + .onTapGesture { + alert = .subStatusAlert(status: chatSubStatus) + } + } if let connStats = connectionStats { Button("Change receiving address") { alert = .switchAddressAlert @@ -259,12 +263,12 @@ struct ChatInfoView: View { } } } - + Section { clearChatButton() deleteContactButton() } - + if developerTools { Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { infoRow("Local name", chat.chatInfo.localDisplayName) @@ -272,8 +276,9 @@ struct ChatInfoView: View { Button ("Debug delivery") { Task { do { - let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId)) - await MainActor.run { alert = .queueInfo(info: info) } + if let info = try await apiContactQueueInfo(chat.chatInfo.apiId) { + await MainActor.run { alert = .queueInfo(info: queueInfoText(info)) } + } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") let a = getErrorAlert(e, "Error") @@ -288,7 +293,7 @@ struct ChatInfoView: View { .navigationBarHidden(true) .disabled(progressIndicator) .opacity(progressIndicator ? 0.6 : 1) - + if progressIndicator { ProgressView().scaleEffect(2) } @@ -300,7 +305,7 @@ 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) @@ -321,7 +326,7 @@ struct ChatInfoView: View { .alert(item: $alert) { alertItem in switch(alertItem) { case .clearChatAlert: return clearChatAlert() - case .networkStatusAlert: return networkStatusAlert() + case let .subStatusAlert(status): return subStatusAlert(status) case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) case .syncConnectionForceAlert: @@ -339,7 +344,7 @@ struct ChatInfoView: View { } } .actionSheet(item: $actionSheet) { $0.actionSheet } - .sheet(item: $sheet) { + .sheet(item: $sheet) { if #available(iOS 16.0, *) { $0.content .presentationDetents([.fraction($0.fraction)]) @@ -358,41 +363,52 @@ struct ChatInfoView: View { } } } - + private func contactInfoHeader() -> some View { VStack(spacing: 8) { let cInfo = chat.chatInfo ChatInfoImage(chat: chat, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.vertical, 12) + // show actual display name, alias can be edited in this view + let displayName = contact.profile.displayName.trimmingCharacters(in: .whitespacesAndNewlines) + let fullName = cInfo.fullName.trimmingCharacters(in: .whitespacesAndNewlines) if contact.verified { ( Text(Image(systemName: "checkmark.shield")) .foregroundColor(theme.colors.secondary) .font(.title2) + textSpace - + Text(contact.profile.displayName) + + Text(displayName) .font(.largeTitle) ) .multilineTextAlignment(.center) .lineLimit(2) .padding(.bottom, 2) } else { - Text(contact.profile.displayName) + Text(displayName) .font(.largeTitle) .multilineTextAlignment(.center) .lineLimit(2) .padding(.bottom, 2) } - if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName { + if fullName != "" && fullName != displayName && fullName != cInfo.displayName.trimmingCharacters(in: .whitespacesAndNewlines) { Text(cInfo.fullName) .font(.title2) + .multilineTextAlignment(.center) + .lineLimit(3) + .padding(.bottom, 2) + } + if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { + let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) .multilineTextAlignment(.center) .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) } } .frame(maxWidth: .infinity, alignment: .center) } - + private func localAliasTextEdit() -> some View { TextField("Set contact name…", text: $localAlias) .disableAutocorrection(true) @@ -409,7 +425,7 @@ struct ChatInfoView: View { .multilineTextAlignment(.center) .foregroundColor(theme.colors.secondary) } - + private func setContactAlias() { Task { do { @@ -432,13 +448,13 @@ struct ChatInfoView: View { .disabled(!contact.ready || chat.chatItems.isEmpty) } - private func muteButton(width: CGFloat) -> some View { - InfoViewButton( - image: chat.chatInfo.ntfsEnabled ? "speaker.slash.fill" : "speaker.wave.2.fill", - title: chat.chatInfo.ntfsEnabled ? "mute" : "unmute", + private func muteButton(width: CGFloat, nextNtfMode: MsgFilter) -> some View { + return InfoViewButton( + image: nextNtfMode.iconFilled, + title: "\(nextNtfMode.text(mentions: false))", width: width ) { - toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) + toggleNotifications(chat, enableNtfs: nextNtfMode) } .disabled(!contact.ready || !contact.active) } @@ -472,7 +488,7 @@ struct ChatInfoView: View { ) } } - + private func contactPreferencesButton() -> some View { NavigationLink { ContactPreferencesView( @@ -488,21 +504,20 @@ struct ChatInfoView: View { Label("Contact preferences", systemImage: "switch.2") } } - + private func sendReceiptsOption() -> some View { - Picker(selection: $sendReceipts) { + WrappedPicker(selection: $sendReceipts) { ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in Text(opt.text) } } label: { Label("Send receipts", systemImage: "checkmark.message") } - .frame(height: 36) .onChange(of: sendReceipts) { _ in setSendReceipts() } } - + private func setSendReceipts() { var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults chatSettings.sendRcpts = sendReceipts.bool() @@ -522,7 +537,7 @@ struct ChatInfoView: View { .foregroundColor(.orange) } } - + private func synchronizeConnectionButtonForce() -> some View { Button { alert = .syncConnectionForceAlert @@ -531,27 +546,7 @@ struct ChatInfoView: View { .foregroundColor(.red) } } - - private func networkStatusRow() -> some View { - HStack { - Text("Network status") - Image(systemName: "info.circle") - .foregroundColor(theme.colors.primary) - .font(.system(size: 14)) - Spacer() - Text(networkModel.contactNetworkStatus(contact).statusString) - .foregroundColor(theme.colors.secondary) - serverImage() - } - } - - private func serverImage() -> some View { - let status = networkModel.contactNetworkStatus(contact) - return Image(systemName: status.imageName) - .foregroundColor(status == .connected ? .green : theme.colors.secondary) - .font(.system(size: 12)) - } - + private func deleteContactButton() -> some View { Button(role: .destructive) { deleteContactDialog( @@ -567,7 +562,7 @@ struct ChatInfoView: View { .foregroundColor(Color.red) } } - + private func clearChatButton() -> some View { Button() { alert = .clearChatAlert @@ -576,7 +571,7 @@ struct ChatInfoView: View { .foregroundColor(Color.orange) } } - + private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), @@ -590,14 +585,14 @@ struct ChatInfoView: View { secondaryButton: .cancel() ) } - - private func networkStatusAlert() -> Alert { + + private func subStatusAlert(_ status: SubscriptionStatus) -> Alert { Alert( title: Text("Network status"), - message: Text(networkModel.contactNetworkStatus(contact).statusExplanation) + message: Text(status.statusExplanation) ) } - + private func switchContactAddress() { Task { do { @@ -616,7 +611,7 @@ struct ChatInfoView: View { } } } - + private func abortSwitchContactAddress() { Task { do { @@ -634,7 +629,7 @@ struct ChatInfoView: View { } } } - + private func savePreferences() { Task { do { @@ -653,6 +648,30 @@ struct ChatInfoView: View { } } +struct SubStatusRow: View { + @EnvironmentObject var theme: AppTheme + var status: SubscriptionStatus + + var body: some View { + HStack { + Text("Network status") + Image(systemName: "info.circle") + .foregroundColor(theme.colors.primary) + .font(.system(size: 14)) + Spacer() + Text(status.statusString) + .foregroundColor(theme.colors.secondary) + serverImage(status) + } + } + + private func serverImage(_ status: SubscriptionStatus) -> some View { + return Image(systemName: status.imageName) + .foregroundColor(status == .active ? .green : theme.colors.secondary) + .font(.system(size: 12)) + } +} + struct ChatTTLOption: View { @ObservedObject var chat: Chat @Binding var progressIndicator: Bool @@ -660,19 +679,18 @@ struct ChatTTLOption: View { @State private var chatItemTTL: ChatTTL = ChatTTL.chat(.seconds(0)) var body: some View { - Picker("Delete messages after", selection: $chatItemTTL) { + WrappedPicker("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( @@ -682,17 +700,23 @@ struct ChatTTLOption: View { ) { 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, replaceChat: true) + await loadChat(chat: chat, im: ItemsModel.shared, 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, replaceChat: true) + await loadChat(chat: chat, im: ItemsModel.shared, clearItems: true) await MainActor.run { chatItemTTL = currentChatItemTTL progressIndicator = false @@ -825,7 +849,7 @@ private struct CallButton: View { )) } } - } else if contact.nextSendGrpInv { + } else if contact.sendMsgToConnect { showAlert(SomeAlert( alert: mkAlert( title: "Can't call contact", @@ -930,7 +954,7 @@ struct ChatWallpaperEditorSheet: View { 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 { + } else if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { uiThemes } else { ThemeModeOverrides() @@ -966,7 +990,7 @@ struct ChatWallpaperEditorSheet: View { 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 { + } else if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { uiThemes } else { ThemeModeOverrides() @@ -1044,12 +1068,12 @@ struct ChatWallpaperEditorSheet: View { chat.wrappedValue = Chat.init(chatInfo: ChatInfo.direct(contact: contact)) themes = themesFromChat(chat.wrappedValue) } - } else if case var ChatInfo.group(groupInfo) = chat.wrappedValue.chatInfo { + } 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)) + ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil)) + chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil)) themes = themesFromChat(chat.wrappedValue) } } @@ -1129,13 +1153,13 @@ func setChatTTL(_ ttl: ChatTTL, hasPreviousTTL: Bool, onCancel: @escaping () -> } 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( diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift index 3b3e1b3899..0283e9c07e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift @@ -50,7 +50,7 @@ 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(theme.colors.secondary) @@ -60,16 +60,16 @@ struct CICallItemView: View { @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(theme.colors.secondary) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift index 02be8af73b..b2b4441646 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -12,8 +12,8 @@ import SimpleXChat struct CIChatFeatureView: View { @EnvironmentObject var m: ChatModel @Environment(\.revealed) var revealed: Bool - @ObservedObject var im = ItemsModel.shared @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var feature: Feature @@ -53,7 +53,7 @@ struct CIChatFeatureView: View { private func mergedFeatures() -> [FeatureInfo]? { var fs: [FeatureInfo] = [] var icons: Set = [] - if var i = m.getChatItemIndex(chatItem) { + if var i = m.getChatItemIndex(im, chatItem) { while i < im.reversedChatItems.count, let f = featureInfo(im.reversedChatItems[i]) { if !icons.contains(f.icon) { @@ -108,6 +108,7 @@ struct CIChatFeatureView_Previews: PreviewProvider { let enabled = FeatureEnabled(forUser: false, forContact: false) CIChatFeatureView( chat: Chat.sampleData, + im: ItemsModel.shared, 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 2c9c261536..67f7b69e2c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift @@ -26,9 +26,9 @@ struct CIFeaturePreferenceView: View { 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() } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index a785f3e6d8..1b9376b5db 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -19,42 +19,42 @@ struct CIFileView: View { var body: some View { if smallViewSize != nil { fileIndicator() - .onTapGesture(perform: fileAction) + .simultaneousGesture(TapGesture().onEnded(fileAction)) } else { let metaReserve = edited ? " " : " " - Button(action: fileAction) { - HStack(alignment: .bottom, spacing: 6) { - fileIndicator() - .padding(.top, 5) - .padding(.bottom, 3) - if let file = file { - let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) - VStack(alignment: .leading, spacing: 2) { - Text(file.fileName) - .lineLimit(1) - .multilineTextAlignment(.leading) - .foregroundColor(theme.colors.onBackground) - Text(prettyFileSize + metaReserve) - .font(.caption) - .lineLimit(1) - .multilineTextAlignment(.leading) - .foregroundColor(theme.colors.secondary) - } - } else { - Text(metaReserve) + HStack(alignment: .bottom, spacing: 6) { + fileIndicator() + .padding(.top, 5) + .padding(.bottom, 3) + if let file = file { + let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) + VStack(alignment: .leading, spacing: 2) { + Text(file.fileName) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.onBackground) + Text(prettyFileSize + metaReserve) + .font(.caption) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.secondary) } + } else { + Text(metaReserve) } - .padding(.top, 4) - .padding(.bottom, 6) - .padding(.leading, 10) - .padding(.trailing, 12) } + .padding(.top, 4) + .padding(.bottom, 6) + .padding(.leading, 10) + .padding(.trailing, 12) + .simultaneousGesture(TapGesture().onEnded(fileAction)) .disabled(!itemInteractive) } } + @inline(__always) private var itemInteractive: Bool { if let file = file { switch (file.fileStatus) { @@ -278,6 +278,7 @@ func showFileErrorAlert(_ err: FileError, temporary: Bool = false) { struct CIFileView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared let sentFile: ChatItem = ChatItem( chatDir: .directSnd, meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), @@ -293,16 +294,16 @@ struct CIFileView_Previews: PreviewProvider { file: nil ) Group { - ChatItemView(chat: Chat.sampleData, chatItem: sentFile) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample()) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10))) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation)) - 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)) - ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, 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), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: fileChatItemWtFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) } .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 107208a033..3fcf578875 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -84,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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index d491563913..d1f49f635a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct CIImageView: View { @EnvironmentObject var m: ChatModel let chatItem: ChatItem + var scrollToItem: ((ChatItem.ID) -> Void)? = nil var preview: UIImage? let maxWidth: CGFloat var imgWidth: CGFloat? @@ -25,12 +26,14 @@ struct CIImageView: View { if let uiImage = getLoadedImage(file) { Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } } .fullScreenCover(isPresented: $showFullScreenImage) { - FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage) + FullScreenMediaView(chatItem: chatItem, scrollToItem: scrollToItem, image: uiImage, showView: $showFullScreenImage) } .if(!smallView) { view in view.modifier(PrivacyBlur(blurred: $blurred)) } - .onTapGesture { showFullScreenImage = true } + .if(!blurred) { v in + v.simultaneousGesture(TapGesture().onEnded { showFullScreenImage = true }) + } .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenImage = false } @@ -42,7 +45,7 @@ struct CIImageView: View { imageView(preview).modifier(PrivacyBlur(blurred: $blurred)) } } - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { if let file = file { switch file.fileStatus { case .rcvInvitation, .rcvAborted: @@ -79,7 +82,7 @@ struct CIImageView: View { default: () } } - } + }) } } .onDisappear { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift index 18fd682646..5e9fa691de 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift @@ -7,10 +7,11 @@ // import SwiftUI +import SimpleXChat struct CIInvalidJSONView: View { @EnvironmentObject var theme: AppTheme - var json: String + var json: Data? @State private var showJSON = false var body: some View { @@ -23,16 +24,16 @@ struct CIInvalidJSONView: View { .padding(.vertical, 6) .background(Color(uiColor: .tertiarySystemGroupedBackground)) .textSelection(.disabled) - .onTapGesture { showJSON = true } + .simultaneousGesture(TapGesture().onEnded { showJSON = true }) .appSheet(isPresented: $showJSON) { - invalidJSONView(json) + 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") @@ -49,6 +50,6 @@ func invalidJSONView(_ json: String) -> some View { 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 692e6bb8a6..f07e90b953 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -21,30 +21,94 @@ struct CILinkView: View { .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) + Text(linkPreview.uri) .font(.caption) .lineLimit(1) .foregroundColor(theme.colors.secondary) } .padding(.horizontal, 12) .frame(maxWidth: .infinity, alignment: .leading) + .simultaneousGesture(TapGesture().onEnded { + openBrowserAlert(uri: linkPreview.uri) + }) } } } +func openBrowserAlert(uri: String) { + let (url, err) = sanitizeUri(uri) + if let url { + let uriStr = url.uri.absoluteString + showAlert( + NSLocalizedString("Open link?", comment: "alert title"), + message: uriStr.count > 160 ? "\(uriStr.prefix(160))…" : uriStr, + actions: { + if let sanitizedUri = url.sanitizedUri { + [ + cancelAlertAction, + UIAlertAction( + title: NSLocalizedString("Open full link", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(url.uri) } + ), + UIAlertAction( + title: NSLocalizedString("Open clean link", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(sanitizedUri) } + ) + ] + } else { + [ + cancelAlertAction, + UIAlertAction( + title: NSLocalizedString("Open", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(url.uri) } + ) + ] + } + } + ) + } else { + showInvalidLinkAlert(uri, error: err) + } +} + +func showInvalidLinkAlert(_ uri: String, error: String? = nil) { + let message = if let error, !error.isEmpty { + error + "\n" + uri + } else { + uri + } + showAlert( + NSLocalizedString("Invalid link", comment: "alert title"), + message: message, + actions: {[okAlertAction]} + ) +} + +func sanitizeUri(_ s: String) -> (url: (uri: URL, sanitizedUri: URL?)?, error: String?) { + let parsed = parseSanitizeUri(s, safe: false) + return if let uri = URL(string: s), let uriInfo = parsed?.uriInfo { + (url: (uri: uri, sanitizedUri: uriInfo.sanitized.flatMap { URL(string: $0) }), error: nil) + } else { + (url: nil, error: parsed?.parseError) + } +} + struct LargeLinkPreview_Previews: PreviewProvider { static var previews: some View { let preview = LinkPreview( - uri: URL(string: "http://DuckDuckGo.com")!, + uri: "http://DuckDuckGo.com", title: "Privacy, simplified.", description: "", image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z" diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift index d24c737907..2898a318a9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift @@ -20,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 { - ItemsModel.shared.loadOpenChat("@\(contactId)") + .simultaneousGesture(TapGesture().onEnded { + ItemsModel.shared.loadOpenChat("@\(contactId)") { + dismissAllSheets(animated: true) } - } + }) } else { memberCreatedContactView() } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index e58ad0f74e..fc73778239 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -15,7 +15,7 @@ struct CIMetaView: View { @Environment(\.showTimestamp) var showTimestamp: Bool var chatItem: ChatItem var metaColor: Color - var paleMetaColor = Color(UIColor.tertiaryLabel) + var paleMetaColor = Color(uiColor: .tertiaryLabel) var showStatus = true var showEdited = true var invertedMaterial = false @@ -152,11 +152,13 @@ func ciMetaText( return r.font(.caption) } +@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) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 4603a026cd..3201332c1e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -45,7 +45,7 @@ struct CIRcvDecryptionError: View { viewBody() .onAppear { // for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear - if case let .group(groupInfo) = chat.chatInfo, + if case let .group(groupInfo, _) = chat.chatInfo, case let .groupRcv(groupMember) = chatItem.chatDir { do { let (member, stats) = try apiGroupMemberInfoSync(groupInfo.apiId, groupMember.groupMemberId) @@ -68,7 +68,7 @@ struct CIRcvDecryptionError: View { } } - @ViewBuilder private func viewBody() -> some View { + private func viewBody() -> some View { Group { if case let .direct(contact) = chat.chatInfo, let contactStats = contact.activeConn?.connectionStats { @@ -83,7 +83,7 @@ struct CIRcvDecryptionError: View { } else { basicDecryptionErrorItem() } - } else if case let .group(groupInfo) = chat.chatInfo, + } 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 { @@ -133,7 +133,7 @@ struct CIRcvDecryptionError: View { CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } - .onTapGesture(perform: { onClick() }) + .simultaneousGesture(TapGesture().onEnded(onClick)) .padding(.vertical, 6) .textSelection(.disabled) } @@ -151,7 +151,7 @@ struct CIRcvDecryptionError: View { CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } - .onTapGesture(perform: { onClick() }) + .simultaneousGesture(TapGesture().onEnded(onClick)) .padding(.vertical, 6) .textSelection(.disabled) } @@ -161,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 f774299ad3..eacbe9360a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -47,57 +47,57 @@ struct CIVideoView: View { let file = chatItem.file ZStack(alignment: smallView ? .topLeading : .center) { ZStack(alignment: .topLeading) { - if let file = file, let preview = preview, let decrypted = urlDecrypted, smallView { - smallVideoView(decrypted, file, preview) - } else 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, smallView { - smallVideoViewEncrypted(file, defaultPreview) - } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil { - videoViewEncrypted(file, defaultPreview, duration) - } else if let preview, let file { - Group { if smallView { smallViewImageView(preview, file) } else { imageView(preview) } } - .onTapGesture { - 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: () - } + 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: () + } + }) + } } if !smallView { durationProgress() } } if !blurred, let file, showDownloadButton(file.fileStatus) { - if !smallView { - Button { - receiveFileIfValidSize(file: file, receiveFile: receiveFile) - } label: { - playPauseIcon("play.fill") - } - } else if !file.showStatusIconInSmallView { + if !smallView || !file.showStatusIconInSmallView { playPauseIcon("play.fill") - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { receiveFileIfValidSize(file: file, receiveFile: receiveFile) - } + }) } } } @@ -151,27 +151,26 @@ struct CIVideoView: View { ZStack(alignment: .center) { let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) imageView(defaultPreview) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { decrypt(file: file) { showFullScreenPlayer = urlDecrypted != nil } - } + }) .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenPlayer = false } if !blurred { if !decryptionInProgress { - Button { - decrypt(file: file) { - if urlDecrypted != nil { - videoPlaying = true - player?.play() + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + .simultaneousGesture(TapGesture().onEnded { + decrypt(file: file) { + if urlDecrypted != nil { + videoPlaying = true + player?.play() + } } - } - } label: { - playPauseIcon(canBePlayed ? "play.fill" : "play.slash") - } - .disabled(!canBePlayed) + }) + .disabled(!canBePlayed) } else { videoDecryptionProgress() } @@ -194,29 +193,30 @@ struct CIVideoView: View { } } .modifier(PrivacyBlur(enabled: !videoPlaying, blurred: $blurred)) - .onTapGesture { - switch player.timeControlStatus { - case .playing: - player.pause() - videoPlaying = false - case .paused: - if canBePlayed { - showFullScreenPlayer = true + .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 && !blurred { - Button { - m.stopPreviousRecPlay = url - player.play() - } label: { - playPauseIcon(canBePlayed ? "play.fill" : "play.slash") - } - .disabled(!canBePlayed) + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + .simultaneousGesture(TapGesture().onEnded { + m.stopPreviousRecPlay = url + player.play() + }) + .disabled(!canBePlayed) } } fileStatusIcon() @@ -235,7 +235,7 @@ struct CIVideoView: View { return ZStack(alignment: .topLeading) { let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) smallViewImageView(preview, file) - .onTapGesture { + .onTapGesture { // this is shown in chat list, where onTapGesture works decrypt(file: file) { showFullScreenPlayer = urlDecrypted != nil } @@ -256,7 +256,7 @@ struct CIVideoView: View { private func smallVideoView(_ url: URL, _ file: CIFile, _ preview: UIImage) -> some View { return ZStack(alignment: .topLeading) { smallViewImageView(preview, file) - .onTapGesture { + .onTapGesture { // this is shown in chat list, where onTapGesture works showFullScreenPlayer = true } .onChange(of: m.activeCallViewIsCollapsed) { _ in @@ -354,14 +354,14 @@ struct CIVideoView: View { case .sndCancelled: fileIcon("xmark", 10, 13) case let .sndError(sndFileError): fileIcon("xmark", 10, 13) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { showFileErrorAlert(sndFileError) - } + }) case let .sndWarning(sndFileError): fileIcon("exclamationmark.triangle.fill", 10, 13) - .onTapGesture { + .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): @@ -375,14 +375,14 @@ struct CIVideoView: View { case .rcvCancelled: fileIcon("xmark", 10, 13) case let .rcvError(rcvFileError): fileIcon("xmark", 10, 13) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { showFileErrorAlert(rcvFileError) - } + }) case let .rcvWarning(rcvFileError): fileIcon("exclamationmark.triangle.fill", 10, 13) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { showFileErrorAlert(rcvFileError, temporary: true) - } + }) case .invalid: fileIcon("questionmark", 10, 13) } } @@ -429,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() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index ff4378c715..47aee2a586 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -168,14 +168,14 @@ struct VoiceMessagePlayer: View { case .sndCancelled: playbackButton() case let .sndError(sndFileError): fileStatusIcon("multiply", 14) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { showFileErrorAlert(sndFileError) - } + }) case let .sndWarning(sndFileError): fileStatusIcon("exclamationmark.triangle.fill", 16) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { showFileErrorAlert(sndFileError, temporary: true) - } + }) case .rcvInvitation: downloadButton(recordingFile, "play.fill") case .rcvAccepted: loadingIcon() case .rcvTransfer: loadingIcon() @@ -184,14 +184,14 @@ struct VoiceMessagePlayer: View { case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) case let .rcvError(rcvFileError): fileStatusIcon("multiply", 14) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { showFileErrorAlert(rcvFileError) - } + }) case let .rcvWarning(rcvFileError): fileStatusIcon("exclamationmark.triangle.fill", 16) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { showFileErrorAlert(rcvFileError, temporary: true) - } + }) case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) } } else { @@ -255,59 +255,29 @@ struct VoiceMessagePlayer: View { } } - @ViewBuilder private func playbackButton() -> some View { - if sizeMultiplier != 1 { - switch playbackState { - case .noPlayback: - playPauseIcon("play.fill", theme.colors.primary) - .onTapGesture { - if let recordingSource = getLoadedFileSource(recordingFile) { - startPlayback(recordingSource) - } - } - case .playing: - playPauseIcon("pause.fill", theme.colors.primary) - .onTapGesture { - audioPlayer?.pause() - playbackState = .paused - notifyStateChange() - } - case .paused: - playPauseIcon("play.fill", theme.colors.primary) - .onTapGesture { - audioPlayer?.play() - playbackState = .playing - notifyStateChange() - } - } - } else { - switch playbackState { - case .noPlayback: - Button { + 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) } - } label: { - playPauseIcon("play.fill", theme.colors.primary) - } - case .playing: - Button { + case .playing: audioPlayer?.pause() playbackState = .paused notifyStateChange() - } label: { - playPauseIcon("pause.fill", theme.colors.primary) - } - case .paused: - Button { + case .paused: audioPlayer?.play() playbackState = .playing notifyStateChange() - } label: { - playPauseIcon("play.fill", theme.colors.primary) } - } - } + }) } private func playPauseIcon(_ image: String, _ color: Color/* = .accentColor*/) -> some View { @@ -329,28 +299,14 @@ struct VoiceMessagePlayer: View { } private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View { - Group { - if sizeMultiplier != 1 { - playPauseIcon(icon, theme.colors.primary) - .onTapGesture { - Task { - if let user = chatModel.currentUser { - await receiveFile(user: user, fileId: recordingFile.fileId) - } - } + playPauseIcon(icon, theme.colors.primary) + .simultaneousGesture(TapGesture().onEnded { + Task { + if let user = chatModel.currentUser { + await receiveFile(user: user, fileId: recordingFile.fileId) } - } else { - Button { - Task { - if let user = chatModel.currentUser { - await receiveFile(user: user, fileId: recordingFile.fileId) - } - } - } label: { - playPauseIcon(icon, theme.colors.primary) } - } - } + }) } func notifyStateChange() { @@ -430,6 +386,7 @@ struct VoiceMessagePlayer: View { } } +@inline(__always) func voiceMessageSizeBasedOnSquareSize(_ squareSize: CGFloat) -> CGFloat { let squareToCircleRatio = 0.935 return squareSize + squareSize * (1 - squareToCircleRatio) @@ -446,10 +403,12 @@ class VoiceItemState { 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)" } @@ -476,6 +435,7 @@ class VoiceItemState { struct CIVoiceView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared let sentVoiceMessage: ChatItem = ChatItem( chatDir: .directSnd, meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), @@ -498,10 +458,10 @@ struct CIVoiceView_Previews: PreviewProvider { duration: 30, allowMenu: Binding.constant(true) ) - ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, allowMenu: .constant(true)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), allowMenu: .constant(true)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), allowMenu: .constant(true)) - ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: voiceMessageWtFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true)) } .previewLayout(.fixed(width: 360, height: 360)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index 47a2cbb6cb..0b6f249b9c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -77,6 +77,7 @@ struct FramedCIVoiceView: View { struct FramedCIVoiceView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared let sentVoiceMessage: ChatItem = ChatItem( chatDir: .directSnd, meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), @@ -92,11 +93,11 @@ struct FramedCIVoiceView_Previews: PreviewProvider { file: CIFile.getSample(fileStatus: .sndComplete) ) Group { - ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there")) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10))) - 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.")) - ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, 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."), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: voiceMessageWithQuote, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) } .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 6da893d1d2..c9c9952688 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -12,9 +12,11 @@ import SimpleXChat struct FramedItemView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme - @EnvironmentObject var scrollModel: ReverseListScrollModel @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel var chatItem: ChatItem + var scrollToItem: (ChatItem.ID) -> Void + @Binding var scrollToItemId: ChatItem.ID? var preview: UIImage? var maxWidth: CGFloat = .infinity @State var msgWidth: CGFloat = 0 @@ -23,8 +25,6 @@ struct FramedItemView: View { @State private var useWhiteMetaColor: Bool = false @State var showFullScreenImage = false @Binding var allowMenu: Bool - @State private var showSecrets = false - @State private var showQuoteSecrets = false @State private var showFullscreenGallery: Bool = false var body: some View { @@ -57,18 +57,26 @@ struct FramedItemView: View { if let qi = chatItem.quotedItem { ciQuoteView(qi) - .onTapGesture { - if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) { + .simultaneousGesture(TapGesture().onEnded { + if let ci = im.reversedChatItems.first(where: { $0.id == qi.itemId }) { withAnimation { - scrollModel.scrollToItem(id: ci.id) + scrollToItem(ci.id) } + } else if let id = qi.itemId { + if (chatItem.isReport && im.secondaryIMFilter != nil) { + scrollToItemId = id + } else { + scrollToItem(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, msgContentView: framedMsgContentView) + ChatItemContentView(chat: chat, im: im, chatItem: chatItem, msgContentView: framedMsgContentView) .padding(chatItem.content.msgContent != nil ? 0 : 4) .overlay(DetermineWidth()) } @@ -85,19 +93,19 @@ struct FramedItemView: View { .overlay(DetermineWidth()) .accessibilityLabel("") } - } + } .background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) } .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } if let (title, text) = chatItem.meta.itemStatus.statusInfo { - v.onTapGesture { + v.simultaneousGesture(TapGesture().onEnded { AlertManager.shared.showAlert( Alert( title: Text(title), message: Text(text) ) ) - } + }) } else { v } @@ -117,7 +125,7 @@ struct FramedItemView: View { } else { switch (chatItem.content.msgContent) { case let .image(text, _): - CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery) + CIImageView(chatItem: chatItem, scrollToItem: scrollToItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear @@ -155,7 +163,7 @@ struct FramedItemView: View { case let .file(text): ciFileView(chatItem, text) case let .report(text, reason): - ciMsgContentView(chatItem, Text(text.isEmpty ? reason.text : "\(reason.text): ").italic().foregroundColor(.red)) + ciMsgContentView(chatItem, txtPrefix: reason.attrString) case let .link(_, preview): CILinkView(linkPreview: preview) ciMsgContentView(chatItem) @@ -199,6 +207,7 @@ 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): @@ -240,7 +249,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, theme)) + .background(backgroundColor) + .environment(\.containerBackground, UIColor(backgroundColor)) if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth { v.frame(maxWidth: mediaWidth, alignment: .leading) } else { @@ -254,7 +264,7 @@ struct FramedItemView: View { VStack(alignment: .leading, spacing: 2) { Text(sender) .font(.caption) - .foregroundColor(theme.colors.secondary) + .foregroundColor(qi.chatDir == .groupSnd ? .accentColor : theme.colors.secondary) .lineLimit(1) ciQuotedMsgTextView(qi, lines: 2) } @@ -266,14 +276,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 { @@ -288,24 +296,27 @@ struct FramedItemView: View { private func membership() -> GroupMember? { switch chat.chatInfo { - case let .group(groupInfo: groupInfo): return groupInfo.membership + case let .group(groupInfo: groupInfo, _): return groupInfo.membership default: return nil } } - @ViewBuilder private func ciMsgContentView(_ ci: ChatItem, _ txtPrefix: Text? = nil) -> 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) @@ -336,13 +347,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." + ) } } @@ -382,15 +392,16 @@ func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color { struct FramedItemView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared Group{ - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), 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)), 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)), 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)), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), 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 "), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } @@ -398,17 +409,18 @@ struct FramedItemView_Previews: PreviewProvider { struct FramedItemView_Edited_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), 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), 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), 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), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), 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), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), 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), 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), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, 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), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, 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), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) } .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 200)) @@ -417,17 +429,18 @@ struct FramedItemView_Edited_Previews: PreviewProvider { struct FramedItemView_Deleted_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), 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)), 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)), 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)), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), 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)), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), 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)), 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)), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, 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)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, 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)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), 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 044ee2a26d..f243a83142 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift @@ -13,8 +13,8 @@ import AVKit struct FullScreenMediaView: View { @EnvironmentObject var m: ChatModel - @EnvironmentObject var scrollModel: ReverseListScrollModel @State var chatItem: ChatItem + var scrollToItem: ((ChatItem.ID) -> Void)? @State var image: UIImage? @State var player: AVPlayer? = nil @State var url: URL? = nil @@ -71,7 +71,7 @@ struct FullScreenMediaView: View { let w = abs(t.width) if t.height > 60 && t.height > w * 2 { showView = false - scrollModel.scrollToItem(id: chatItem.id) + scrollToItem?(chatItem.id) } else if w > 60 && w > abs(t.height) * 2 && !scrolling { let previous = t.width > 0 scrolling = true @@ -126,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 afeb88b05d..47a30f6cf3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -31,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() @@ -47,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.") )) } @@ -71,7 +71,7 @@ struct CIMsgError: View { .padding(.vertical, 6) .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 87a9b2ce61..c6a5d0353c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -14,6 +14,7 @@ struct MarkedDeletedItemView: View { @EnvironmentObject var theme: AppTheme @Environment(\.revealed) var revealed: Bool @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel var chatItem: ChatItem var body: some View { @@ -29,14 +30,14 @@ struct MarkedDeletedItemView: View { var mergedMarkedDeletedText: LocalizedStringKey { if !revealed, let ciCategory = chatItem.mergeCategory, - var i = m.getChatItemIndex(chatItem) { + var i = m.getChatItemIndex(im, chatItem) { var moderated = 0 var blocked = 0 var blockedByAdmin = 0 var deleted = 0 var moderatedBy: Set = [] - while i < ItemsModel.shared.reversedChatItems.count, - let ci = .some(ItemsModel.shared.reversedChatItems[i]), + while i < im.reversedChatItems.count, + let ci = .some(im.reversedChatItems[i]), ci.mergeCategory == ciCategory, let itemDeleted = ci.meta.itemDeleted { switch itemDeleted { @@ -85,6 +86,7 @@ struct MarkedDeletedItemView_Previews: PreviewProvider { Group { MarkedDeletedItemView( chat: Chat.sampleData, + im: ItemsModel.shared, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)) ).environment(\.revealed, true) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index e9b6d0ba84..2a1b526893 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -11,51 +11,74 @@ import SimpleXChat let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) -private let noTyping = Text(verbatim: " ") - -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: Text? = nil + 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() @@ -65,104 +88,374 @@ struct MsgContentView: View { private func stopTyping() { timer?.invalidate() timer = nil + typingIdx = 0 } - private func msgContentView() -> Text { - var v = messageText(text, formattedText, sender, showSecrets: showSecrets, secondaryColor: theme.colors.secondary, prefix: prefix) + @inline(__always) + private func msgContentView() -> some View { + let r = messageText( + text, + formattedText, + textStyle: textStyle, + sender: sender, + mentions: mentions, + userMemberId: userMemberId, + showSecrets: showSecrets, + commands: chat.chatInfo.useCommands && chat.chatInfo.sndReady, + backgroundColor: containerBackground, + prefix: prefix + ) + let s = r.string + let t: Text if let mt = meta { 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, sendCommand: { cmd in sendCommandMsg(chat, cmd) }) } - private func typingIndicator(_ recent: Bool) -> Text { - return (recent ? typingIndicators[typingIdx] : noTyping) - .font(.body.monospaced()) - .kerning(-2) - .foregroundColor(theme.colors.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(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) + (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, secondaryColor: Color, prefix: Text? = nil) -> 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(secondaryColor) + textSpace + res - } - - if let p = prefix { - res = p + 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, + sendCommand: ((String) -> Void)? = nil, + centered: Bool = false, + smallFont: Bool = false +) -> some View { + t.if(r.hasSecrets, transform: hiddenSecretsView) + .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, sendCommand: sendCommand, centered: centered, smallFont: smallFont)) } } -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: "") +// smallFont parameter is used to pad height, otherwise CTFrameGetLines fails to see them as lines - it's needed if font is not .body +@inline(__always) +private func handleTextTaps( + _ s: NSAttributedString, + showSecrets: Binding>? = nil, + sendCommand: ((String) -> Void)? = nil, + centered: Bool, + smallFont: Bool +) -> 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 paddedSize = smallFont ? CGSize(width: g.size.width, height: g.size.height + 1.0) : g.size + let path = CGPath(rect: CGRect(origin: .zero, size: paddedSize), 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) + var maxWidth: CGFloat = 0 + if centered { + for line in lines { + let bounds = CTLineGetBoundsWithOptions(line, .useOpticalBounds) + if bounds.width > maxWidth { + maxWidth = bounds.width + } + } + } + for i in 0 ..< lines.count { + let bounds = CTLineGetBoundsWithOptions(lines[i], .useOpticalBounds) + let offsetX = centered ? (maxWidth - bounds.width) / 2 : 0 + if bounds.offsetBy(dx: origins[i].x + offsetX, dy: origins[i].y).contains(point) { + let relativePoint = centered ? CGPoint(x: point.x - origins[i].x - offsetX, y: point.y - origins[i].y) : point + index = CTLineGetStringIndexForPosition(lines[i], relativePoint) + break + } + } + } + if let index, let (uri, browser) = attributedStringLink(s, for: index) { + if browser { + openBrowserAlert(uri: uri) + } else if let url = URL(string: uri) { + UIApplication.shared.open(url) + } else { + showInvalidLinkAlert(uri) + } + } + }) + } + + func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool)? { + var linkURL: String? + var browser: Bool = false + s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in + if index >= range.location && index < range.location + range.length { + if let url = attrs[linkAttrKey] as? String { + linkURL = url + 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) + } + } else if let sendCommand, let cmd = attrs[commandAttrKey] as? String { + sendCommand(cmd) + } + 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") + +private let commandAttrKey = NSAttributedString.Key("chat.simplex.app.command") + +typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool) + +@inline(__always) +func markdownText( + _ s: String, + textStyle: UIFont.TextStyle = .body, + sender: String? = nil, + preview: Bool = false, + mentions: [String: CIMention]? = nil, + userMemberId: String? = nil, + showSecrets: Set? = nil, + backgroundColor: Color +) -> MsgTextResult { + messageText( + s, + parseSimpleXMarkdown(s), + textStyle: textStyle, + sender: sender, + preview: preview, + mentions: mentions, + userMemberId: userMemberId, + showSecrets: showSecrets, + commands: false, + backgroundColor: UIColor(backgroundColor) + ) +} + + +func messageText( + _ text: String, + _ formattedText: [FormattedText]?, + textStyle: UIFont.TextStyle = .body, + sender: String?, + preview: Bool = false, + mentions: [String: CIMention]?, + userMemberId: String?, + showSecrets: Set?, + commands: Bool = false, + 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 link = t.hasPrefix("http://") || t.hasPrefix("https://") + ? t + : "https://" + t + attrs[linkAttrKey] = link + attrs[webLinkAttrKey] = true + handleTaps = true + } + case let .hyperLink(text, uri): + attrs = linkAttrs() + if let text { t = text } + if !preview { + attrs[linkAttrKey] = uri + attrs[webLinkAttrKey] = true + handleTaps = true + } + case let .simplexLink(text, linkType, simplexUri, smpHosts): + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = simplexUri + handleTaps = true + } + if let s = text ?? (privacySimplexLinkModeDefault.get() == .description ? linkType.description : nil) { + res.append(NSAttributedString(string: s + " ", attributes: attrs)) + italic = italic ?? UIFont(descriptor: descr.withSymbolicTraits(.traitItalic) ?? descr, size: descr.pointSize) + attrs[.font] = italic + t = viaHost(smpHosts) + } + case let .command(cmdStr): + snippet = snippet ?? UIFont.monospacedSystemFont(ofSize: descr.pointSize, weight: .regular) + attrs[.font] = snippet + t = "/" + cmdStr + if !preview && commands { + attrs[.foregroundColor] = uiLinkColor + attrs[commandAttrKey] = t + handleTaps = true + } + case let .mention(memberName): + if let m = mentions?[memberName] { + mention = mention ?? UIFont(descriptor: descr.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: descr.pointSize) + 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] = "mailto:" + ft.text + handleTaps = true + } + case .phone: + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = "tel:" + t.replacingOccurrences(of: " ", with: "") + handleTaps = true + } + case .unknown: () + case .none: () + } + res.append(NSAttributedString(string: t, attributes: attrs)) + } + } else { + res.append(NSMutableAttributedString(string: text, attributes: plain)) + } + + return (string: res, hasSecrets: hasSecrets, handleTaps: handleTaps) + + func linkAttrs() -> [NSAttributedString.Key: Any] { + link = link ?? [ + .font: font, + .foregroundColor: uiLinkColor, + .underlineStyle: NSUnderlineStyle.single.rawValue + ] + return link! + } +} + +@inline(__always) +private func mentionText(_ name: String) -> String { + name.contains(" @") ? "@'\(name)'" : "@\(name)" } func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String { - linkType.description + " " + "(via \(smpHosts.first ?? "?"))" + linkType.description + " " + viaHost(smpHosts) +} + +func viaHost(_ smpHosts: [String]) -> String { + "(via \(smpHosts.first ?? "?"))" } struct MsgContentView_Previews: PreviewProvider { @@ -172,9 +465,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 587957cd5d..dfc620c402 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -41,7 +41,7 @@ struct ChatItemForwardingView: View { .alert(item: $alert) { $0.alert } } - @ViewBuilder private func forwardListView() -> some View { + private func forwardListView() -> some View { VStack(alignment: .leading) { if !chatsToForwardTo.isEmpty { List { diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 62ea607d27..87c6ba92f8 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -14,6 +14,7 @@ struct ChatItemInfoView: View { @Environment(\.dismiss) var dismiss @EnvironmentObject var theme: AppTheme var ci: ChatItem + var userMemberId: String? @Binding var chatItemInfo: ChatItemInfo? @State private var selection: CIInfoTab = .history @State private var alert: CIInfoViewAlert? = nil @@ -130,9 +131,9 @@ struct ChatItemInfoView: View { } } - @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() @@ -196,7 +197,7 @@ struct ChatItemInfoView: View { } } - @ViewBuilder private func historyTab() -> some View { + private func historyTab() -> some View { GeometryReader { g in let maxWidth = (g.size.width - 32) * 0.84 ScrollView { @@ -226,12 +227,13 @@ 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: backgroundColor) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(ci, theme)) + .background(backgroundColor) .modifier(ChatItemClipped()) .contextMenu { if itemVersion.msgContent.text != "" { @@ -256,9 +258,9 @@ 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: Color) -> 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() @@ -271,14 +273,18 @@ struct ChatItemInfoView: View { var text: String var formattedText: [FormattedText]? var sender: String? = nil - @State private var showSecrets = false + var mentions: [String: CIMention]? + var userMemberId: String? + var backgroundColor: Color + @State private var showSecrets: Set = [] var body: some View { - toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, showSecrets: showSecrets, secondaryColor: theme.colors.secondary)) + let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: UIColor(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 { @@ -296,9 +302,10 @@ 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: backgroundColor) .padding(.horizontal, 12) .padding(.vertical, 6) .background(quotedMsgFrameColor(qi, theme)) @@ -331,7 +338,7 @@ struct ChatItemInfoView: View { : 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() @@ -351,8 +358,9 @@ struct ChatItemInfoView: View { Button { Task { await MainActor.run { - ItemsModel.shared.loadOpenChat(forwardedFromItem.chatInfo.id) - dismiss() + ItemsModel.shared.loadOpenChat(forwardedFromItem.chatInfo.id) { + dismiss() + } } } } label: { @@ -368,7 +376,7 @@ struct ChatItemInfoView: View { } } - @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) @@ -399,7 +407,7 @@ struct ChatItemInfoView: View { } } - @ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { ScrollView { VStack(alignment: .leading, spacing: 16) { details() @@ -414,7 +422,7 @@ struct ChatItemInfoView: View { .frame(maxHeight: .infinity, alignment: .top) } - @ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { LazyVStack(alignment: .leading, spacing: 12) { let mss = membersStatuses(memberDeliveryStatuses) if !mss.isEmpty { @@ -548,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 ebbc55a932..5f48c18881 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -18,6 +18,10 @@ extension EnvironmentValues { 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 } @@ -27,26 +31,40 @@ extension EnvironmentValues { 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 + @ObservedObject var im: ItemsModel @EnvironmentObject var theme: AppTheme @Environment(\.showTimestamp) var showTimestamp: Bool @Environment(\.revealed) var revealed: Bool var chatItem: ChatItem + var scrollToItem: (ChatItem.ID) -> Void + @Binding var scrollToItemId: ChatItem.ID? var maxWidth: CGFloat = .infinity @Binding var allowMenu: Bool init( chat: Chat, + im: ItemsModel, chatItem: ChatItem, + scrollToItem: @escaping (ChatItem.ID) -> Void, + scrollToItemId: Binding = .constant(nil), showMember: Bool = false, maxWidth: CGFloat = .infinity, allowMenu: Binding = .constant(false) ) { self.chat = chat + self.im = im self.chatItem = chatItem + self.scrollToItem = scrollToItem + _scrollToItemId = scrollToItemId self.maxWidth = maxWidth _allowMenu = allowMenu } @@ -54,14 +72,14 @@ struct ChatItemView: View { var body: some View { let ci = chatItem if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) { - MarkedDeletedItemView(chat: chat, chatItem: chatItem) + MarkedDeletedItemView(chat: chat, im: im, 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, allowMenu: $allowMenu) } else if ci.content.msgContent == nil { - ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case + ChatItemContentView(chat: chat, im: im, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case } else { framedItemView() } @@ -89,7 +107,10 @@ struct ChatItemView: View { }() return FramedItemView( chat: chat, + im: im, chatItem: chatItem, + scrollToItem: scrollToItem, + scrollToItemId: $scrollToItemId, preview: preview, maxWidth: maxWidth, imgWidth: adjustedMaxWidth, @@ -104,6 +125,7 @@ struct ChatItemContentView: View { @EnvironmentObject var theme: AppTheme @Environment(\.revealed) var revealed: Bool @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel var chatItem: ChatItem var msgContentView: () -> Content @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -127,7 +149,9 @@ struct ChatItemContentView: View { case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case .rcvDirectEvent: eventItemView() case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem) + case .rcvGroupEvent(.newMemberPendingReview): CIEventView(eventText: pendingReviewEventItemText()) case .rcvGroupEvent: eventItemView() + case .sndGroupEvent(.userPendingReview): CIEventView(eventText: pendingReviewEventItemText()) case .sndGroupEvent: eventItemView() case .rcvConnEvent: eventItemView() case .sndConnEvent: eventItemView() @@ -136,7 +160,7 @@ struct ChatItemContentView: View { 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, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary) + CIChatFeatureView(chat: chat, im: im, 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) @@ -148,6 +172,7 @@ struct ChatItemContentView: View { case let .rcvDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo)) case .sndGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText()) case .rcvGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText()) + case .chatBanner: EmptyView() case let .invalidJSON(json): CIInvalidJSONView(json: json) } } @@ -168,6 +193,13 @@ struct ChatItemContentView: View { CIEventView(eventText: eventItemViewText(theme.colors.secondary)) } + private func pendingReviewEventItemText() -> Text { + Text(chatItem.content.text) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .fontWeight(.bold) + } + private func eventItemViewText(_ secondaryColor: Color) -> Text { if !revealed, let t = mergedGroupEventText { return chatEventText(t + textSpace + chatItem.timestampText, secondaryColor) @@ -183,7 +215,7 @@ struct ChatItemContentView: View { } private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View { - CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor) + CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, iconColor: iconColor) } private var mergedGroupEventText: Text? { @@ -210,16 +242,21 @@ struct ChatItemContentView: View { } private func directE2EEInfoText(_ info: E2EEInfo) -> Text { - info.pqEnabled - ? Text("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.") - .font(.caption) - .foregroundColor(theme.colors.secondary) - .fontWeight(.light) - : e2eeInfoNoPQText() + if let pqEnabled = info.pqEnabled { + pqEnabled + ? e2eeInfoText("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.") + : e2eeInfoNoPQText() + } else { + e2eeInfoText("Messages are protected by **end-to-end encryption**.") + } } 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.") + e2eeInfoText("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.") + } + + private func e2eeInfoText(_ s: LocalizedStringKey) -> Text { + Text(s) .font(.caption) .foregroundColor(theme.colors.secondary) .fontWeight(.light) @@ -243,16 +280,17 @@ func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text { struct ChatItemView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared Group{ - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello")) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too")) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂")) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂")) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂")) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample()) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true)).environment(\.revealed, true) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true)).environment(\.revealed, true) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getDeletedContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).environment(\.revealed, true) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).environment(\.revealed, true) } .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 70)) @@ -262,57 +300,72 @@ struct ChatItemView_Previews: PreviewProvider { struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil) Group{ ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)), quotedItem: nil, file: nil - ) + ), + scrollToItem: { _ in }, + scrollToItemId: Binding.constant(nil) ) ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead), content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2), quotedItem: nil, file: nil - ) + ), + scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil) ) ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin), quotedItem: nil, file: nil - ) + ), + scrollToItem: { _ in }, + scrollToItemId: Binding.constant(nil) ) ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)), quotedItem: nil, file: nil - ) + ), + scrollToItem: { _ in }, + scrollToItemId: Binding.constant(nil) ) ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)), content: ciFeatureContent, quotedItem: nil, file: nil - ) + ), + scrollToItem: { _ in }, + scrollToItemId: Binding.constant(nil) ) } .environment(\.revealed, true) diff --git a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift new file mode 100644 index 0000000000..93ecf870eb --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift @@ -0,0 +1,516 @@ +// +// 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, + _ im: ItemsModel, + _ pagination: ChatPagination, + _ 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, scope: im.groupScopeInfo?.toChatScope(), contentTag: im.contentTag, 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 = im.chatState.unreadAfterItemId + + let oldItems = Array(im.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 im.secondaryIMFilter == nil && chatModel.getChat(chat.id) == nil { + chatModel.addChat(chat) + } + await MainActor.run { + im.reversedChatItems = chat.chatItems.reversed() + if im.secondaryIMFilter == nil { + chatModel.updateChatInfo(chat.chatInfo) + } + im.chatState.splits = newSplits + if !chat.chatItems.isEmpty { + im.chatState.unreadAfterItemId = chat.chatItems.last!.id + } + im.chatState.totalAfter = navInfo.afterTotal + im.chatState.unreadTotal = chat.chatStats.unreadCount + im.chatState.unreadAfter = navInfo.afterUnread + im.chatState.unreadAfterNewestLoaded = navInfo.afterUnread + + im.preloadState.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, im.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 { + im.reversedChatItems = newReversed + im.chatState.splits = modifiedSplits.newSplits + im.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, im.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 { + im.reversedChatItems = newReversed + im.chatState.splits = newSplits + im.chatState.moveUnreadAfterItem(im.chatState.splits.first ?? new.last!.id, new) + // loading clear bottom area, updating number of unread items after the newest loaded item + if indexToAddIsLast { + im.chatState.unreadAfterNewestLoaded -= unreadInLoaded + } + } + case .around: + var newSplits: [Int64] + if openAroundItemId == nil { + newItems.append(contentsOf: oldItems) + newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, im.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 { + im.reversedChatItems = newReversed + im.chatState.splits = orderedSplits + im.chatState.unreadAfterItemId = chat.chatItems.last!.id + im.chatState.totalAfter = navInfo.afterTotal + im.chatState.unreadTotal = chat.chatStats.unreadCount + im.chatState.unreadAfter = navInfo.afterUnread + + if let openAroundItemId { + im.chatState.unreadAfterNewestLoaded = navInfo.afterUnread + if im.secondaryIMFilter == nil { + ChatModel.shared.openAroundItemId = openAroundItemId // TODO [knocking] move openAroundItemId from ChatModel to ItemsModel? + ChatModel.shared.chatId = chat.id + } + } else { + // no need to set it, count will be wrong + // chatState.unreadAfterNewestLoaded = navInfo.afterUnread + } + im.preloadState.clear() + } + case .last: + newItems.append(contentsOf: oldItems) + let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, im.chatState.splits) + newItems.append(contentsOf: chat.chatItems) + let items = newItems + await MainActor.run { + im.reversedChatItems = items.reversed() + im.chatState.splits = newSplits + if im.secondaryIMFilter == nil { + chatModel.updateChatInfo(chat.chatInfo) + } + im.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..5f2102b8bc --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift @@ -0,0 +1,457 @@ +// +// 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 im: ItemsModel + 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(_ im: ItemsModel, _ revealedItems: Set) -> MergedItems { + if im.reversedChatItems.isEmpty { + return MergedItems(im: im, items: [], splits: [], indexInParentItems: [:]) + } + + let unreadCount = im.chatState.unreadTotal + + let unreadAfterItemId = im.chatState.unreadAfterItemId + let itemSplits = im.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 - im.chatState.unreadAfterNewestLoaded + var lastRevealedIdsInMergedItems: BoxedValue<[Int64]>? = nil + var lastRangeInReversedForMergedItems: BoxedValue>? = nil + var recent: MergedItem? = nil + while index < im.reversedChatItems.count { + let item = im.reversedChatItems[index] + let prev = index >= 1 ? im.reversedChatItems[index - 1] : nil + let next = index + 1 < im.reversedChatItems.count ? im.reversedChatItems[index + 1] : nil + let category = item.mergeCategory + let itemIsSplit = itemSplits.contains(item.id) + + if item.id == unreadAfterItemId { + unreadBefore = unreadCount - im.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: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1)) + } + unclosedSplitIndex = index + unclosedSplitIndexInParent = visibleItemIndexInParent + } else if index + 1 == im.reversedChatItems.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: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent)) + } + indexInParentItems[item.id] = visibleItemIndexInParent + index += 1 + } + return MergedItems( + im: im, + 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 { + // 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(_ im: ItemsModel, _ 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 = im.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 im.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..2fb1c3fb35 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift @@ -0,0 +1,174 @@ +// +// 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, _ im: ItemsModel) 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, im, ChatPagination.last(count: 50)) + await MainActor.run { + loadingMoreItems.wrappedValue = false + loadingBottomItems.wrappedValue = false + } +} + +func preloadIfNeeded( + _ im: ItemsModel, + _ allowLoadMoreItems: Binding, + _ ignoreLoadingRequests: Binding, + _ listState: EndlessScrollView.ListState, + _ mergedItems: BoxedValue, + loadItems: @escaping (Bool, ChatPagination) async -> Bool, + loadLastItems: @escaping () async -> Void +) { + let state = im.preloadState + 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(im, 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 && !im.lastItemsLoaded { + await loadLastItems() + } + } + } else if listState.itemsCanCoverScreen && !im.lastItemsLoaded { + state.preloading = true + Task { + defer { state.preloading = false } + await loadLastItems() + } + } +} + +func preloadItems( + _ im: ItemsModel, + _ 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] = im.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 == im.reversedChatItems.count && firstItemIdWas == im.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] = im.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 baceb5b4ab..709758655f 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -15,40 +15,59 @@ private let memberImageSize: CGFloat = 34 struct ChatView: View { @EnvironmentObject var chatModel: ChatModel - @ObservedObject var im = ItemsModel.shared + @StateObject private var connectProgressManager = ConnectProgressManager.shared + @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 - @StateObject private var scrollModel = ReverseListScrollModel() + @ObservedObject var im: ItemsModel + @State var mergedItems: BoxedValue + @State var floatingButtonModel: FloatingButtonModel + @Binding var scrollToItemId: ChatItem.ID? @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 loadingItems = false - @State private var firstPage = false - @State private var revealedChatItem: ChatItem? - @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 selectedMember: GMember? = nil // opening GroupLinkView on link button (incognito) @State private var showGroupLinkSheet: Bool = false - @State private var groupLink: String? + @State private var groupLink: GroupLink? @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 showUserSupportChatSheet = false + @State private var showCommandsMenu = false + @State private var supportChatMemberInfoLinkActive = false + + @State private var scrollView: EndlessScrollView = EndlessScrollView(frame: .zero) @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + let userSupportScopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil) + var body: some View { if #available(iOS 16.0, *) { viewBody @@ -59,44 +78,97 @@ struct ChatView: View { } } - @ViewBuilder private var viewBody: some View { let cInfo = chat.chatInfo - ZStack { + let memberSupportChat: (groupInfo: GroupInfo, member: GroupMember?)? = + if case let .group(groupInfo, .memberSupport(member)) = cInfo { + (groupInfo, member) + } else { + nil + } + let userMemberKnockingChat = memberSupportChat?.groupInfo.membership.memberPending == true + 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 + .if(wallpaperImage != nil && im.secondaryIMFilter == nil) { view in view.modifier( ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor) ) } VStack(spacing: 0) { ZStack(alignment: .bottomTrailing) { - chatItemsList() - FloatingButtons(theme: theme, scrollModel: scrollModel, chat: chat) + if userMemberKnockingChat { + ZStack(alignment: .top) { + chatItemsList() + userMemberKnockingTitleBar() + } + } else { + chatItemsList() + } + if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty { + GroupMentionsView(im: im, groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible) + } + if !chat.chatInfo.menuCommands.isEmpty { + CommandsMenuView(chat: chat, composeState: $composeState, selectedRange: $selectedRange, showCommandsMenu: $showCommandsMenu) + } + FloatingButtons(im: im, theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: { + mergedItems.boxedValue = MergedItems.create(im, revealedItems) + scrollView.updateItems(mergedItems.boxedValue.items) + } + ) + } + if let connectInProgressText = connectProgressManager.showConnectProgress { + connectInProgressView(connectInProgressText) + } + if let connectingText { + Text(connectingText) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .padding(.top) } - connectingText() if selectedChatItems == nil { + let reason = chat.chatInfo.userCantSendReason + let composeEnabled = ( + chat.chatInfo.sendMsgEnabled || + (chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) || // allow to join prepared group without message + (chat.chatInfo.contact?.nextAcceptContactRequest ?? false) // allow to accept or reject contact request + ) ComposeView( chat: chat, + im: im, composeState: $composeState, - keyboardVisible: $keyboardVisible + showCommandsMenu: $showCommandsMenu, + keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate, + selectedRange: $selectedRange, + disabledText: reason?.composeLabel ) - .disabled(!cInfo.sendMsgEnabled) + .disabled(!composeEnabled) + .if(!composeEnabled) { v in + v.disabled(true).onTapGesture { + AlertManager.shared.showAlertMsg( + title: "You can't send messages!", + message: reason?.alertMessage + ) + } + } } else { SelectedItemsBottomToolbar( - chatItems: ItemsModel.shared.reversedChatItems, + im: im, selectedChatItems: $selectedChatItems, chatInfo: chat.chatInfo, deleteItems: { forAll in allowToDeleteSelectedMessagesForAll = forAll showDeleteSelectedMessages = true }, + archiveItems: { + showArchiveSelectedReports = true + }, moderateItems: { - if case let .group(groupInfo) = chat.chatInfo { + if case let .group(groupInfo, _) = chat.chatInfo { showModerateSelectedMessagesAlert(groupInfo) } }, @@ -104,15 +176,44 @@ struct ChatView: View { ) } } + if im.showLoadingProgress == chat.id { + ProgressView().scaleEffect(2) + } + if case let .group(groupInfo, _) = chat.chatInfo, + case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter, + case let .memberSupport(groupMember_) = groupScopeInfo, + let groupMember = groupMember_ { + NavigationLink(isActive: $supportChatMemberInfoLinkActive) { + GroupMemberInfoView( + groupInfo: groupInfo, + chat: chat, + groupMember: GMember(groupMember), + scrollToItemId: $scrollToItemId, + openedFromSupportChat: true + ) + .navigationBarHidden(false) + .modifier(BackButton(disabled: Binding.constant(false)) { + supportChatMemberInfoLinkActive = false + }) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } } .safeAreaInset(edge: .top) { VStack(spacing: .zero) { - if searchMode { searchToolbar() } + if showSearch { searchToolbar() } Divider() } .background(ToolbarMaterial.material(toolbarMaterial)) } - .navigationTitle(cInfo.chatViewName) + .navigationTitle( + memberSupportChat == nil + ? cInfo.chatViewName + : memberSupportChat?.member?.chatViewName ?? NSLocalizedString("Chat with admins", comment: "chat toolbar") + ) .background(theme.colors.background) .navigationBarTitleDisplayMode(.inline) .environmentObject(theme) @@ -130,24 +231,37 @@ struct ChatView: View { } } } - .appSheet(item: $selectedMember) { member in - Group { - if case let .group(groupInfo) = chat.chatInfo { - GroupMemberInfoView( - groupInfo: groupInfo, - chat: chat, - groupMember: member, - navigation: true - ) + .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, 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, selected.sorted(), true, deletedSelectedMessages) + } + } + } + } + .appSheet(item: $selectedMember, onDismiss: { + chatModel.secondaryIM = nil + }) { member in + if case let .group(groupInfo, _) = chat.chatInfo { + GroupMemberInfoView( + groupInfo: groupInfo, + chat: chat, + groupMember: member, + scrollToItemId: $scrollToItemId, + 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) - } + if case let .group(groupInfo, _) = cInfo { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) } } .sheet(isPresented: Binding( @@ -166,44 +280,123 @@ struct ChatView: View { ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState) } } + .appSheet( + isPresented: $showUserSupportChatSheet, + onDismiss: { + if chat.chatInfo.groupInfo?.membership.memberPending ?? false { + chatModel.chatId = nil + } + } + ) { + if let groupInfo = cInfo.groupInfo { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: userSupportScopeInfo), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } + } .onAppear { + ConnectProgressManager.shared.cancelConnectProgress() + 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 + } + } + } + } + // if this is the main chat of the group with the pending member (knocking) + if case let .group(groupInfo, nil) = chat.chatInfo, + groupInfo.membership.memberPending { + ItemsModel.loadSecondaryChat(chat.id, chatFilter: .groupChatScopeContext(groupScopeInfo: userSupportScopeInfo)) { + showUserSupportChatSheet = true + chatModel.secondaryPendingInviteeChatOpened = true + } + } + } + .onChange(of: chatModel.secondaryPendingInviteeChatOpened) { secondaryChatOpened in + if secondaryChatOpened { + ItemsModel.loadSecondaryChat(chat.id, chatFilter: .groupChatScopeContext(groupScopeInfo: userSupportScopeInfo)) { + showUserSupportChatSheet = true + } + } } .onChange(of: chatModel.chatId) { cId in + ConnectProgressManager.shared.cancelConnectProgress() showChatInfoSheet = false selectedChatItems = nil - scrollModel.scrollToBottom() + 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, revealedItems) + 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() } } - .onChange(of: revealedChatItem) { _ in - NotificationCenter.postReverseListNeedsLayout() - } - .onChange(of: im.isLoading) { isLoading in - if !isLoading, - im.reversedChatItems.count <= loadItemsPerPage, - filtered(im.reversedChatItems).count < 10 { - loadChatItems(chat.chatInfo) + .onChange(of: chatModel.secondaryPendingInviteeChatOpened) { opened in + if im.secondaryIMFilter != nil && !opened { + Task { + try? await Task.sleep(nanoseconds: 650_000000) + dismiss() + } + } + } + .onChange(of: chatModel.openAroundItemId) { openAround in + if let openAround { + closeSearch() + mergedItems.boxedValue = MergedItems.create(im, revealedItems) + 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 + } + } } } - .environmentObject(scrollModel) .onDisappear { + ConnectProgressManager.shared.cancelConnectProgress() 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 = [] + chatModel.chatAgentConnId = nil + chatModel.chatSubStatus = nil + im.reversedChatItems = [] + im.chatState.clear() chatModel.groupMembers = [] chatModel.groupMembersIndexes.removeAll() chatModel.membersLoaded = false @@ -216,148 +409,280 @@ struct ChatView: View { } .toolbar { ToolbarItem(placement: .principal) { - if selectedChatItems != nil { - SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) - } else if case let .direct(contact) = cInfo { - Button { - Task { - showChatInfoSheet = true - } - } label: { - ChatInfoToolbar(chat: chat) - } - .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 chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } } - } label: { - ChatInfoToolbar(chat: chat) - .tint(theme.colors.primary) - } - .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { - GroupChatInfoView( - chat: chat, - groupInfo: Binding( - get: { groupInfo }, - set: { gInfo in - chat.chatInfo = .group(groupInfo: gInfo) - chat.created = Date.now - } - ), - onSearch: { focusSearch() }, - localAlias: groupInfo.localAlias - ) - } - } else if case .local = cInfo { - ChatInfoToolbar(chat: chat) + if im.secondaryIMFilter == nil { + primaryPrincipalToolbarContent() + } else if !userMemberKnockingChat { // no toolbar while knocking chat, it's unstable on sheet + secondaryPrincipalToolbarContent() } } ToolbarItem(placement: .navigationBarTrailing) { - let isLoading = im.isLoading && im.showLoadingProgress - if selectedChatItems != nil { - Button { - withAnimation { - selectedChatItems = nil - } - } 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) - } - } - Menu { - if !isLoading { - 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") - .tint(isLoading ? Color.clear : nil) - .overlay { if isLoading { ProgressView() } } - } - } - 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 { - if !isLoading { - searchButton() - ToggleNtfsButton(chat: chat) - } - } label: { - Image(systemName: "ellipsis") - .tint(isLoading ? Color.clear : nil) - .overlay { if isLoading { ProgressView() } } - } - } - case .local: - searchButton() - default: - EmptyView() + if im.secondaryIMFilter == nil { + primaryTrailingToolbarContent() + } else if !userMemberKnockingChat { + secondaryTrailingToolbarContent() + } + } + } + .if(im.secondaryIMFilter == nil) { v in + v.onChange(of: scrollToItemId) { itemId in + if let itemId = itemId { + dismissAllSheets(animated: false) { + scrollToItem(itemId) + scrollToItemId = nil } } } } } - + + private func connectInProgressView(_ s: String) -> some View { + VStack(spacing: 0) { + Divider() + + HStack(spacing: 12) { + ProgressView() + Text(s) + + Spacer() + + Button { + ConnectProgressManager.shared.cancelConnectProgress() + } label: { + Image(systemName: "multiply") + } + .tint(theme.colors.primary) + } + .padding(12) + .frame(minHeight: 54) + .frame(maxWidth: .infinity, alignment: .leading) + .background(ToolbarMaterial.material(toolbarMaterial)) + } + } + + @inline(__always) + @ViewBuilder private func primaryPrincipalToolbarContent() -> some View { + let cInfo = chat.chatInfo + if selectedChatItems != nil { + SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) + } else if case let .direct(contact) = cInfo { + Button { + Task { + showChatInfoSheet = true + } + } label: { + ChatInfoToolbar(chat: chat) + } + .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 chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } } + } label: { + ChatInfoToolbar(chat: chat) + .tint(theme.colors.primary) + } + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { + chatModel.secondaryIM = nil + theme = buildTheme() + }) { + GroupChatInfoView( + chat: chat, + groupInfo: Binding( + get: { groupInfo }, + set: { gInfo in + chat.chatInfo = .group(groupInfo: gInfo, groupChatScope: nil) + chat.created = Date.now + } + ), + scrollToItemId: $scrollToItemId, + onSearch: { focusSearch() }, + localAlias: groupInfo.localAlias + ) + } + } else if case .local = cInfo { + ChatInfoToolbar(chat: chat) + } + } + + @inline(__always) + @ViewBuilder private func primaryTrailingToolbarContent() -> some View { + let cInfo = chat.chatInfo + if selectedChatItems != nil { + Button { + withAnimation { + selectedChatItems = nil + } + } 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) + } + } + 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") + } + } + 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() + } + } + } + + @inline(__always) + @ViewBuilder private func secondaryPrincipalToolbarContent() -> some View { + if selectedChatItems != nil { + SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) + } else { + switch im.secondaryIMFilter { + case let .groupChatScopeContext(groupScopeInfo): + switch groupScopeInfo { + case let .memberSupport(groupMember_): + if let groupMember = groupMember_ { + Button { + supportChatMemberInfoLinkActive = true + } label: { + MemberSupportChatToolbar(groupMember: groupMember) + } + } else { + textChatToolbar("Chat with admins") + } + case .reports: + textChatToolbar("Member reports") + } + case let .msgContentTagContext(contentTag): + switch contentTag { + case .report: + textChatToolbar("Member reports") + default: + EmptyView() + } + case .none: + EmptyView() + } + } + } + + @inline(__always) + @ViewBuilder private func secondaryTrailingToolbarContent() -> some View { + if selectedChatItems != nil { + Button { + withAnimation { + selectedChatItems = nil + } + } label: { + Text("Cancel") + } + } else { + searchButton() + } + } + + @inline(__always) + private func userMemberKnockingTitleBar() -> some View { + VStack(spacing: 0) { + Text("Chat with admins") + .font(.headline) + .foregroundColor(theme.colors.onBackground) + .padding(.top, 8) + .padding(.bottom, 14) + .frame(maxWidth: .infinity) + .background(ToolbarMaterial.material(toolbarMaterial)) + Divider() + } + } + + func textChatToolbar(_ text: LocalizedStringKey) -> some View { + Text(text) + .font(.headline) + .lineLimit(1) + .foregroundColor(theme.colors.onBackground) + .frame(width: 220) + } + private func initChatView() { let cInfo = chat.chatInfo // This check prevents the call to apiContactInfo after the app is suspended, and the database is closed. - if case .active = scenePhase, - case let .direct(contact) = cInfo { - Task { - do { - let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId) - await MainActor.run { - if let s = stats { - chatModel.updateContactConnectionStats(contact, s) + if case .active = scenePhase { + if case let .direct(contact) = cInfo { + Task { + do { + let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId) + await MainActor.run { + if let s = stats { + chatModel.updateContactConnectionStats(contact, s) + if let conn = contact.activeConn { + chatModel.chatAgentConnId = conn.agentConnId + chatModel.chatSubStatus = s.subStatus + } + } } + } catch let error { + logger.error("apiContactInfo error: \(responseError(error))") + } + } + } else { + Task { + await MainActor.run { + chatModel.chatAgentConnId = nil + chatModel.chatSubStatus = nil } - } catch let error { - logger.error("apiContactInfo error: \(responseError(error))") } } } @@ -370,7 +695,40 @@ struct ChatView: View { await markChatUnread(chat, unreadChat: false) } } - ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount + floatingButtonModel.updateOnListChange(scrollView.listState) + } + + private func scrollToItem(_ 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 = im.reversedChatItems.count + let triedToLoad = await loadChatItems(chat, pagination) + if !triedToLoad { + return + } + var repeatsLeft = 50 + while oldSize == im.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(im.reversedChatItems.count - 1, index)) + await MainActor.run { animatedScrollingInProgress = false } + } + } + } + } catch { + logger.error("Error scrolling to item: \(error)") + } + } } private func searchToolbar() -> some View { @@ -394,16 +752,14 @@ struct ChatView: View { .cornerRadius(10.0) Button ("Cancel") { - searchText = "" - searchMode = false - searchFocussed = false - Task { await 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 } @@ -421,204 +777,427 @@ struct ChatView: View { .map { $0.element } } - private func chatItemsList() -> some View { let cInfo = chat.chatInfo - let mergedItems = filtered(im.reversedChatItems) return GeometryReader { g in - ReverseList(items: mergedItems, scrollState: $scrollModel.state) { 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 - return ChatItemWithMenu( - chat: $chat, - chatItem: ci, - maxWidth: maxWidth, - composeState: $composeState, - selectedMember: $selectedMember, - showChatInfoSheet: $showChatInfoSheet, - revealedChatItem: $revealedChatItem, - selectedChatItems: $selectedChatItems, - forwardedChatItems: $forwardedChatItems - ) + //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 + } + return Group { + if case .chatBanner = ci.content { + VStack { + ChatBannerView(chat: $chat) + .padding(.bottom, 90) + .padding(.top, 8) + + let listItem = mergedItem.newest() + if let prevItem = listItem.prevItem { + DateSeparator(date: prevItem.meta.itemTs).padding(8) + } + } + } else { + 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 + ChatItemWithMenu( + im: im, + chat: $chat, + index: index, + isLastItem: index == mergedItems.boxedValue.items.count - 1, + chatItem: ci, + scrollToItem: scrollToItem, + 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 - } loadPage: { - loadChatItems(cInfo) } - .opacity(ItemsModel.shared.isLoading ? 0 : 1) - .padding(.vertical, -InvertedTableView.inset) - .onTapGesture { hideKeyboard() } - .onChange(of: searchText) { _ in - Task { await loadChat(chat: chat, search: searchText) } + .onAppear { + if !im.isLoading { + updateWithInitiallyLoadedItems() + } } - .onChange(of: im.itemAdded) { added in - if added { + .onChange(of: im.isLoading) { loading in + if !loading { + updateWithInitiallyLoadedItems() + } + } + .onChange(of: im.reversedChatItems) { items in + mergedItems.boxedValue = MergedItems.create(im, revealedItems) + scrollView.updateItems(mergedItems.boxedValue.items) + if im.itemAdded { im.itemAdded = false - if FloatingButtonModel.shared.isReallyNearBottom { - scrollModel.scrollToBottom() + if scrollView.listState.firstVisibleItemIndex < 2 { + scrollView.scrollToBottomAnimated() + } else { + scrollView.scroll(by: 34) } } } + .onChange(of: revealedItems) { revealed in + mergedItems.boxedValue = MergedItems.create(im, revealed) + 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) + } + } } } - @ViewBuilder private func connectingText() -> some View { - if case let .direct(contact) = chat.chatInfo, - !contact.sndReady, - contact.active, - !contact.nextSendGrpInv { - Text("connecting…") - .font(.caption) - .foregroundColor(theme.colors.secondary) - .padding(.top) - } else { - EmptyView() + struct ChatBannerView: View { + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness + @Binding @ObservedObject var chat: Chat + @State private var showSecrets: Set = [] + + var body: some View { + let v = VStack(spacing: 8) { + ChatInfoImage(chat: chat, size: alertProfileImageSize) + + Text(chat.chatInfo.displayName) + .font(.title3) + .multilineTextAlignment(.center) + .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: 240) + + let fullName = chat.chatInfo.fullName.trimmingCharacters(in: .whitespacesAndNewlines) + if fullName != "" && fullName != chat.chatInfo.displayName && fullName != chat.chatInfo.displayName.trimmingCharacters(in: .whitespacesAndNewlines) { + Text(chat.chatInfo.fullName) + .font(.subheadline) + .multilineTextAlignment(.center) + .lineLimit(3) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: 260) + } + + if let shortDescr = chat.chatInfo.shortDescr { + let r = markdownText(shortDescr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) + .multilineTextAlignment(.center) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal) + } + + if let chatContext { + Text(chatContext) + .font(.callout) + .foregroundColor(theme.colors.secondary) + .padding(.top, 8) + } + } + .frame(maxWidth: .infinity) + .padding() + .background(theme.appColors.receivedMessage) + .clipShape(RoundedRectangle(cornerRadius: msgRectMaxRadius * roundness)) + if let (label, connLink) = chatAddress() { + v.contextMenu { + Button { + let shareItems: [Any] = [connLink] + showShareSheet(items: shareItems) + } label: { + Label(label, systemImage: "square.and.arrow.up") + } + } + .padding(.horizontal) + } else { + v.padding(.horizontal) + } + + } + + func chatAddress() -> (label: LocalizedStringKey, connLink: String)? { + switch chat.chatInfo { + case let .direct(contact): + if !contact.nextConnectPrepared && !contact.nextAcceptContactRequest { + let connLink: String? = if let pct = contact.preparedContact, case .con = pct.uiConnLinkType { + pct.connLinkToConnect.simplexChatUri() + } else { + contact.profile.contactLink + } + if let connLink { + return ("SimpleX address", connLink) + } + } + case let .group(groupInfo, _): + if !groupInfo.nextConnectPrepared { + if let pg = groupInfo.preparedGroup { + let connLink = pg.connLinkToConnect.simplexChatUri() + switch groupInfo.businessChat?.chatType { + case .none: return ("Group link", connLink) + case .business: return ("Business address", connLink) + default: () + } + } + } + default: () + } + return nil + } + + var chatContext: LocalizedStringKey? { + switch chat.chatInfo { + case let .direct(contact): + if contact.nextConnectPrepared, let linkType = contact.preparedContact?.uiConnLinkType { + switch linkType { + case .inv: + "Tap Connect to chat" + case .con: + contact.isBot ? "Tap Connect to use bot" : "Tap Connect to send request" + } + } else if contact.nextAcceptContactRequest { + "Accept contact request" + } else if case .bot = contact.profile.peerType { + "Bot" + } else { + "Your contact" + } + case let .group(groupInfo, _): + switch groupInfo.businessChat?.chatType { + case .none: + if groupInfo.nextConnectPrepared { + "Tap Join group" + } else { + switch (groupInfo.membership.memberStatus) { + case .memInvited: "Join group" + case .memCreator: "Your group" + default: "Group" + } + } + case .business: + if groupInfo.nextConnectPrepared { + "Tap Connect to chat" + } else { + "Business connection" + } + case .customer: + "Your business contact" + } + default: nil + } } } - class FloatingButtonModel: ObservableObject { - static let shared = FloatingButtonModel() - @Published var unreadBelow: Int = 0 - @Published var isNearBottom: Bool = true - @Published var date: Date? - @Published var isDateVisible: Bool = false - var totalUnread: Int = 0 - var isReallyNearBottom: Bool = true - var hideDateWorkItem: DispatchWorkItem? + private var connectingText: LocalizedStringKey? { + switch (chat.chatInfo) { + case let .direct(contact): + if !contact.sndReady && contact.active && !contact.sendMsgToConnect && !contact.nextAcceptContactRequest { + (contact.preparedContact?.uiConnLinkType == .con && !contact.isBot) || contact.contactGroupMemberId != nil + ? "contact should accept…" + : "connecting…" + } else { + nil + } + case let .group(groupInfo, _): + switch (groupInfo.membership.memberStatus) { + case .memUnknown: groupInfo.preparedGroup?.connLinkStartedConnection == true ? "connecting…" : nil + case .memAccepted: "connecting…" + default: nil + } + default: nil + } + } - func updateOnListChange(_ listState: ListState) { - let im = ItemsModel.shared - let unreadBelow = - if let id = listState.bottomItemId, - let index = im.reversedChatItems.firstIndex(where: { $0.id == id }) - { - im.reversedChatItems[.. 0 && listState.scrollOffset < 500 + private func searchTextChanged(_ s: String) { + Task { + await loadChat(chat: chat, im: im, search: s) + mergedItems.boxedValue = MergedItems.create(im, revealedItems) + await MainActor.run { + scrollView.updateItems(mergedItems.boxedValue.items) } - - // set floating button indication mode - let nearBottom = listState.scrollOffset < 800 - 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) + 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() } } - - 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 { + @ObservedObject var im: ItemsModel let theme: AppTheme - let scrollModel: ReverseListScrollModel + let scrollView: EndlessScrollView let chat: Chat - @ObservedObject var model = FloatingButtonModel.shared + @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 { + if let date = model.date, date.timeIntervalSince1970 > 0 { DateSeparator(date: date) .padding(.vertical, 4).padding(.horizontal, 8) .background(.thinMaterial) .clipShape(Capsule()) .opacity(model.isDateVisible ? 1 : 0) + .padding(.vertical, 4) } VStack { - let unreadAbove = model.totalUnread - model.unreadBelow - if unreadAbove > 0 { - circleButton { - unreadCountText(unreadAbove) - .font(.callout) - .foregroundColor(theme.colors.primary) - } - .onTapGesture { - scrollModel.scrollToNextPage() - } - .contextMenu { - Button { - Task { - await markChatRead(chat) + 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(im, chat) + } + } label: { + Label("Mark read", systemImage: "checkmark") } - } label: { - Label("Mark read", systemImage: "checkmark") } } } Spacer() - if model.unreadBelow > 0 { - circleButton { - unreadCountText(model.unreadBelow) - .font(.callout) - .foregroundColor(theme.colors.primary) + 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 || !im.lastItemsLoaded { + requestedTopScroll = false + requestedBottomScroll = true + } else { + scrollToBottom() + } + } } - .onTapGesture { - scrollModel.scrollToBottom() - } - } else if !model.isNearBottom { - circleButton { - Image(systemName: "chevron.down") - .foregroundColor(theme.colors.primary) - } - .onTapGesture { scrollModel.scrollToBottom() } } } .padding() .frame(maxWidth: .infinity, alignment: .trailing) } + .onChange(of: loadingTopItems) { loading in + if !loading && requestedTopScroll { + requestedTopScroll = false + scrollToTopUnread() + } + } + .onChange(of: loadingBottomItems) { loading in + if !loading && requestedBottomScroll && im.lastItemsLoaded { + requestedBottomScroll = false + scrollToBottom() + } + } .onDisappear(perform: model.resetDate) } + private func scrollToTopUnread() { + Task { + if !im.chatState.splits.isEmpty { + await MainActor.run { loadingMoreItems = true } + await loadChat(chatId: chat.id, im: im, 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)") + } + } + } + + 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() @@ -677,14 +1256,32 @@ struct ChatView: View { } private func focusSearch() { - searchMode = true + 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 { + if case let .group(gInfo, _) = chat.chatInfo { Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } } } } label: { @@ -694,11 +1291,12 @@ struct ChatView: View { private func groupLinkButton() -> some View { Button { - if case let .group(gInfo) = chat.chatInfo { + if case let .group(gInfo, _) = chat.chatInfo { Task { do { - if let link = try apiGetGroupLink(gInfo.groupId) { - (groupLink, groupLinkMemberRole) = link + if let gLink = try apiGetGroupLink(gInfo.groupId) { + groupLink = gLink + groupLinkMemberRole = gLink.acceptMemberRole } } catch let error { logger.error("ChatView apiGetGroupLink: \(responseError(error))") @@ -745,6 +1343,7 @@ struct ChatView: View { let (validItems, confirmation) = try await apiPlanForwardChatItems( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), itemIds: Array(selectedChatItems) ) if let confirmation { @@ -810,7 +1409,7 @@ struct ChatView: View { ) } } - + func forwardAction(_ items: [Int64]) -> UIAlertAction { UIAlertAction( title: NSLocalizedString("Forward messages", comment: "alert action"), @@ -834,7 +1433,6 @@ struct ChatView: View { } func openForwardingSheet(_ items: [Int64]) async { - let im = ItemsModel.shared var items = Set(items) var fci = [ChatItem]() for reversedChatItem in im.reversedChatItems { @@ -848,43 +1446,38 @@ struct ChatView: View { } } - private func loadChatItems(_ cInfo: ChatInfo) { - Task { - if loadingItems || firstPage { return } - loadingItems = true - do { - var reversedPage = Array() - var chatItemsAvailable = true - // Load additional items until the page is +50 large after merging - while chatItemsAvailable && filtered(reversedPage).count < loadItemsPerPage { - let pagination: ChatPagination = - if let lastItem = reversedPage.last ?? im.reversedChatItems.last { - .before(chatItemId: lastItem.id, count: loadItemsPerPage) - } else { - .last(count: loadItemsPerPage) - } - let chatItems = try await apiGetChatItems( - type: cInfo.chatType, - id: cInfo.apiId, - pagination: pagination, - search: searchText - ) - chatItemsAvailable = !chatItems.isEmpty - reversedPage.append(contentsOf: chatItems.reversed()) - } - await MainActor.run { - if reversedPage.count == 0 { - firstPage = true - } else { - im.reversedChatItems.append(contentsOf: reversedPage) - } - loadingItems = false - } - } catch let error { - logger.error("apiGetChat error: \(responseError(error))") - await MainActor.run { loadingItems = false } + 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, + im, + pagination, + searchText, + nil, + { visibleItemIndexesNonReversed(im, scrollView.listState, mergedItems.boxedValue) } + ) + return true } func stopAudioPlayer() { @@ -892,96 +1485,151 @@ struct ChatView: View { VoiceItemState.chatView = [:] } + func onChatItemsUpdated() { + if !mergedItems.boxedValue.isActualState() { + //logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(im.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(im.reversedChatItems.count)") + return + } + floatingButtonModel.updateOnListChange(scrollView.listState) + preloadIfNeeded( + im, + $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, im) + } + } + ) + } + private struct ChatItemWithMenu: View { + @ObservedObject var im: ItemsModel @EnvironmentObject var m: ChatModel @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 scrollToItem: (ChatItem.ID) -> Void + @Binding var scrollToItemId: ChatItem.ID? + let merged: MergedItem let maxWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? @Binding var showChatInfoSheet: Bool - @Binding var revealedChatItem: ChatItem? + @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 archivingReports: Set? = nil + @State private var showArchivingReports = false @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? @State private var msgWidth: CGFloat = 0 + @State private var touchInProgress: Bool = false @Binding var selectedChatItems: Set? @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 - var revealed: Bool { chatItem == revealedChatItem } + var revealed: Bool { revealedItems.contains(chatItem.id) } typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?) - func getItemSeparation(_ chatItem: ChatItem, at i: Int?) -> ItemSeparation { - let im = ItemsModel.shared - if let i, i > 0 && im.reversedChatItems.count >= i { - let nextItem = im.reversedChatItems[i - 1] - let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60 - return ( - timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs), - largeGap: largeGap, - date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs - ) + 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 { - return (timestamp: true, largeGap: true, date: nil) + 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 = m.getChatItemIndex(chatItem) - let ciCategory = chatItem.mergeCategory - let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory) - let range = itemsRange(currIndex, prevHidden) - let timeSeparation = getItemSeparation(chatItem, at: currIndex) - let im = ItemsModel.shared - Group { - if revealed, let range = range { - let items = Array(zip(Array(range), im.reversedChatItems[range])) - VStack(spacing: 0) { - ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in - let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1] - chatItemView(ci, nil, prev, getItemSeparation(ci, at: i)) - .overlay { - if let selected = selectedChatItems, ci.canBeDeletedForSelf { - Color.clear - .contentShape(Rectangle()) - .onTapGesture { - let checked = selected.contains(ci.id) - selectUnselectChatItem(select: !checked, ci) - } - } - } - } - } - } else { - VStack(spacing: 0) { - chatItemView(chatItem, range, prevItem, timeSeparation) - if let date = timeSeparation.date { - DateSeparator(date: date).padding(8) - } - } + 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 { + nil + } + let showAvatar = shouldShowAvatar(item, merged.oldest().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()) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { let checked = selected.contains(chatItem.id) selectUnselectChatItem(select: !checked, chatItem) - } + }) } } + if let date = itemSeparation.date { + DateSeparator(date: date).padding(8) } } .onAppear { @@ -991,42 +1639,73 @@ struct ChatView: View { markedRead = true } if let range { - let itemIds = unreadItemIds(range) + let (itemIds, unreadMentions) = unreadItemIds(range) if !itemIds.isEmpty { waitToMarkRead { - await apiMarkChatItemsRead(chat.chatInfo, itemIds) + await apiMarkChatItemsRead(im, chat.chatInfo, itemIds, mentionsRead: unreadMentions) } } } else if chatItem.isRcvNew { waitToMarkRead { - await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id]) + await apiMarkChatItemsRead(im, 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] { - let im = ItemsModel.shared - return range.compactMap { i in - if i >= 0 && i < im.reversedChatItems.count { - let ci = im.reversedChatItems[i] - return if ci.isRcvNew { ci.id } else { nil } - } else { - return nil + private func unreadItemIds(_ range: ClosedRange) -> ([ChatItem.ID], Int) { + 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) { - Task { - _ = try? await Task.sleep(nanoseconds: 600_000000) - if m.chatId == chat.chatInfo.id { - await op() + markReadTask = Task { + do { + _ = try await Task.sleep(nanoseconds: 600_000000) + if m.chatId == chat.chatInfo.id { + await op() + } + } catch { + // task was cancelled } } } - + + private var searchIsNotBlank: Bool { + get { + searchText.count > 0 && !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + } @available(iOS 16.0, *) struct MemberLayout: Layout { @@ -1072,36 +1751,46 @@ struct ChatView: View { } } - @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View { + @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 let .group(groupInfo, _) = chat.chatInfo { + if showAvatar { VStack(alignment: .leading, spacing: 4) { if ci.content.showMemberName { Group { - if memCount == 1 && member.memberRole > .member { + let (prevMember, memCount): (GroupMember?, Int) = + if let range = range { + m.getPrevHiddenMember(member, range) + } else { + (nil, 1) + } + if memCount == 1 && (member.memberRole > .member || ci.meta.showGroupAsSender) { + let (name, role) = if ci.meta.showGroupAsSender { + (groupInfo.chatViewName, NSLocalizedString("group", comment: "shown on group welcome message")) + } else { + (member.chatViewName, member.memberRole.text) + } Group { if #available(iOS 16.0, *) { MemberLayout(spacing: 16, msgWidth: msgWidth) { - Text(member.chatViewName) + Text(name) .lineLimit(1) - Text(member.memberRole.text) + Text(role) .fontWeight(.semibold) .lineLimit(1) .padding(.trailing, 8) } } else { HStack(spacing: 16) { - Text(member.chatViewName) + Text(name) .lineLimit(1) - Text(member.memberRole.text) + Text(role) .fontWeight(.semibold) .lineLimit(1) .layoutPriority(1) @@ -1128,17 +1817,24 @@ struct ChatView: View { .padding(.trailing, 12) } HStack(alignment: .top, spacing: 10) { - MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background) - .onTapGesture { - 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 - } - } + if ci.meta.showGroupAsSender { + ProfileImage(imageStr: groupInfo.image, iconName: groupInfo.chatIconName, size: memberImageSize, backgroundColor: theme.colors.background) + .simultaneousGesture(TapGesture().onEnded { + showChatInfoSheet = true + }) + } else { + 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 } } @@ -1188,20 +1884,31 @@ struct ChatView: View { } } - @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View { + func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading - VStack(alignment: alignment.horizontal, spacing: 3) { - ChatItemView( - chat: chat, - chatItem: ci, - 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("") + return VStack(alignment: alignment.horizontal, spacing: 3) { + HStack { + if ci.chatDir.sent { + goToItemButton(true) + } + ChatItemView( + chat: chat, + im: im, + chatItem: ci, + scrollToItem: scrollToItem, + 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) @@ -1222,12 +1929,28 @@ struct ChatView: View { 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, 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, reports.sorted(), true) + self.archivingReports = [] + } + } + } + } .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) .sheet(isPresented: $showChatItemInfoSheet, onDismiss: { chatItemInfo = nil }) { - ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo) + ChatItemInfoView(ci: ci, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, chatItemInfo: $chatItemInfo) } } @@ -1257,12 +1980,12 @@ struct ChatView: View { .padding(.horizontal, 6) .padding(.vertical, 4) .if(chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted)) { v in - v.onTapGesture { + v.simultaneousGesture(TapGesture().onEnded { setReaction(ci, add: !r.userReacted, reaction: r.reaction) - } + }) } switch chat.chatInfo { - case let .group(groupInfo): + case let .group(groupInfo, _): v.contextMenu { ReactionContextMenu( groupInfo: groupInfo, @@ -1285,7 +2008,7 @@ struct ChatView: View { @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 case let .group(gInfo, _) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil { if ci.chatDir != .groupSnd, gInfo.membership.memberRole >= .moderator { archiveReportButton(ci) } @@ -1343,9 +2066,13 @@ struct ChatView: View { if ci.chatDir != .groupSnd { if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { moderateButton(ci, groupInfo) - } // else if ci.meta.itemDeleted == nil, case let .group(gInfo) = chat.chatInfo, gInfo.membership.memberRole == .member, !live, composeState.voiceMessageRecordingState == .noRecording { - // reportButton(ci) - // } + } 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 { @@ -1451,6 +2178,7 @@ struct ChatView: View { let chatItem = try await apiChatItemReaction( type: cInfo.chatType, id: cInfo.apiId, + scope: cInfo.groupChatScope(), itemId: ci.id, add: add, reaction: reaction @@ -1510,7 +2238,7 @@ struct ChatView: View { } label: { Label( NSLocalizedString("Save", comment: "chat item action"), - systemImage: file.cryptoArgs == nil ? "square.and.arrow.down" : "lock.open" + systemImage: "square.and.arrow.down" ) } } @@ -1564,11 +2292,11 @@ struct ChatView: View { Task { do { let cInfo = chat.chatInfo - let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, itemId: ci.id) + let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemId: ci.id) await MainActor.run { chatItemInfo = ciInfo } - if case let .group(gInfo) = chat.chatInfo { + if case let .group(gInfo, _) = chat.chatInfo { await m.loadGroupMembers(gInfo) } } catch let error { @@ -1609,7 +2337,7 @@ struct ChatView: View { private func hideButton() -> Button { Button { withConditionalAnimation { - revealedChatItem = nil + reveal(false) } } label: { Label( @@ -1622,13 +2350,13 @@ struct ChatView: View { private func deleteButton(_ ci: ChatItem, label: LocalizedStringKey = "Delete") -> Button { Button(role: .destructive) { if !revealed, - let currIndex = m.getChatItemIndex(ci), + let currIndex = m.getChatItemIndex(im, ci), let ciCategory = ci.mergeCategory { let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory) if let range = itemsRange(currIndex, prevHidden) { var itemIds: [Int64] = [] for i in range { - itemIds.append(ItemsModel.shared.reversedChatItems[i].id) + itemIds.append(im.reversedChatItems[i].id) } showDeleteMessages = true deletingItems = itemIds @@ -1677,20 +2405,11 @@ struct ChatView: View { ) } } - + private func archiveReportButton(_ cItem: ChatItem) -> Button { - Button(role: .destructive) { - AlertManager.shared.showAlert( - Alert( - title: Text("Archive report?"), - message: Text("The report will be archived for you."), - primaryButton: .destructive(Text("Archive")) { - deletingItem = cItem - deleteMessage(.cidmInternalMark, moderate: false) - }, - secondaryButton: .cancel() - ) - ) + Button { + archivingReports = [cItem.id] + showArchivingReports = true } label: { Label("Archive report", systemImage: "archivebox") } @@ -1699,7 +2418,7 @@ struct ChatView: View { private func revealButton(_ ci: ChatItem) -> Button { Button { withConditionalAnimation { - revealedChatItem = ci + reveal(true) } } label: { Label( @@ -1712,7 +2431,7 @@ struct ChatView: View { private func expandButton() -> Button { Button { withConditionalAnimation { - revealedChatItem = chatItem + reveal(true) } } label: { Label( @@ -1725,7 +2444,7 @@ struct ChatView: View { private func shrinkButton() -> Button { Button { withConditionalAnimation { - revealedChatItem = nil + reveal(false) } } label: { Label ( @@ -1734,7 +2453,7 @@ struct ChatView: View { ) } } - + private func reportButton(_ ci: ChatItem) -> Button { Button(role: .destructive) { var buttons: [ActionSheet.Button] = ReportReason.supportedReasons.map { reason in @@ -1748,9 +2467,9 @@ struct ChatView: View { } } } - + buttons.append(.cancel()) - + actionSheet = SomeActionSheet( actionSheet: ActionSheet( title: Text("Report reason?"), @@ -1765,7 +2484,7 @@ struct ChatView: View { ) } } - + var deleteMessagesTitle: LocalizedStringKey { let n = deletingItems.count return n == 1 ? "Delete message?" : "Delete \(n) messages?" @@ -1775,12 +2494,12 @@ struct ChatView: View { selectedChatItems = selectedChatItems ?? [] var itemIds: [Int64] = [] if !revealed, - let currIndex = m.getChatItemIndex(ci), + let currIndex = m.getChatItemIndex(im, 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) + itemIds.append(im.reversedChatItems[i].id) } } else { itemIds.append(ci.id) @@ -1814,6 +2533,7 @@ struct ChatView: View { try await apiDeleteChatItems( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), itemIds: [di.id], mode: mode ) @@ -1830,6 +2550,7 @@ struct ChatView: View { if deletedItem.isActiveReport { m.decreaseGroupReportsCounter(chat.chatInfo.id) } + m.updateChatInfo(itemDeletion.deletedChatItem.chatInfo) } } } @@ -1838,7 +2559,7 @@ struct ChatView: View { } } } - + @ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View { if !r.userReacted || r.totalReacted > 1 { Button { showChatInfoSheet = true } label: { @@ -1853,6 +2574,34 @@ struct ChatView: View { } } + 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 { + im.loadOpenChatNoWait(chat.id, chatItem.id) + } + } + } else if let chatTypeApiIdMsgId { + goToItemInnerButton(alignStart, "arrow.right", touchInProgress: touchInProgress) { + closeKeyboardAndRun { + let (chatType, apiId, msgId) = chatTypeApiIdMsgId + im.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId) + } + } + } + } + private struct SelectedChatItem: View { @EnvironmentObject var theme: AppTheme var ciId: Int64 @@ -1874,6 +2623,84 @@ struct ChatView: View { } } +class FloatingButtonModel: ObservableObject { + @ObservedObject var im: ItemsModel + + public init(im: ItemsModel) { + self.im = im + } + + @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, im.chatState.unreadTotal - lastVisibleItem.unreadBefore) + } else { + 0 + } + let unreadAbove = im.chatState.unreadTotal - unreadBelow + let date: Date? = + if let lastVisible = listState.visibleItems.last { + Calendar.current.startOfDay(for: lastVisible.item.oldest().item.meta.itemTs) + } else { + nil + } + + // 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 func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey { chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" } @@ -1895,6 +2722,7 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe try await apiDeleteChatItems( type: chatInfo.chatType, id: chatInfo.apiId, + scope: chatInfo.groupChatScope(), itemIds: itemIds, mode: mode ) @@ -1903,15 +2731,18 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe await MainActor.run { for di in deletedItems { if let toItem = di.toChatItem { - _ = ChatModel.shared.upsertChatItem(chat.chatInfo, toItem.chatItem) + _ = 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(chat.chatInfo.id) + ChatModel.shared.decreaseGroupReportsCounter(chatInfo.id) } } + if let updatedChatInfo = deletedItems.last?.deletedChatItem.chatInfo { + ChatModel.shared.updateChatInfo(updatedChatInfo) + } } await onSuccess() } catch { @@ -1921,11 +2752,46 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe } } +func archiveReports(_ chat: Chat, _ itemIds: [Int64], _ forAll: Bool, _ onSuccess: @escaping () async -> Void = {}) { + if itemIds.count > 0 { + let chatInfo = chat.chatInfo + 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) + } + } + if let updatedChatInfo = deleted.last?.deletedChatItem.chatInfo { + ChatModel.shared.updateChatInfo(updatedChatInfo) + } + } + 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 { + } else if case let .group(groupInfo, _) = chat.chatInfo { groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight) } else { nil as ThemeModeOverride? @@ -1961,7 +2827,7 @@ struct ReactionContextMenu: View { @ViewBuilder private func groupMemberReactionList() -> some View { if memberReactions.isEmpty { ForEach(Array(repeating: 0, count: reactionCount.totalReacted), id: \.self) { _ in - Text(verbatim: " ") + textSpace } } else { ForEach(memberReactions, id: \.groupMember.groupMemberId) { mr in @@ -2044,21 +2910,19 @@ 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) } @@ -2080,7 +2944,7 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) { case var .direct(contact): contact.chatSettings = chatSettings ChatModel.shared.updateContact(contact) - case var .group(groupInfo): + case var .group(groupInfo, _): groupInfo.chatSettings = chatSettings ChatModel.shared.updateGroup(groupInfo) default: () @@ -2097,7 +2961,8 @@ struct ChatView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.chatId = "@1" - ItemsModel.shared.reversedChatItems = [ + let im = ItemsModel.shared + im.reversedChatItems = [ ChatItem.getSample(1, .directSnd, .now, "hello"), ChatItem.getSample(2, .directRcv, .now, "hi"), ChatItem.getSample(3, .directRcv, .now, "hi there"), @@ -2109,7 +2974,13 @@ struct ChatView_Previews: PreviewProvider { ChatItem.getSample(9, .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.") ] @State var showChatInfo = false - return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) - .environmentObject(chatModel) + return ChatView( + chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), + im: im, + mergedItems: BoxedValue(MergedItems.create(im, [])), + floatingButtonModel: FloatingButtonModel(im: im), + scrollToItemId: Binding.constant(nil) + ) + .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/Chat/CommandsMenuView.swift b/apps/ios/Shared/Views/Chat/CommandsMenuView.swift new file mode 100644 index 0000000000..525bf5725c --- /dev/null +++ b/apps/ios/Shared/Views/Chat/CommandsMenuView.swift @@ -0,0 +1,187 @@ +// +// CommandsMenuView.swift +// SimpleX (iOS) +// +// Created by EP on 03/08/2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +let COMMAND_ROW_SIZE: CGFloat = 48 +let MAX_VISIBLE_COMMAND_ROWS: CGFloat = 5.8 + +struct CommandsMenuView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @ObservedObject var chat: Chat + @Binding var composeState: ComposeState + @Binding var selectedRange: NSRange + @Binding var showCommandsMenu: Bool + + @State private var currentCommands: [ChatBotCommand] = [] + @State private var menuTreeBackPath: [(label: String, commands: [ChatBotCommand])] = [] + @State private var keywordWidth: CGFloat = 0 + + var body: some View { + ZStack(alignment: .bottom) { + if !currentCommands.isEmpty { + Color.white.opacity(0.01) + .edgesIgnoringSafeArea(.all) + .onTapGesture { + showCommandsMenu = false + currentCommands = [] + menuTreeBackPath = [] + } + VStack(spacing: 0) { + Spacer() + let cmdsCount = currentCommands.count + (menuTreeBackPath.isEmpty ? 0 : 1) + let scroll = ScrollView { + VStack(spacing: 0) { + if let prev = menuTreeBackPath.last { + Divider() + menuLabelRow(prev) + } + ForEach(currentCommands, id: \.self, content: commandRow) + } + } + .frame(maxWidth: .infinity, maxHeight: COMMAND_ROW_SIZE * min(MAX_VISIBLE_COMMAND_ROWS, CGFloat(cmdsCount))) + .background(theme.colors.background) + + if #available(iOS 16.0, *) { + scroll.scrollDismissesKeyboard(.never) + } else { + scroll + } + } + .onPreferenceChange(DetermineWidth.Key.self) { keywordWidth = $0 } + } + } + .onChange(of: composeState.message) { message in + let msg = message.trimmingCharacters(in: .whitespaces) + if msg == "/" { + currentCommands = chat.chatInfo.menuCommands + } else if msg.first == "/" { + currentCommands = filterShownCommands(chat.chatInfo.menuCommands, msg.dropFirst()) + } else { + showCommandsMenu = false + currentCommands = [] + } + menuTreeBackPath = [] + } + .onChange(of: showCommandsMenu) { show in + currentCommands = show ? chat.chatInfo.menuCommands : [] + menuTreeBackPath = [] + } + } + + private func menuLabelRow(_ prev: (label: String, commands: [ChatBotCommand])) -> some View { + HStack { + Image(systemName: "chevron.left") + .foregroundColor(theme.colors.secondary) + Text(prev.label) + .fontWeight(.medium) + .frame(maxWidth: .infinity) + } + .padding(.horizontal) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + if !menuTreeBackPath.isEmpty { + currentCommands = menuTreeBackPath.removeLast().commands + } + } + } + + @ViewBuilder + private func commandRow(_ command: ChatBotCommand) -> some View { + Divider() + switch command { + case let .command(keyword, label, params): + HStack { + Text(label) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + Text("/" + keyword) + .font(.subheadline) + .lineLimit(1) + .foregroundColor(theme.colors.secondary) + .frame(minWidth: keywordWidth, alignment: .trailing) + .overlay(DetermineWidth()) + } + .padding(.horizontal) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + if let params { + composeState.message = "/\(keyword) \(params)" + selectedRange = NSRange(location: composeState.message.count, length: 0) + } else { + composeState.message = "" + sendCommandMsg(chat, "/\(keyword)") + } + showCommandsMenu = false + currentCommands = [] + menuTreeBackPath = [] + } + case let .menu(label, cmds): + HStack { + Text(label) + .fontWeight(.medium) + .lineLimit(1) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(theme.colors.secondary) + } + .padding(.horizontal) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + menuTreeBackPath.append((label: label, commands: currentCommands)) + currentCommands = cmds + } + } + } + + private func filterShownCommands(_ commands: [ChatBotCommand], _ msg: String.SubSequence) -> [ChatBotCommand] { + var cmds: [ChatBotCommand] = [] + for command in commands { + switch command { + case let .command(keyword, _, _): + if keyword.starts(with: msg) { + cmds.append(command) + } + case let .menu(_, innerCmds): + cmds.append(contentsOf: filterShownCommands(innerCmds, msg)) + } + } + return cmds + } +} + +func sendCommandMsg(_ chat: Chat, _ cmd: String) { + if chat.chatInfo.sndReady { + Task { + if let chatItems = await apiSendMessages( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), + composedMessages: [ComposedMessage(msgContent: .text(cmd))] + ) { + await MainActor.run { + for ci in chatItems { + ChatModel.shared.addChatItem(chat.chatInfo, ci) + } + } + } + } + } else { + showAlert( + NSLocalizedString("You can't send messages!", comment: "alert title"), + message: NSLocalizedString("To send commands you must be connected.", comment: "alert message"), + actions: { [okAlertAction] } + ) + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index 6c44aeea83..878ebd9cbf 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -18,7 +18,7 @@ struct ComposeLinkView: View { var body: some View { HStack(alignment: .center, spacing: 8) { - if let linkPreview = linkPreview { + if let linkPreview { linkPreviewView(linkPreview) } else { ProgressView() @@ -49,7 +49,7 @@ struct ComposeLinkView: View { VStack(alignment: .center, spacing: 4) { Text(linkPreview.title) .lineLimit(1) - Text(linkPreview.uri.absoluteString) + Text(linkPreview.uri) .font(.caption) .lineLimit(1) .foregroundColor(theme.colors.secondary) @@ -63,7 +63,7 @@ struct ComposeLinkView: View { struct SmallLinkPreview_Previews: PreviewProvider { static var previews: some View { let preview = LinkPreview( - uri: URL(string: "http://DuckDuckGo.com")!, + uri: "http://DuckDuckGo.com", title: "Privacy, simplified.", description: "", image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z" diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index a68a4987a1..3745d0f0b8 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -1,16 +1,11 @@ -// -// ComposeView.swift -// SimpleX -// -// Created by Evgeny on 13/03/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// import SwiftUI import SimpleXChat import SwiftyGif import PhotosUI +let MAX_NUMBER_OF_MENTIONS = 3 + enum ComposePreview { case noPreview case linkPreview(linkPreview: LinkPreview?) @@ -19,7 +14,7 @@ enum ComposePreview { case filePreview(fileName: String, file: URL) } -enum ComposeContextItem { +enum ComposeContextItem: Equatable { case noContextItem case quotedItem(chatItem: ChatItem) case editingItem(chatItem: ChatItem) @@ -39,31 +34,42 @@ 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 progressByTimeout = false + var useLinkPreviews = true + 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, @@ -72,10 +78,12 @@ struct ComposeState { } else { self.voiceMessageRecordingState = .noRecording } + self.mentions = editingItem.mentions ?? [:] } init(forwardingItems: [ChatItem], fromChatInfo: ChatInfo) { self.message = "" + self.parsedMessage = [] self.preview = .noPreview self.contextItem = .forwardingItems(chatItems: forwardingItems, fromChatInfo: fromChatInfo) self.voiceMessageRecordingState = .noRecording @@ -83,20 +91,38 @@ struct ComposeState { 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 @@ -117,14 +143,14 @@ struct ComposeState { default: return false } } - + var reporting: Bool { switch contextItem { case .reportedItem: return true default: return false } } - + var submittingValidReport: Bool { switch contextItem { case let .reportedItem(_, reason): @@ -135,13 +161,13 @@ struct ComposeState { 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 || submittingValidReport + default: return !whitespaceOnly || forwarding || liveMessage != nil || submittingValidReport } } @@ -222,7 +248,11 @@ struct ComposeState { } var empty: Bool { - message == "" && noPreview + whitespaceOnly && noPreview + } + + var whitespaceOnly: Bool { + message.allSatisfy { $0.isWhitespace } } } @@ -291,13 +321,18 @@ struct ComposeView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel @Binding var composeState: ComposeState + @Binding var showCommandsMenu: Bool @Binding var keyboardVisible: Bool + @Binding var keyboardHiddenDate: Date + @Binding var selectedRange: NSRange + var disabledText: LocalizedStringKey? = nil - @State var linkUrl: URL? = nil + @State var linkUrl: String? = nil @State var hasSimplexLink: Bool = false - @State var prevLinkUrl: URL? = nil - @State var pendingLinkUrl: URL? = nil + @State var prevLinkUrl: String? = nil + @State var pendingLinkUrl: String? = nil @State var cancelledLinks: Set = [] @Environment(\.colorScheme) private var colorScheme @@ -317,23 +352,46 @@ struct ComposeView: View { @UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false + @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false + @State private var updatingCompose = false var body: some View { VStack(spacing: 0) { Divider() - if chat.chatInfo.contact?.nextSendGrpInv ?? false { - ContextInvitingContactMemberView() + + if chat.chatInfo.nextConnectPrepared, + let user = chatModel.currentUser { + ContextProfilePickerView( + chat: chat, + selectedUser: user + ) Divider() } - + + if let groupInfo = chat.chatInfo.groupInfo, + case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter, + case let .memberSupport(member) = groupScopeInfo, + let member = member, + member.memberPending, + composeState.contextItem == .noContextItem, + composeState.noPreview { + ContextPendingMemberActionsView( + groupInfo: groupInfo, + member: member + ) + 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 simplexLinkProhibited = im.secondaryIMFilter == nil && hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) + let fileProhibited = im.secondaryIMFilter == nil && composeState.attachmentPreview && !chat.groupFeatureEnabled(.files) let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice) + let disableSendButton = simplexLinkProhibited || fileProhibited || voiceProhibited if simplexLinkProhibited { msgNotAllowedView("SimpleX links not allowed", icon: "link") Divider() @@ -350,76 +408,46 @@ struct ComposeView: View { case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed default: previewView() } - HStack (alignment: .bottom) { - let b = Button { - showChooseSource = true - } label: { - Image(systemName: "paperclip") - .resizable() - } - .disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false)) - .frame(width: 25, height: 25) - .padding(.bottom, 12) - .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 { - AlertManager.shared.showAlertMsg( - title: "Files and media prohibited!", - message: "Only group owners can enable files and media." - ) - } - } else { - b - } - ZStack(alignment: .leading) { - SendMessageView( - composeState: $composeState, - sendMessage: { ttl in - sendMessage(ttl: ttl) - resetLinkPreview() - }, - sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil, - updateLiveMessage: updateLiveMessage, - cancelLiveMessage: { - composeState.liveMessage = nil - chatModel.removeLiveDummy() - }, - nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false, - voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice), - disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited, - showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert, - startVoiceMessageRecording: { - Task { - await startVoiceMessageRecording() - } - }, - finishVoiceMessageRecording: finishVoiceMessageRecording, - allowVoiceMessagesToContact: allowVoiceMessagesToContact, - timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages), - onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }}, - keyboardVisible: $keyboardVisible, - sendButtonColor: chat.chatInfo.incognito - ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) - : theme.colors.primary - ) - .padding(.trailing, 12) - .disabled(!chat.userCanSend) - if chat.userIsObserver { - Text("you are observer") - .italic() - .foregroundColor(theme.colors.secondary) - .padding(.horizontal, 12) - .onTapGesture { - AlertManager.shared.showAlertMsg( - title: "You can't send messages!", - message: "Please contact group admin." - ) - } + let contact = chat.chatInfo.contact + + if chat.chatInfo.groupInfo?.nextConnectPrepared == true { + if chat.chatInfo.groupInfo?.businessChat == nil { + connectButtonView("Join group", icon: "person.2.fill", connect: connectPreparedGroup) + } else { + sendContactRequestView(disableSendButton, icon: "briefcase.fill", sendRequest: connectPreparedGroup) + } + } else if contact?.nextSendGrpInv == true { + contextSendMessageToConnect("Send direct message to connect") + Divider() + HStack (alignment: .center) { + attachmentAndCommandsButtons().disabled(true) + sendMessageView(disableSendButton, sendToConnect: sendMemberContactInvitation) + } + .padding(.horizontal, 12) + } else if let contact, + contact.nextConnectPrepared == true, + let linkType = contact.preparedContact?.uiConnLinkType { + switch linkType { + case .inv: + connectButtonView("Connect", icon: "person.fill.badge.plus", connect: sendConnectPreparedContact) + case .con: + if contact.isBot { + connectButtonView("Connect", icon: "bolt.fill", connect: sendConnectPreparedContact) + } else { + sendContactRequestView(disableSendButton, icon: "person.fill.badge.plus", sendRequest: sendConnectPreparedContactRequest) } } + } else if contact?.nextAcceptContactRequest == true, let crId = contact?.contactRequestId { + ContextContactRequestActionsView(contactRequestId: crId) + } else if let ct = contact, ct.nextAcceptContactRequest, let groupDirectInv = ct.groupDirectInv { + ContextMemberContactActionsView(contact: ct, groupDirectInv: groupDirectInv) + } else { + HStack (alignment: .center) { + attachmentAndCommandsButtons() + sendMessageView(disableSendButton) + } + .padding(.horizontal, 12) } } .background { @@ -428,26 +456,58 @@ struct ComposeView: View { .ignoresSafeArea(.all, edges: .bottom) } .onChange(of: composeState.message) { msg in - if composeState.linkPreviewAllowed { - if msg.count > 0 { - showLinkPreview(msg) + if updatingCompose { + updatingCompose = false + return + } + var parsedMsg = parseSimpleXMarkdown(msg) + if privacySanitizeLinks, let parsed = parsedMsg { + let r = sanitizeMessage(parsed) + if let sanitizedPos = r.sanitizedPos { + updatingCompose = true + composeState = composeState.copy(message: r.message, parsedMessage: r.parsedMsg) + if sanitizedPos < selectedRange.location { + selectedRange = NSRange(location: sanitizedPos, length: 0) + } + parsedMsg = r.parsedMsg + } else { + composeState = composeState.copy(parsedMessage: parsedMsg) + } + } else { + composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg)) + } + if composeState.linkPreviewAllowed && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) { + if !msg.isEmpty { + showLinkPreview(parsedMsg) } else { resetLinkPreview() hasSimplexLink = false + composeState = composeState.copy(preview: .noPreview) } - } else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) { - (_, hasSimplexLink) = parseMessage(msg) } else { - hasSimplexLink = false + resetLinkPreview() + hasSimplexLink = !msg.isEmpty && !chat.groupFeatureEnabled(.simplexLinks) && getMessageLinks(parsedMsg).hasSimplexLink + if composeState.linkPreviewAllowed { + composeState = composeState.copy(preview: .noPreview) + } } } - .onChange(of: chat.userCanSend) { canSend in - if !canSend { + .onChange(of: chat.chatInfo.sendMsgEnabled) { sendEnabled in + if !sendEnabled { cancelCurrentVoiceRecording() clearCurrentDraft() clearState() } } + .onChange(of: composeState.inProgress) { inProgress in + if inProgress { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + composeState.progressByTimeout = composeState.inProgress + } + } else { + composeState.progressByTimeout = false + } + } .confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true @@ -583,6 +643,241 @@ struct ComposeView: View { } } + private func connectButtonView(_ label: LocalizedStringKey, icon: String, connect: @escaping () -> Void) -> some View { + Button(action: connect) { + ZStack(alignment: .trailing) { + Label(label, systemImage: icon) + .frame(maxWidth: .infinity) + if composeState.progressByTimeout { + ProgressView() + .padding() + } + } + } + .frame(height: 60) + .disabled(composeState.inProgress) + } + + private func sendContactRequestView(_ disableSendButton: Bool, icon: String, sendRequest: @escaping () -> Void) -> some View { + HStack (alignment: .center) { + sendMessageView( + disableSendButton, + placeholder: NSLocalizedString("Add message", comment: "placeholder for sending contact request"), + sendToConnect: sendRequest + ) + if composeState.whitespaceOnly { + Button(action: sendRequest) { + HStack { + Text("Connect").fontWeight(.medium) + Image(systemName: icon) + } + } + .padding(.horizontal, 8) + .disabled(composeState.inProgress) + } + } + .padding(.horizontal, 12) + } + + private func sendMessageView(_ disableSendButton: Bool, placeholder: String? = nil, sendToConnect: (() -> Void)? = nil) -> some View { + ZStack(alignment: .leading) { + SendMessageView( + placeholder: placeholder, + composeState: $composeState, + selectedRange: $selectedRange, + sendMessage: { ttl in + sendMessage(ttl: ttl) + resetLinkPreview() + }, + sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil, + updateLiveMessage: updateLiveMessage, + cancelLiveMessage: { + composeState.liveMessage = nil + chatModel.removeLiveDummy() + }, + sendToConnect: sendToConnect, + hideSendButton: chat.chatInfo.nextConnect && chat.chatInfo.contact?.nextSendGrpInv != true && composeState.whitespaceOnly, + voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice), + disableSendButton: disableSendButton, + showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert, + startVoiceMessageRecording: { + Task { + await startVoiceMessageRecording() + } + }, + finishVoiceMessageRecording: finishVoiceMessageRecording, + allowVoiceMessagesToContact: allowVoiceMessagesToContact, + 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) + : theme.colors.primary + ) + .disabled(!chat.chatInfo.sendMsgEnabled) + + if let disabledText { + Text(disabledText) + .italic() + .foregroundColor(theme.colors.secondary) + .padding(.horizontal, 12) + } + } + } + + @ViewBuilder private func attachmentAndCommandsButtons() -> some View { + let msg = composeState.message.trimmingCharacters(in: .whitespaces) + let showAttachment = chat.chatInfo.contact?.profile.peerType != .bot || chat.chatInfo.featureEnabled(.files) + let showCommands = chat.chatInfo.useCommands && (!showAttachment || msg.isEmpty || msg.starts(with: "/")) + if showCommands { + commandsButton() + } + if showAttachment { + attachmentButton() + .padding(.trailing, 3) + .if(showCommands) { v in v.padding(.leading, 3) } + } + } + + private func commandsButton() -> some View { + Button { + showCommandsMenu.toggle() + } label: { + Text(verbatim: "//") + .font(.title3) + .italic() + .contentShape(Rectangle()) + } + .disabled(!chat.chatInfo.sendMsgEnabled || chat.chatInfo.menuCommands.isEmpty) + .frame(width: 25, height: 25) + .tint(theme.colors.primary) + .padding(.bottom, 2) + } + + @ViewBuilder private func attachmentButton() -> some View { + let b = Button { + showChooseSource = true + } label: { + Image(systemName: "paperclip") + .resizable() + } + .disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled) + .frame(width: 25, height: 25) + .tint(theme.colors.primary) + if im.secondaryIMFilter == nil, + !chat.chatInfo.featureEnabled(.files) { + b.disabled(true).onTapGesture { + AlertManager.shared.showAlertMsg( + title: "Files and media prohibited!", + message: chat.chatInfo.groupInfo == nil ? nil : "Only group owners can enable files and media." + ) + } + } else { + b + } + } + + private func sendMemberContactInvitation() { + Task { + do { + await MainActor.run { hideKeyboard() } + if let mc = connectCheckLinkPreview() { + await sending() + let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc) + await MainActor.run { + self.chatModel.updateContact(contact) + clearState() + } + } else { + AlertManager.shared.showAlertMsg(title: "Empty message!") + } + } catch { + await MainActor.run { composeState.inProgress = false } + logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)") + AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))") + } + } + } + + private func sendConnectPreparedContactRequest() { + hideKeyboard() + let empty = composeState.whitespaceOnly + AlertManager.shared.showAlert(Alert( + title: Text("Send contact request?"), + message: Text("You will be able to send messages **only after your request is accepted**."), + primaryButton: .default( + Text(empty ? "Send request without message" : "Send request"), + action: sendConnectPreparedContact + ), + secondaryButton: + empty + ? .cancel(Text("Add message"), action: hideKeyboard) + : .cancel() + )) + } + + private func sendConnectPreparedContact() { + Task { + await MainActor.run { hideKeyboard() } + await sending() + let mc = connectCheckLinkPreview() + let incognito = chat.chatInfo.profileChangeProhibited ? chat.chatInfo.incognito : incognitoDefault + if let contact = await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognito, msg: mc) { + await MainActor.run { + self.chatModel.updateContact(contact) + clearState() + } + } else { + await MainActor.run { composeState.inProgress = false } + } + } + } + + private func connectPreparedGroup() { + Task { + await MainActor.run { hideKeyboard() } + await sending() + let mc = connectCheckLinkPreview() + let incognito = chat.chatInfo.profileChangeProhibited ? chat.chatInfo.incognito : incognitoDefault + if let groupInfo = await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: incognito, msg: mc) { + await MainActor.run { + self.chatModel.updateGroup(groupInfo) + clearState() + } + } else { + await MainActor.run { composeState.inProgress = false } + } + } + } + + @inline(__always) + private func connectCheckLinkPreview() -> MsgContent? { + let msgText = composeState.message.trimmingCharacters(in: .whitespacesAndNewlines) + return msgText.isEmpty ? nil : checkLinkPreview_(msgText) + } + + @inline(__always) + private func checkLinkPreview() -> MsgContent { + checkLinkPreview_(composeState.message.trimmingCharacters(in: .whitespacesAndNewlines)) + } + + private func checkLinkPreview_(_ msgText: String) -> MsgContent { + switch (composeState.preview) { + case let .linkPreview(linkPreview: linkPreview): + if let parsedMsg = parseSimpleXMarkdown(msgText), + let url = getMessageLinks(parsedMsg).url, + let linkPreview = linkPreview, + url == linkPreview.uri { + return .link(text: msgText, preview: linkPreview) + } else { + return .text(msgText) + } + default: + return .text(msgText) + } + } + private func addMediaContent(_ content: UploadContent) async { if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) { var newMedia: [(String, UploadContent?)] = [] @@ -719,8 +1014,19 @@ struct ComposeView: View { .frame(maxWidth: .infinity, alignment: .leading) .background(.thinMaterial) } - - + + private func contextSendMessageToConnect(_ s: LocalizedStringKey) -> some View { + HStack { + Image(systemName: "message") + .foregroundColor(theme.colors.secondary) + Text(s) + } + .padding(12) + .frame(minHeight: 54) + .frame(maxWidth: .infinity, alignment: .leading) + .background(ToolbarMaterial.material(toolbarMaterial)) + } + 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") @@ -793,17 +1099,16 @@ struct ComposeView: View { var sent: ChatItem? let msgText = text ?? composeState.message let liveMessage = composeState.liveMessage + let mentions = composeState.memberMentions if !live { if liveMessage != nil { composeState = composeState.copy(liveMessage: nil) } await sending() } - if chat.chatInfo.contact?.nextSendGrpInv ?? false { - await sendMemberContactInvitation() - } else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem { + if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem { // Composed text is send as a reply to the last forwarded item sent = await forwardItems(chatItems, fromChatInfo, ttl).last if !composeState.message.isEmpty { - _ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl) + _ = 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) @@ -819,10 +1124,11 @@ 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) + 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 { @@ -847,10 +1153,10 @@ struct ComposeView: View { 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) } } } @@ -878,23 +1184,6 @@ struct ComposeView: View { nil } } - - func sending() async { - await MainActor.run { composeState.inProgress = true } - } - - func sendMemberContactInvitation() async { - do { - let mc = checkLinkPreview() - let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc) - await MainActor.run { - self.chatModel.updateContact(contact) - } - } catch { - logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)") - AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))") - } - } func updateMessage(_ ei: ChatItem, live: Bool) async -> ChatItem? { if let oldMsgContent = ei.content.msgContent { @@ -904,8 +1193,9 @@ struct ComposeView: View { let chatItem = try await apiUpdateChatItem( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), itemId: ei.id, - msg: mc, + updatedMessage: UpdatedMessage(msgContent: mc, mentions: composeState.memberMentions), live: live ) await MainActor.run { @@ -939,6 +1229,9 @@ struct ComposeView: View { return .file(msgText) case .report(_, let reason): return .report(text: msgText, reason: reason) + // TODO [short links] update chat link + case let .chat(_, chatLink): + return .chat(text: msgText, chatLink: chatLink) case .unknown(let type, _): return .unknown(type: type, text: msgText) } @@ -958,7 +1251,7 @@ struct ComposeView: View { return nil } } - + func send(_ reportReason: ReportReason, chatItemId: Int64) async -> ChatItem? { if let chatItems = await apiReportMessage( groupId: chat.chatInfo.apiId, @@ -966,20 +1259,40 @@ struct ComposeView: View { reportReason: reportReason, reportText: msgText ) { - await MainActor.run { - for chatItem in chatItems { - chatModel.addChatItem(chat.chatInfo, chatItem) + if showReportsInSupportChatAlertDefault.get() { + await MainActor.run { + showReportsInSupportChatAlert() } } return chatItems.first } - + return nil } - - func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { + + func showReportsInSupportChatAlert() { + showAlert( + NSLocalizedString("Report sent to moderators", comment: "alert title"), + message: NSLocalizedString("You can view your reports in Chat with admins.", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Don't show again", comment: "alert action"), + style: .default, + handler: { _ in + showReportsInSupportChatAlertDefault.set(false) + } + ), + UIAlertAction( + title: NSLocalizedString("Ok", comment: "alert action"), + style: .default + ) + ]} + ) + } + + 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)], + [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc, mentions: mentions)], live: live, ttl: ttl ).first @@ -991,6 +1304,7 @@ struct ComposeView: View { : await apiSendMessages( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), live: live, ttl: ttl, composedMessages: msgs @@ -1015,8 +1329,10 @@ struct ComposeView: View { if let chatItems = await apiForwardChatItems( toChatType: chat.chatInfo.chatType, toChatId: chat.chatInfo.apiId, + toScope: chat.chatInfo.groupChatScope(), fromChatType: fromChatInfo.chatType, fromChatId: fromChatInfo.apiId, + fromScope: fromChatInfo.groupChatScope(), itemIds: forwardedItems.map { $0.id }, ttl: ttl ) { @@ -1039,21 +1355,10 @@ struct ComposeView: View { return [] } } + } - func checkLinkPreview() -> MsgContent { - switch (composeState.preview) { - case let .linkPreview(linkPreview: linkPreview): - if let url = parseMessage(msgText).url, - let linkPreview = linkPreview, - url == linkPreview.uri { - return .link(text: msgText, preview: linkPreview) - } else { - return .text(msgText) - } - default: - return .text(msgText) - } - } + func sending() async { + await MainActor.run { composeState.inProgress = true } } private func startVoiceMessageRecording() async { @@ -1162,9 +1467,9 @@ struct ComposeView: View { } } - private func showLinkPreview(_ s: String) { + private func showLinkPreview(_ parsedMsg: [FormattedText]?) { prevLinkUrl = linkUrl - (linkUrl, hasSimplexLink) = parseMessage(s) + (linkUrl, hasSimplexLink) = getMessageLinks(parsedMsg) if let url = linkUrl { if url != composeState.linkPreview?.uri && url != pendingLinkUrl { pendingLinkUrl = url @@ -1181,43 +1486,45 @@ struct ComposeView: View { } } - private func parseMessage(_ msg: String) -> (url: URL?, hasSimplexLink: Bool) { - guard let parsedMsg = parseSimpleXMarkdown(msg) else { return (nil, false) } - let url: URL? = if let uri = parsedMsg.first(where: { ft in - ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) - }) { - URL(string: uri.text) - } else { - nil - } + private func getMessageLinks(_ parsedMsg: [FormattedText]?) -> (url: String?, hasSimplexLink: Bool) { + guard let parsedMsg else { return (nil, false) } let simplexLink = parsedMsgHasSimplexLink(parsedMsg) - return (url, simplexLink) + for ft in parsedMsg { + if let link = ft.linkUri, !cancelledLinks.contains(link) && !isSimplexLink(link) { + return (link, simplexLink) + } + } + return (nil, simplexLink) } private func isSimplexLink(_ link: String) -> Bool { - link.starts(with: "https://simplex.chat") || link.starts(with: "http://simplex.chat") + link.starts(with: "https://simplex.chat") || link.starts(with: "http://simplex.chat") || link.starts(with: "simplex:/") } private func cancelLinkPreview() { - if let pendingLink = pendingLinkUrl?.absoluteString { + if let pendingLink = pendingLinkUrl { cancelledLinks.insert(pendingLink) } - if let uri = composeState.linkPreview?.uri.absoluteString { + if let uri = composeState.linkPreview?.uri { cancelledLinks.insert(uri) } pendingLinkUrl = nil composeState = composeState.copy(preview: .noPreview) } - private func loadLinkPreview(_ url: URL) { - if pendingLinkUrl == url { + private func loadLinkPreview(_ urlStr: String) { + if pendingLinkUrl == urlStr, let url = URL(string: urlStr) { composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) getLinkPreview(url: url) { linkPreview in - if let linkPreview = linkPreview, - pendingLinkUrl == url { + if let linkPreview, pendingLinkUrl == urlStr { + privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) - pendingLinkUrl = nil + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + composeState = composeState.copy(preview: .noPreview) + } } + pendingLinkUrl = nil } } } @@ -1230,24 +1537,31 @@ struct ComposeView: View { } } -struct ComposeView_Previews: PreviewProvider { - static var previews: some View { - let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) - @State var composeState = ComposeState(message: "hello") - - return Group { - ComposeView( - chat: chat, - composeState: $composeState, - keyboardVisible: Binding.constant(true) - ) - .environmentObject(ChatModel()) - ComposeView( - chat: chat, - composeState: $composeState, - keyboardVisible: Binding.constant(true) - ) - .environmentObject(ChatModel()) +func sanitizeMessage(_ parsedMsg: [FormattedText]) -> (message: String, parsedMsg: [FormattedText], sanitizedPos: Int?) { + var pos: Int = 0 + var updatedMsg = "" + var sanitizedPos: Int? = nil + let updatedParsedMsg = parsedMsg.map { ft in + var updated = ft + switch ft.format { + case .uri: + if let sanitized = parseSanitizeUri(ft.text, safe: true)?.uriInfo?.sanitized { + updated = FormattedText(text: sanitized, format: .uri) + pos += updated.text.count + sanitizedPos = pos + } + case let .hyperLink(text, uri): + if let sanitized = parseSanitizeUri(uri, safe: true)?.uriInfo?.sanitized { + let updatedText = if let text { "[\(text)](\(sanitized))" } else { sanitized } + updated = FormattedText(text: updatedText, format: .hyperLink(showText: text, linkUri: sanitized)) + pos += updated.text.count + sanitizedPos = pos + } + default: + pos += ft.text.count } + updatedMsg += updated.text + return updated } + return (message: updatedMsg, parsedMsg: updatedParsedMsg, sanitizedPos: sanitizedPos) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift new file mode 100644 index 0000000000..82c89cd43d --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift @@ -0,0 +1,97 @@ +// +// ContextContactRequestActionsView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 02.05.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ContextContactRequestActionsView: View { + @EnvironmentObject var theme: AppTheme + var contactRequestId: Int64 + @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + @State private var inProgress = false + @State private var progressByTimeout = false + + var body: some View { + HStack(spacing: 0) { + Button(role: .destructive, action: showRejectRequestAlert) { + Label("Reject", systemImage: "multiply") + } + .frame(maxWidth: .infinity, minHeight: 60) + + Button { + if ChatModel.shared.addressShortLinkDataSet { + acceptRequest() + } else { + showAcceptRequestAlert() + } + } label: { + Label("Accept", systemImage: "checkmark") + } + .frame(maxWidth: .infinity, minHeight: 60) + } + .disabled(inProgress) + .frame(maxWidth: .infinity) + .background(ToolbarMaterial.material(toolbarMaterial)) + .opacity(progressByTimeout ? 0.4 : 1) + .overlay { + if progressByTimeout { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .onChange(of: inProgress) { inPrgrs in + if inPrgrs { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + progressByTimeout = inProgress + } + } else { + progressByTimeout = false + } + } + } + + private func showRejectRequestAlert() { + showAlert( + NSLocalizedString("Reject contact request", comment: "alert title"), + message: NSLocalizedString("The sender will NOT be notified", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Reject", comment: "alert action"), style: .destructive) { _ in + Task { await rejectContactRequest(contactRequestId, dismissToChatList: true) } + }, + cancelAlertAction + ]} + ) + } + + private func showAcceptRequestAlert() { + showAlert( + NSLocalizedString("Accept contact request", comment: "alert title"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Accept", comment: "alert action"), style: .default) { _ in + acceptRequest() + }, + UIAlertAction(title: NSLocalizedString("Accept incognito", comment: "alert action"), style: .default) { _ in + acceptRequest(incognito: true) + }, + cancelAlertAction + ]} + ) + } + + private func acceptRequest(incognito: Bool = false) { + Task { + await acceptContactRequest(incognito: incognito, contactRequestId: contactRequestId, inProgress: $inProgress) + } + } +} + +#Preview { + ContextContactRequestActionsView( + contactRequestId: 1 + ) +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift deleted file mode 100644 index 82090f312a..0000000000 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ContextInvitingContactMemberView.swift -// SimpleX (iOS) -// -// Created by spaced4ndy on 18.09.2023. -// Copyright © 2023 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -struct ContextInvitingContactMemberView: View { - @EnvironmentObject var theme: AppTheme - - var body: some View { - HStack { - Image(systemName: "message") - .foregroundColor(theme.colors.secondary) - Text("Send direct message to connect") - } - .padding(12) - .frame(minHeight: 54) - .frame(maxWidth: .infinity, alignment: .leading) - .background(.thinMaterial) - } -} - -struct ContextInvitingContactMemberView_Previews: PreviewProvider { - static var previews: some View { - ContextInvitingContactMemberView() - } -} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 3cb747ec68..845442c75f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -70,8 +70,10 @@ struct ContextItemView: View { .lineLimit(lines) } - private func contextMsgPreview(_ contextItem: ChatItem) -> Text { - return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) + 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) { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift new file mode 100644 index 0000000000..9a73b2b5d4 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift @@ -0,0 +1,110 @@ +// +// ContextMemberContactActionsView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 31.07.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ContextMemberContactActionsView: View { + @EnvironmentObject var theme: AppTheme + var contact: Contact + var groupDirectInv: GroupDirectInvitation + @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + @State private var inProgress = false + @State private var progressByTimeout = false + + var body: some View { + VStack { + if groupDirectInv.memberRemoved { + Label("Member is deleted - can't accept request", systemImage: "info.circle") + .foregroundColor(theme.colors.secondary) + .font(.subheadline) + .padding(.horizontal) + .frame(maxWidth: .infinity, minHeight: 60) + } else { + HStack(spacing: 0) { + Button(role: .destructive, action: { showRejectMemberContactRequestAlert(contact) }) { + Label("Reject", systemImage: "multiply") + } + .frame(maxWidth: .infinity, minHeight: 60) + + Button { + acceptMemberContactRequest(contact, inProgress: $inProgress) + } label: { + Label("Accept", systemImage: "checkmark") + } + .frame(maxWidth: .infinity, minHeight: 60) + } + } + } + .disabled(inProgress || groupDirectInv.memberRemoved) + .frame(maxWidth: .infinity) + .background(ToolbarMaterial.material(toolbarMaterial)) + .opacity(progressByTimeout ? 0.4 : 1) + .overlay { + if progressByTimeout { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .onChange(of: inProgress) { inPrgrs in + if inPrgrs { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + progressByTimeout = inProgress + } + } else { + progressByTimeout = false + } + } + } +} + +func showRejectMemberContactRequestAlert(_ contact: Contact) { + showAlert( + NSLocalizedString("Reject contact request", comment: "alert title"), + message: NSLocalizedString("The sender will NOT be notified", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Reject", comment: "alert action"), style: .destructive) { _ in + deleteContact(contact) + }, + cancelAlertAction + ]} + ) +} + +private func deleteContact(_ contact: Contact) { + Task { + do { + _ = try await apiDeleteContact(id: contact.contactId, chatDeleteMode: .full(notify: false)) + await MainActor.run { + ChatModel.shared.removeChat(contact.id) + ChatModel.shared.chatId = nil + } + } catch let error { + logger.error("apiDeleteContact: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error deleting chat!", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + +func acceptMemberContactRequest(_ contact: Contact, inProgress: Binding? = nil) { + Task { + await acceptMemberContact(contactId: contact.contactId, inProgress: inProgress) + } +} + +#Preview { + ContextMemberContactActionsView( + contact: Contact.sampleData, + groupDirectInv: GroupDirectInvitation.sampleData + ) +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift new file mode 100644 index 0000000000..e9913053ea --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift @@ -0,0 +1,106 @@ +// +// ContextPendingMemberActionsView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 02.05.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ContextPendingMemberActionsView: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.dismiss) var dismiss + var groupInfo: GroupInfo + var member: GroupMember + @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + + var body: some View { + HStack(spacing: 0) { + ZStack { + Text("Reject") + .foregroundColor(.red) + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + showRejectMemberAlert(groupInfo, member, dismiss: dismiss) + } + + ZStack { + Text("Accept") + .foregroundColor(theme.colors.primary) + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + showAcceptMemberAlert(groupInfo, member, dismiss: dismiss) + } + } + .frame(minHeight: 54) + .frame(maxWidth: .infinity) + .background(ToolbarMaterial.material(toolbarMaterial)) + } +} + +func showRejectMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) { + showAlert( + title: NSLocalizedString("Reject member?", comment: "alert title"), + buttonTitle: "Reject", + buttonAction: { removeMember(groupInfo, member, withMessages: false, dismiss: dismiss) }, + cancelButton: true + ) +} + +func showAcceptMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) { + showAlert( + NSLocalizedString("Accept member", comment: "alert title"), + message: NSLocalizedString("Member will join the group, accept member?", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Accept as member", comment: "alert action"), + style: .default, + handler: { _ in + acceptMember(groupInfo, member, .member, dismiss: dismiss) + } + ), + UIAlertAction( + title: NSLocalizedString("Accept as observer", comment: "alert action"), + style: .default, + handler: { _ in + acceptMember(groupInfo, member, .observer, dismiss: dismiss) + } + ), + cancelAlertAction + ]} + ) +} + +func acceptMember(_ groupInfo: GroupInfo, _ member: GroupMember, _ role: GroupMemberRole, dismiss: DismissAction? = nil) { + Task { + do { + let (gInfo, acceptedMember) = try await apiAcceptMember(groupInfo.groupId, member.groupMemberId, role) + await MainActor.run { + _ = ChatModel.shared.upsertGroupMember(gInfo, acceptedMember) + ChatModel.shared.updateGroup(gInfo) + dismiss?() + } + } catch let error { + logger.error("apiAcceptMember error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting member", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + +#Preview { + ContextPendingMemberActionsView( + groupInfo: GroupInfo.sampleData, + member: GroupMember.sampleData + ) +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift new file mode 100644 index 0000000000..427a600627 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift @@ -0,0 +1,305 @@ +// +// ContextProfilePickerView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 13.06.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +let USER_ROW_SIZE: CGFloat = 60 +let MAX_VISIBLE_USER_ROWS: CGFloat = 4.8 + +struct ContextProfilePickerView: View { + @ObservedObject var chat: Chat + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @State var selectedUser: User + @State private var users: [User] = [] + @State private var listExpanded = false + @State private var expandedListReady = false + @State private var showIncognitoSheet = false + + @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false + + var body: some View { + viewBody() + .onAppear { + users = chatModel.users + .map { $0.user } + .filter { u in u.activeUser || !u.hidden } + } + .sheet(isPresented: $showIncognitoSheet) { + IncognitoHelp() + } + } + + private func viewBody() -> some View { + Group { + if !listExpanded || chat.chatInfo.profileChangeProhibited { + currentSelection() + } else { + profilePicker() + } + } + } + + private func currentSelection() -> some View { + VStack(spacing: 0) { + HStack { + Text("Your profile") + .font(.callout) + .foregroundColor(theme.colors.secondary) + Spacer() + } + .padding(.top, 8) + .padding(.bottom, -4) + .padding(.leading, 12) + .padding(.trailing) + + if chat.chatInfo.profileChangeProhibited { + if chat.chatInfo.incognito { + incognitoOption() + } else { + profilerPickerUserOption(selectedUser) + } + } else if incognitoDefault { + incognitoOption() + } else { + profilerPickerUserOption(selectedUser) + } + } + } + + private func profilePicker() -> some View { + ScrollViewReader { proxy in + Group { + if expandedListReady { + let scroll = ScrollView { + LazyVStack(spacing: 0) { + let otherUsers = users + .filter { u in u.userId != selectedUser.userId } + .sorted(using: KeyPathComparator(\.activeOrder)) + ForEach(otherUsers) { p in + profilerPickerUserOption(p) + .contentShape(Rectangle()) + Divider() + .padding(.leading) + .padding(.leading, 48) + } + + if incognitoDefault { + profilerPickerUserOption(selectedUser) + .contentShape(Rectangle()) + Divider() + .padding(.leading) + .padding(.leading, 48) + + incognitoOption() + .contentShape(Rectangle()) + .id("BOTTOM_ANCHOR") + } else { + incognitoOption() + .contentShape(Rectangle()) + Divider() + .padding(.leading) + .padding(.leading, 48) + + profilerPickerUserOption(selectedUser) + .contentShape(Rectangle()) + .id("BOTTOM_ANCHOR") + } + } + } + .frame(maxHeight: USER_ROW_SIZE * min(MAX_VISIBLE_USER_ROWS, CGFloat(users.count + 1))) // + 1 for incognito + .onAppear { + DispatchQueue.main.async { + withAnimation(nil) { + proxy.scrollTo("BOTTOM_ANCHOR", anchor: .bottom) + } + } + } + .onDisappear { + expandedListReady = false + } + + if #available(iOS 16.0, *) { + scroll.scrollDismissesKeyboard(.never) + } else { + scroll + } + } else { + // Keep showing current selection to avoid flickering of scroll to bottom + currentSelection() + .onAppear { + // Delay rendering of expanded profile list + DispatchQueue.main.async { + expandedListReady = true + } + } + } + } + } + } + + private func profilerPickerUserOption(_ user: User) -> some View { + Button { + if !chat.chatInfo.profileChangeProhibited { + if selectedUser == user { + if !incognitoDefault { + listExpanded.toggle() + } else { + incognitoDefault = false + listExpanded = false + } + } else if selectedUser != user { + changeProfile(user) + } + } else { + showCantChangeProfileAlert() + } + } label: { + HStack { + ProfileImage(imageStr: user.image, size: 38) + Text(user.chatViewName) + .fontWeight(selectedUser == user && !incognitoDefault ? .medium : .regular) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + + Spacer() + + if selectedUser == user && !incognitoDefault { + if listExpanded { + Image(systemName: "chevron.down") + .font(.system(size: 12, weight: .bold)) + .foregroundColor(theme.colors.secondary) + .opacity(0.7) + } else if !chat.chatInfo.profileChangeProhibited { + Image(systemName: "chevron.up") + .font(.system(size: 12, weight: .bold)) + .foregroundColor(theme.colors.secondary) + .opacity(0.7) + } + } + } + .padding(.leading, 12) + .padding(.trailing) + .frame(height: USER_ROW_SIZE) + } + } + + private func changeProfile(_ newUser: User) { + Task { + do { + if let contact = chat.chatInfo.contact { + let updatedContact = try await apiChangePreparedContactUser(contactId: contact.contactId, newUserId: newUser.userId) + await MainActor.run { + selectedUser = newUser + incognitoDefault = false + listExpanded = false + chatModel.updateContact(updatedContact) + } + } else if let groupInfo = chat.chatInfo.groupInfo { + let updatedGroupInfo = try await apiChangePreparedGroupUser(groupId: groupInfo.groupId, newUserId: newUser.userId) + await MainActor.run { + selectedUser = newUser + incognitoDefault = false + listExpanded = false + chatModel.updateGroup(updatedGroupInfo) + } + } + do { + try await changeActiveUserAsync_(newUser.userId, viewPwd: nil, keepingChatId: chat.id) + } catch { + await MainActor.run { + showAlert( + NSLocalizedString("Error switching profile", comment: "alert title"), + message: String.localizedStringWithFormat(NSLocalizedString("Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile.", comment: "alert message"), newUser.chatViewName) + ) + } + } + } catch let error { + await MainActor.run { + if let currentUser = chatModel.currentUser { + selectedUser = currentUser + } + showAlert( + NSLocalizedString("Error changing chat profile", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } + + private func incognitoOption() -> some View { + Button { + if !chat.chatInfo.profileChangeProhibited { + if incognitoDefault { + listExpanded.toggle() + } else { + incognitoDefault = true + listExpanded = false + } + } else { + showCantChangeProfileAlert() + } + } label : { + HStack { + incognitoProfileImage() + Text("Incognito") + .fontWeight(incognitoDefault ? .medium : .regular) + .foregroundColor(theme.colors.onBackground) + Image(systemName: "info.circle") + .font(.system(size: 16)) + .foregroundColor(theme.colors.primary) + .onTapGesture { + showIncognitoSheet = true + } + + Spacer() + + if incognitoDefault { + if listExpanded { + Image(systemName: "chevron.down") + .font(.system(size: 12, weight: .bold)) + .foregroundColor(theme.colors.secondary) + .opacity(0.7) + } else if !chat.chatInfo.profileChangeProhibited { + Image(systemName: "chevron.up") + .font(.system(size: 12, weight: .bold)) + .foregroundColor(theme.colors.secondary) + .opacity(0.7) + } + } + } + .padding(.leading, 12) + .padding(.trailing) + .frame(height: USER_ROW_SIZE) + } + } + + private func incognitoProfileImage() -> some View { + Image(systemName: "theatermasks.fill") + .resizable() + .scaledToFit() + .frame(width: 38) + .foregroundColor(.indigo) + } + + private func showCantChangeProfileAlert() { + showAlert( + NSLocalizedString("Can't change profile", comment: "alert title"), + message: NSLocalizedString("To use another profile after connection attempt, delete the chat and use the link again.", comment: "alert message") + ) + } +} + +#Preview { + ContextProfilePickerView( + chat: Chat.sampleData, + selectedUser: User.sampleData + ) +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index 2fc122f249..c5fd8e39d0 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -16,19 +16,15 @@ 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) @@ -37,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 } @@ -48,49 +43,45 @@ 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) field.setPlaceholderView() - updateFont(field) - updateHeight(field) + 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) + field.placeholder = text.isEmpty ? placeholder : "" } - - let castedField = field as! CustomUITextField - if castedField.placeholder != placeholder { - castedField.placeholder = placeholder + if field.placeholder != placeholder { + field.placeholder = text.isEmpty ? placeholder : "" } - } - - 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) - } - } - - 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 } +// This block causes delays in closing keyboard when navigating from chat view to chat list. +// It is also a candidate for iOS 26.1 freeze. +// This was added in commit below to open keyboard programmatically via a passed binding but this approach is not reliable. +// https://github.com/simplex-chat/simplex-chat/pull/6003/commits/cb666de51375623451a5e80dcf59449adc7d2a5f +// if focused && !field.isFocused { +// DispatchQueue.main.async { +// if !field.isFocused { +// field.becomeFirstResponder() +// } +// } +// } } } @@ -98,15 +89,17 @@ 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 } - + private let placeholderLabel: UILabel = UILabel() - init(height: Binding) { + init(parent: NativeTextEditor?, height: Binding) { + self.parent = parent self.height = height super.init(frame: .zero, textContainer: nil) } @@ -128,11 +121,44 @@ 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) { @@ -232,10 +258,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 + } } } @@ -246,7 +284,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 fb69dfdd17..07cd61583b 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -12,13 +12,17 @@ import SimpleXChat private let liveMsgInterval: UInt64 = 3000_000000 struct SendMessageView: View { + var placeholder: String? @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 var cancelLiveMessage: (() -> Void)? = nil - var nextSendGrpInv: Bool = false + var sendToConnect: (() -> Void)? = nil + var hideSendButton: Bool = false var showVoiceMessageButton: Bool = true var voiceMessageAllowed: Bool = true var disableSendButton = false @@ -31,81 +35,76 @@ 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 @State private var showCustomDisappearingMessageDialogue = false @State private var showCustomTimePicker = false @State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get() - @State private var progressByTimeout = false @UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false var body: some View { - ZStack { - let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) - HStack(alignment: .bottom) { - 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) - .frame(maxWidth: .infinity) - } else { - NativeTextEditor( - text: $composeState.message, - disableEditing: $composeState.inProgress, - height: $teHeight, - focused: $keyboardVisible, - placeholder: Binding(get: { composeState.placeholder }, set: { _ in }), - 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) - } - } - .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 { - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - progressByTimeout = composeState.inProgress - } + 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 { - progressByTimeout = false + NativeTextEditor( + text: $composeState.message, + disableEditing: $composeState.inProgress, + height: $teHeight, + focused: $keyboardVisible, + lastUnfocusedDate: $keyboardHiddenDate, + placeholder: Binding(get: { placeholder ?? composeState.placeholder }, set: { _ in }), + selectedRange: $selectedRange, + onImagesAdded: onMediaAdded + ) + .padding(.trailing, 32) + .allowsTightening(false) + .fixedSize(horizontal: false, vertical: true) } } + .overlay(alignment: .topTrailing, content: { + if !composeState.progressByTimeout && teHeight > 100 && !composeState.inProgress { + deleteTextButton() + } + }) + .overlay(alignment: .bottomTrailing) { + if composeState.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) }) .padding(.vertical, 8) } @ViewBuilder private func composeActionButtons() -> some View { let vmrs = composeState.voiceMessageRecordingState - if nextSendGrpInv { - inviteMemberContactButton() + if hideSendButton { + EmptyView() + } else if let connect = sendToConnect { + sendToConnectButton(connect) } else if case .reportedItem = composeState.contextItem { sendMessageButton() } else if showVoiceMessageButton @@ -153,21 +152,17 @@ struct SendMessageView: View { .padding([.top, .trailing], 4) } - private func inviteMemberContactButton() -> some View { - Button { - sendMessage(nil) - } label: { + private func sendToConnectButton(_ connect: @escaping () -> Void) -> some View { + let disabled = !composeState.sendEnabled || composeState.inProgress || disableSendButton + return Button(action: connect) { Image(systemName: "arrow.up.circle.fill") .resizable() - .foregroundColor(sendButtonColor) + .foregroundColor(disabled ? theme.colors.secondary.opacity(0.67) : sendButtonColor) .frame(width: sendButtonSize, height: sendButtonSize) .opacity(sendButtonOpacity) } - .disabled( - !composeState.sendEnabled || - composeState.inProgress - ) - .frame(width: 29, height: 29) + .disabled(disabled) + .frame(width: 31, height: 31) .padding([.bottom, .trailing], 4) } @@ -190,7 +185,7 @@ struct SendMessageView: View { composeState.endLiveDisabled || disableSendButton ) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .contextMenu{ sendButtonContextMenuItems() } @@ -251,6 +246,7 @@ struct SendMessageView: View { } private struct RecordVoiceMessageButton: View { + @Environment(\.isEnabled) var isEnabled @EnvironmentObject var theme: AppTheme var startVoiceMessageRecording: (() -> Void)? var finishVoiceMessageRecording: (() -> Void)? @@ -259,15 +255,14 @@ struct SendMessageView: View { @State private var pressed: TimeInterval? = nil var body: some View { - Button(action: {}) { - Image(systemName: "mic.fill") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .foregroundColor(theme.colors.primary) - } + 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 { @@ -275,9 +270,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 @@ -323,7 +316,7 @@ struct SendMessageView: View { .foregroundColor(theme.colors.secondary) } .disabled(composeState.inProgress) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .padding([.bottom, .trailing], 4) } @@ -351,7 +344,7 @@ struct SendMessageView: View { Image(systemName: "bolt.fill") .resizable() .scaledToFit() - .foregroundColor(theme.colors.primary) + .foregroundColor(isEnabled ? theme.colors.primary : theme.colors.secondary) .frame(width: 20, height: 20) } .frame(width: 29, height: 29) @@ -408,7 +401,7 @@ struct SendMessageView: View { .foregroundColor(theme.colors.primary) } .disabled(composeState.inProgress) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .padding([.bottom, .trailing], 4) } @@ -424,8 +417,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 { @@ -434,9 +429,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 { @@ -444,9 +441,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/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 66fe67a29e..3154f16f5b 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -78,6 +78,12 @@ struct AddGroupMembersViewCommon: View { let count = selectedContacts.count Section { if creatingGroup { + MemberAdmissionButton( + groupInfo: $groupInfo, + admission: groupInfo.groupProfile.memberAdmission_, + currentAdmission: groupInfo.groupProfile.memberAdmission_, + creatingGroup: true + ) GroupPreferencesButton( groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, @@ -145,9 +151,9 @@ struct AddGroupMembersViewCommon: View { return dummy }() - @ViewBuilder private func inviteMembersButton() -> some View { + private func inviteMembersButton() -> some View { let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat" - Button { + return Button { inviteMembers() } label: { HStack { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index b0f896e493..96b5e2898a 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -17,11 +17,12 @@ struct GroupChatInfoView: View { @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat @Binding var groupInfo: GroupInfo + @Binding var scrollToItemId: ChatItem.ID? 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: GroupLink? @State private var groupLinkMemberRole: GroupMemberRole = .member @State private var groupLinkNavLinkActive: Bool = false @State private var addMembersNavLinkActive: Bool = false @@ -33,6 +34,7 @@ struct GroupChatInfoView: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var searchText: String = "" @FocusState private var searchFocussed + @State private var showSecrets: Set = [] enum GroupChatInfoViewAlert: Identifiable { case deleteGroupAlert @@ -44,7 +46,6 @@ struct GroupChatInfoView: View { case unblockMemberAlert(mem: GroupMember) case blockForAllAlert(mem: GroupMember) case unblockForAllAlert(mem: GroupMember) - case removeMemberAlert(mem: GroupMember) case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { @@ -58,7 +59,6 @@ struct GroupChatInfoView: View { case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)" case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)" case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)" - case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)" case let .error(title, _): return "error \(title)" } } @@ -74,12 +74,12 @@ struct GroupChatInfoView: View { List { groupInfoHeader() .listRowBackground(Color.clear) - + localAliasTextEdit() .listRowBackground(Color.clear) .listRowSeparator(.hidden) .padding(.bottom, 18) - + infoActionButtons() .padding(.horizontal) .frame(maxWidth: .infinity) @@ -87,7 +87,25 @@ struct GroupChatInfoView: View { .listRowBackground(Color.clear) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - + + Section { + if groupInfo.canAddMembers && groupInfo.businessChat == nil { + groupLinkButton() + } + if groupInfo.businessChat == nil && groupInfo.membership.memberRole >= .moderator { + memberSupportButton() + } + if groupInfo.canModerate { + GroupReportsChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId) + } + if groupInfo.membership.memberActive + && (groupInfo.membership.memberRole < .moderator || groupInfo.membership.supportChat != nil) { + UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId) + } + } header: { + Text("") + } + Section { if groupInfo.isOwner && groupInfo.businessChat == nil { editGroupButton() @@ -96,19 +114,6 @@ struct GroupChatInfoView: View { addOrEditWelcomeMessage() } GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences) - if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { - sendReceiptsOption() - } else { - sendReceiptsOptionDisabled() - } - - NavigationLink { - ChatWallpaperEditorSheet(chat: chat) - } label: { - Label("Chat theme", systemImage: "photo") - } - } header: { - Text("") } footer: { let label: LocalizedStringKey = ( groupInfo.businessChat == nil @@ -118,56 +123,64 @@ struct GroupChatInfoView: View { Text(label) .foregroundColor(theme.colors.secondary) } - + Section { + if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { + sendReceiptsOption() + } else { + sendReceiptsOptionDisabled() + } + NavigationLink { + ChatWallpaperEditorSheet(chat: chat) + } label: { + Label("Chat theme", systemImage: "photo") + } ChatTTLOption(chat: chat, progressIndicator: $progressIndicator) } footer: { Text("Delete chat messages from your device.") } - - Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) { - if groupInfo.canAddMembers { - if groupInfo.businessChat == nil { - groupLinkButton() + + if !groupInfo.nextConnectPrepared { + Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) { + if groupInfo.canAddMembers { + if (chat.chatInfo.incognito) { + Label("Invite members", systemImage: "plus") + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .onTapGesture { alert = .cantInviteIncognitoAlert } + } else { + addMembersButton() + } } - if (chat.chatInfo.incognito) { - Label("Invite members", systemImage: "plus") - .foregroundColor(Color(uiColor: .tertiaryLabel)) - .onTapGesture { alert = .cantInviteIncognitoAlert } - } else { - addMembersButton() - } - } - if members.count > 8 { 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.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() - } - .opacity(0) - MemberRowView(groupInfo: groupInfo, groupMember: member, alert: $alert) + 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), + scrollToItemId: $scrollToItemId, + user: true, + alert: $alert + ) + ForEach(filteredMembers) { member in + MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert) } } } - + Section { clearChatButton() if groupInfo.canDelete { deleteGroupButton() } - if groupInfo.membership.memberCurrent { + if groupInfo.membership.memberCurrentOrPending { leaveGroupButton() } } - + if developerTools { Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { infoRow("Local name", chat.chatInfo.localDisplayName) @@ -179,7 +192,7 @@ struct GroupChatInfoView: View { .navigationBarHidden(true) .disabled(progressIndicator) .opacity(progressIndicator ? 0.6 : 1) - + if progressIndicator { ProgressView().scaleEffect(2) } @@ -197,7 +210,6 @@ struct GroupChatInfoView: View { case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem) case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem) case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem) - case let .removeMemberAlert(mem): return removeMemberAlert(mem) case let .error(title, error): return mkAlert(title: title, message: error) } } @@ -207,8 +219,9 @@ struct GroupChatInfoView: View { } sendReceipts = SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault) do { - if let link = try apiGetGroupLink(groupInfo.groupId) { - (groupLink, groupLinkMemberRole) = link + if let gLink = try apiGetGroupLink(groupInfo.groupId) { + groupLink = gLink + groupLinkMemberRole = gLink.acceptMemberRole } } catch let error { logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))") @@ -219,19 +232,30 @@ struct GroupChatInfoView: View { private func groupInfoHeader() -> some View { VStack { let cInfo = chat.chatInfo + // show actual display name, alias can be edited in this view + let displayName = (cInfo.groupInfo?.groupProfile.displayName ?? cInfo.displayName).trimmingCharacters(in: .whitespacesAndNewlines) + let fullName = cInfo.fullName.trimmingCharacters(in: .whitespacesAndNewlines) ChatInfoImage(chat: chat, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.top, 12) .padding() - Text(cInfo.groupInfo?.groupProfile.displayName ?? cInfo.displayName) + Text(displayName) .font(.largeTitle) .multilineTextAlignment(.center) .lineLimit(4) .padding(.bottom, 2) - if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName { + if fullName != "" && fullName != displayName && fullName != cInfo.displayName.trimmingCharacters(in: .whitespacesAndNewlines) { Text(cInfo.fullName) .font(.title2) .multilineTextAlignment(.center) - .lineLimit(8) + .lineLimit(3) + .padding(.bottom, 2) + } + if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { + let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) + .multilineTextAlignment(.center) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) } } .frame(maxWidth: .infinity, alignment: .center) @@ -253,7 +277,7 @@ struct GroupChatInfoView: View { .multilineTextAlignment(.center) .foregroundColor(theme.colors.secondary) } - + private func setGroupAlias() { Task { do { @@ -267,7 +291,7 @@ struct GroupChatInfoView: View { } } } - + func infoActionButtons() -> some View { GeometryReader { g in let buttonWidth = g.size.width / 4 @@ -276,7 +300,9 @@ struct GroupChatInfoView: View { if groupInfo.canAddMembers { addMembersActionButton(width: buttonWidth) } - muteButton(width: buttonWidth) + if let nextNtfMode = chat.chatInfo.nextNtfMode { + muteButton(width: buttonWidth, nextNtfMode: nextNtfMode) + } } .frame(maxWidth: .infinity, alignment: .center) } @@ -290,9 +316,9 @@ struct GroupChatInfoView: View { .disabled(!groupInfo.ready || chat.chatItems.isEmpty) } - @ViewBuilder private func addMembersActionButton(width: CGFloat) -> some View { - if chat.chatInfo.incognito { - ZStack { + private func addMembersActionButton(width: CGFloat) -> some View { + ZStack { + if chat.chatInfo.incognito { InfoViewButton(image: "link.badge.plus", title: "invite", width: width) { groupLinkNavLinkActive = true } @@ -304,10 +330,7 @@ struct GroupChatInfoView: View { } .frame(width: 1, height: 1) .hidden() - } - .disabled(!groupInfo.ready) - } else { - ZStack { + } else { InfoViewButton(image: "person.fill.badge.plus", title: "invite", width: width) { addMembersNavLinkActive = true } @@ -320,17 +343,17 @@ struct GroupChatInfoView: View { .frame(width: 1, height: 1) .hidden() } - .disabled(!groupInfo.ready) } + .disabled(!groupInfo.ready) } - private func muteButton(width: CGFloat) -> some View { - InfoViewButton( - image: chat.chatInfo.ntfsEnabled ? "speaker.slash.fill" : "speaker.wave.2.fill", - title: chat.chatInfo.ntfsEnabled ? "mute" : "unmute", + private func muteButton(width: CGFloat, nextNtfMode: MsgFilter) -> some View { + return InfoViewButton( + image: nextNtfMode.iconFilled, + title: "\(nextNtfMode.text(mentions: true))", width: width ) { - toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) + toggleNotifications(chat, enableNtfs: nextNtfMode) } .disabled(!groupInfo.ready) } @@ -353,25 +376,23 @@ struct GroupChatInfoView: View { .onAppear { searchFocussed = false Task { - let groupMembers = await apiListMembers(groupInfo.groupId) - await MainActor.run { - chatModel.groupMembers = groupMembers.map { GMember.init($0) } - chatModel.populateGroupMembersIndexes() - } + await chatModel.loadGroupMembers(groupInfo) } } } private struct MemberRowView: View { + var chat: Chat var groupInfo: GroupInfo @ObservedObject var groupMember: GMember + @Binding var scrollToItemId: ChatItem.ID? @EnvironmentObject var theme: AppTheme var user: Bool = false @Binding var alert: GroupChatInfoViewAlert? var body: some View { let member = groupMember.wrapped - let v = HStack{ + let v1 = HStack{ MemberProfileImage(member, size: 38) .padding(.trailing, 2) // TODO server connection status @@ -387,10 +408,24 @@ struct GroupChatInfoView: View { 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 { + } else if groupInfo.membership.memberRole >= .moderator { // TODO if there are more actions, refactor with lists of swipeActions let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo) let canRemove = member.canBeRemoved(groupInfo: groupInfo) @@ -412,6 +447,11 @@ struct GroupChatInfoView: View { } } + private func memberInfoView() -> some View { + GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember, scrollToItemId: $scrollToItemId) + .navigationBarHidden(false) + } + private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey { if member.activeConn?.connDisabled ?? false { return "disabled" @@ -428,7 +468,7 @@ struct GroupChatInfoView: View { .foregroundColor(theme.colors.secondary) } else { let role = member.memberRole - if [.owner, .admin, .observer].contains(role) { + if [.owner, .admin, .moderator, .observer].contains(role) { Text(member.memberRole.text) .foregroundColor(theme.colors.secondary) } @@ -474,7 +514,7 @@ struct GroupChatInfoView: View { private func removeSwipe(_ member: GroupMember, _ v: V) -> some View { v.swipeActions(edge: .trailing) { Button(role: .destructive) { - alert = .removeMemberAlert(mem: member) + showRemoveMemberAlert(groupInfo, member) } label: { Label("Remove member", systemImage: "trash") .foregroundColor(Color.red) @@ -491,11 +531,6 @@ struct GroupChatInfoView: View { } } - private func memberInfoView(_ groupMember: GMember) -> some View { - GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember) - .navigationBarHidden(false) - } - private func groupLinkButton() -> some View { NavigationLink { groupLinkDestinationView() @@ -521,15 +556,99 @@ struct GroupChatInfoView: View { .navigationBarTitleDisplayMode(.large) } + struct UserSupportChatNavLink: View { + @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme + var groupInfo: GroupInfo + @EnvironmentObject var chatModel: ChatModel + @Binding var scrollToItemId: ChatItem.ID? + @State private var navLinkActive = false + + var body: some View { + let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil) + NavigationLink(isActive: $navLinkActive) { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } label: { + HStack { + Label("Chat with admins", systemImage: chat.supportUnreadCount > 0 ? "flag.fill" : "flag") + Spacer() + if chat.supportUnreadCount > 0 { + UnreadBadge(count: chat.supportUnreadCount, color: theme.colors.primary) + } + } + } + .onChange(of: navLinkActive) { active in + if active { + ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo)) + } + } + } + } + + private func memberSupportButton() -> some View { + NavigationLink { + MemberSupportView(groupInfo: groupInfo, scrollToItemId: $scrollToItemId) + .navigationBarTitle("Chats with members") + .modifier(ThemedBackground()) + .navigationBarTitleDisplayMode(.large) + } label: { + HStack { + Label( + "Chats with members", + systemImage: chat.supportUnreadCount > 0 ? "flag.fill" : "flag" + ) + Spacer() + if chat.supportUnreadCount > 0 { + UnreadBadge(count: chat.supportUnreadCount, color: theme.colors.primary) + } + } + } + } + + struct GroupReportsChatNavLink: View { + @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme + var groupInfo: GroupInfo + @EnvironmentObject var chatModel: ChatModel + @Binding var scrollToItemId: ChatItem.ID? + @State private var navLinkActive = false + + var body: some View { + NavigationLink(isActive: $navLinkActive) { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: .reports), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } label: { + HStack { + Label { + Text("Member reports") + } icon: { + Image(systemName: chat.chatStats.reportsCount > 0 ? "flag.fill" : "flag").foregroundColor(.red) + } + Spacer() + if chat.chatStats.reportsCount > 0 { + UnreadBadge(count: chat.chatStats.reportsCount, color: .red) + } + } + } + .onChange(of: navLinkActive) { active in + if active { + ItemsModel.loadSecondaryChat(chat.id, chatFilter: .msgContentTagContext(contentTag: .report)) + } + } + } + } + private func editGroupButton() -> some View { NavigationLink { GroupProfileView( groupInfo: $groupInfo, groupProfile: groupInfo.groupProfile ) - .navigationBarTitle("Group profile") - .modifier(ThemedBackground()) - .navigationBarTitleDisplayMode(.large) } label: { Label("Edit group profile", systemImage: "pencil") } @@ -571,9 +690,9 @@ struct GroupChatInfoView: View { } } - @ViewBuilder private func leaveGroupButton() -> some View { + private func leaveGroupButton() -> some View { let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat" - Button(role: .destructive) { + return Button(role: .destructive) { alert = .leaveGroupAlert } label: { Label(label, systemImage: "rectangle.portrait.and.arrow.right") @@ -640,14 +759,13 @@ struct GroupChatInfoView: View { } private func sendReceiptsOption() -> some View { - Picker(selection: $sendReceipts) { + WrappedPicker(selection: $sendReceipts) { ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in Text(opt.text) } } label: { Label("Send receipts", systemImage: "checkmark.message") } - .frame(height: 36) .onChange(of: sendReceipts) { _ in setSendReceipts() } @@ -670,32 +788,50 @@ struct GroupChatInfoView: View { alert = .largeGroupReceiptsDisabled } } +} - private func removeMemberAlert(_ mem: GroupMember) -> Alert { - let messageLabel: LocalizedStringKey = ( +func showRemoveMemberAlert(_ groupInfo: GroupInfo, _ mem: GroupMember, dismiss: DismissAction? = nil) { + showAlert( + NSLocalizedString("Remove member?", comment: "alert title"), + message: 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(messageLabel), - primaryButton: .destructive(Text("Remove")) { - Task { - do { - let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId) - await MainActor.run { - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) - } - } catch let error { - logger.error("apiRemoveMember error: \(responseError(error))") - let a = getErrorAlert(error, "Error removing member") - alert = .error(title: a.title, error: a.message) + ? NSLocalizedString("Member will be removed from group - this cannot be undone!", comment: "alert message") + : NSLocalizedString("Member will be removed from chat - this cannot be undone!", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Remove", comment: "alert action"), style: .destructive) { _ in + removeMember(groupInfo, mem, withMessages: false, dismiss: dismiss) + }, + UIAlertAction(title: NSLocalizedString("Remove and delete messages", comment: "alert action"), style: .destructive) { _ in + removeMember(groupInfo, mem, withMessages: true, dismiss: dismiss) + }, + cancelAlertAction + ]} + ) +} + +func removeMember(_ groupInfo: GroupInfo, _ mem: GroupMember, withMessages: Bool, dismiss: DismissAction?) { + Task { + do { + let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId], withMessages) + await MainActor.run { + ChatModel.shared.updateGroup(updatedGroupInfo) + updatedMembers.forEach { updatedMember in + _ = ChatModel.shared.upsertGroupMember(updatedGroupInfo, updatedMember) + if withMessages { + ChatModel.shared.removeMemberItems(updatedMember, byMember: groupInfo.membership, groupInfo) } } - }, - secondaryButton: .cancel() - ) + dismiss?() + } + } catch let error { + logger.error("apiRemoveMembers error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error removing member", comment: "alert title"), + message: responseError(error) + ) + } + } } } @@ -712,11 +848,11 @@ struct GroupPreferencesButton: View { @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( @@ -734,7 +870,7 @@ struct GroupPreferencesButton: View { creatingGroup ? "Save" : "Save and notify group members", comment: "alert button" ) - + if groupInfo.fullGroupPreferences != preferences { showAlert( title: NSLocalizedString("Save preferences?", comment: "alert title"), @@ -752,7 +888,7 @@ struct GroupPreferencesButton: View { } } } - + private func savePreferences() { Task { do { @@ -769,7 +905,6 @@ struct GroupPreferencesButton: View { } } } - } @@ -792,6 +927,7 @@ struct GroupChatInfoView_Previews: PreviewProvider { GroupChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: Binding.constant(GroupInfo.sampleData), + scrollToItemId: Binding.constant(nil), onSearch: {}, localAlias: "" ) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 39288e2d52..bc1ac4ab65 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -10,12 +10,14 @@ import SwiftUI import SimpleXChat struct GroupLinkView: View { + @EnvironmentObject var theme: AppTheme var groupId: Int64 - @Binding var groupLink: String? + @Binding var groupLink: GroupLink? @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 @@ -33,16 +35,23 @@ struct GroupLinkView: View { } var body: some View { - if creatingGroup { - groupLinkView() - .navigationBarBackButtonHidden() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button ("Continue") { linkCreatedCb?() } + ZStack { + if creatingGroup { + groupLinkView() + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button ("Continue") { linkCreatedCb?() } + } } - } - } else { - groupLinkView() + } else { + groupLinkView() + } + if creatingLink { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity) + } } } @@ -69,10 +78,21 @@ struct GroupLinkView: View { } } .frame(height: 36) - SimpleXLinkQRCode(uri: groupLink) - .id("simplex-qrcode-view-for-\(groupLink)") + SimpleXCreatedLinkQRCode(link: groupLink.connLinkContact, short: $showShortLink) + .id("simplex-qrcode-view-for-\(groupLink.connLinkContact.simplexChatUri(short: showShortLink))") + if groupLink.shouldBeUpgraded { + Button { + upgradeAndShareLinkAlert() + } label: { + Label("Upgrade link", systemImage: "arrow.up") + } + } Button { - showShareSheet(items: [simplexChatLink(groupLink)]) + if groupLink.shouldBeUpgraded { + upgradeAndShareLinkAlert(groupLink: groupLink) + } else { + groupLink.shareAddress(short: showShortLink) + } } label: { Label("Share link", systemImage: "square.and.arrow.up") } @@ -87,11 +107,10 @@ struct GroupLinkView: View { Label("Create link", systemImage: "link.badge.plus") } .disabled(creatingLink) - if creatingLink { - ProgressView() - .scaleEffect(2) - .frame(maxWidth: .infinity) - } + } + } header: { + if let groupLink, groupLink.connLinkContact.connShortLink != nil { + ToggleShortLinkHeader(text: Text(""), link: groupLink.connLinkContact, short: $showShortLink) } } .alert(item: $alert) { alert in @@ -118,7 +137,7 @@ struct GroupLinkView: View { .onChange(of: groupLinkMemberRole) { _ in Task { do { - _ = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole) + groupLink = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole) } catch let error { let a = getErrorAlert(error, "Error updating group link") alert = .error(title: a.title, error: a.message) @@ -139,10 +158,10 @@ struct GroupLinkView: View { Task { do { creatingLink = true - let link = try await apiCreateGroupLink(groupId) + let gLink = try await apiCreateGroupLink(groupId) await MainActor.run { creatingLink = false - (groupLink, groupLinkMemberRole) = link + groupLink = gLink } } catch let error { logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))") @@ -154,12 +173,61 @@ struct GroupLinkView: View { } } } + + private func upgradeAndShareLinkAlert(groupLink: GroupLink? = nil) { + showAlert( + NSLocalizedString("Upgrade group link?", comment: "alert message"), + message: NSLocalizedString("The link will be short, and group profile will be shared via the link.", comment: "alert message"), + actions: { + var actions = [UIAlertAction(title: NSLocalizedString("Upgrade", comment: "alert button"), style: .default) { _ in + addShortLink(shareOnCompletion: groupLink != nil) + }] + if let groupLink { + actions.append(UIAlertAction(title: NSLocalizedString("Share old link", comment: "alert button"), style: .default) { _ in + groupLink.shareAddress(short: showShortLink) + }) + } + actions.append(cancelAlertAction) + return actions + } + ) + } + + private func addShortLink(shareOnCompletion: Bool = false) { + Task { + do { + creatingLink = true + let gLink = try await apiAddGroupShortLink(groupId) + await MainActor.run { + creatingLink = false + groupLink = gLink + if shareOnCompletion, let gLink { + gLink.shareAddress(short: showShortLink) + } + } + } catch let error { + logger.error("apiAddGroupShortLink: \(responseError(error))") + await MainActor.run { + creatingLink = false + let a = getErrorAlert(error, "Error adding short link") + alert = .error(title: a.title, error: a.message) + } + } + } + } } 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: GroupLink? = GroupLink( + userContactLinkId: 1, + 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), + shortLinkDataSet: false, + shortLinkLargeDataSet: false, + groupLinkId: "abc", + acceptMemberRole: .member + ) + @State var noGroupLink: GroupLink? = nil return Group { GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member)) @@ -167,4 +235,3 @@ struct GroupLinkView_Previews: PreviewProvider { } } } - diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 102f0333be..207c2170a3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -16,7 +16,9 @@ struct GroupMemberInfoView: View { @State var groupInfo: GroupInfo @ObservedObject var chat: Chat @ObservedObject var groupMember: GMember + @Binding var scrollToItemId: ChatItem.ID? var navigation: Bool = false + var openedFromSupportChat: Bool = false @State private var connectionStats: ConnectionStats? = nil @State private var connectionCode: String? = nil @State private var connectionLoaded: Bool = false @@ -25,7 +27,6 @@ struct GroupMemberInfoView: View { @State private var knownContactConnectionStats: ConnectionStats? = nil @State private var newRole: GroupMemberRole = .member @State private var alert: GroupMemberInfoViewAlert? - @State private var sheet: PlanAndConnectActionSheet? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var justOpened = true @State private var progressIndicator = false @@ -35,12 +36,10 @@ struct GroupMemberInfoView: View { case unblockMemberAlert(mem: GroupMember) case blockForAllAlert(mem: GroupMember) case unblockForAllAlert(mem: GroupMember) - case removeMemberAlert(mem: GroupMember) case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole) case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert - case planAndConnectAlert(alert: PlanAndConnectAlert) case queueInfo(info: String) case someAlert(alert: SomeAlert) case error(title: LocalizedStringKey, error: LocalizedStringKey?) @@ -51,12 +50,10 @@ struct GroupMemberInfoView: View { case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)" case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)" case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)" - case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)" case let .changeMemberRoleAlert(mem, role): return "changeMemberRoleAlert \(mem.groupMemberId) \(role.rawValue)" case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" - case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" case let .queueInfo(info): return "queueInfo \(info)" case let .someAlert(alert): return "someAlert \(alert.id)" case let .error(title, _): return "error \(title)" @@ -103,6 +100,11 @@ struct GroupMemberInfoView: View { if member.memberActive { Section { + if !openedFromSupportChat + && groupInfo.membership.memberRole >= .moderator + && (member.memberRole < .moderator || member.supportChat != nil) { + MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId) + } if let code = connectionCode { verifyCodeButton(code) } if let connStats = connectionStats, connStats.ratchetSyncAllowed { @@ -156,7 +158,15 @@ struct GroupMemberInfoView: View { if let connStats = connectionStats { Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { - // TODO network connection status + if let subStatus = connStats.subStatus { + SubStatusRow(status: subStatus) + .onTapGesture { + showAlert( + NSLocalizedString("Network status", comment: "alert title"), + message: subStatus.statusExplanation + ) + } + } Button("Change receiving address") { alert = .switchAddressAlert } @@ -178,7 +188,7 @@ struct GroupMemberInfoView: View { } } - if groupInfo.membership.memberRole >= .admin { + if groupInfo.membership.memberRole >= .moderator { adminDestructiveSection(member) } else { nonAdminBlockSection(member) @@ -195,8 +205,9 @@ struct GroupMemberInfoView: View { Button ("Debug delivery") { Task { do { - let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) - await MainActor.run { alert = .queueInfo(info: info) } + if let info = try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId) { + await MainActor.run { alert = .queueInfo(info: queueInfoText(info)) } + } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") let a = getErrorAlert(e, "Error") @@ -260,25 +271,22 @@ struct GroupMemberInfoView: View { case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem) case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem) case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem) - case let .removeMemberAlert(mem): return removeMemberAlert(mem) case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem) case .switchAddressAlert: return switchAddressAlert(switchMemberAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) - case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) case let .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) } if progressIndicator { ProgressView().scaleEffect(2) } } .onChange(of: chat.chatInfo) { c in - if case let .group(gI) = chat.chatInfo { + if case let .group(gI, _) = chat.chatInfo { groupInfo = gI } } @@ -345,10 +353,8 @@ struct GroupMemberInfoView: View { Button { planAndConnect( contactLink, - showAlert: { alert = .planAndConnectAlert(alert: $0) }, - showActionSheet: { sheet = $0 }, - dismiss: true, - incognito: nil + theme: theme, + dismiss: true ) } label: { Label("Connect", systemImage: "link") @@ -366,14 +372,8 @@ struct GroupMemberInfoView: View { func newDirectChatButton(_ contactId: Int64, width: CGFloat) -> some View { InfoViewButton(image: "message.fill", title: "message", width: width) { Task { - do { - let chat = try await apiGetChat(type: .direct, id: contactId) - chatModel.addChat(chat) - ItemsModel.shared.loadOpenChat(chat.id) { - dismissAllSheets(animated: true) - } - } catch let error { - logger.error("openDirectChatButton apiGetChat error: \(responseError(error))") + ItemsModel.shared.loadOpenChat("@\(contactId)") { + dismissAllSheets(animated: true) } } } @@ -401,7 +401,6 @@ struct GroupMemberInfoView: View { ItemsModel.shared.loadOpenChat(memberContact.id) { dismissAllSheets(animated: true) } - NetworkModel.shared.setContactNetworkStatus(memberContact, .connected) } } catch let error { logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") @@ -451,35 +450,70 @@ struct GroupMemberInfoView: View { MemberProfileImage(mem, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.top, 12) .padding() + // show alias if set, alias cannot be edited in this view + let displayName = mem.displayName.trimmingCharacters(in: .whitespacesAndNewlines) + let fullName = mem.fullName.trimmingCharacters(in: .whitespacesAndNewlines) if mem.verified { ( Text(Image(systemName: "checkmark.shield")) .foregroundColor(theme.colors.secondary) .font(.title2) + textSpace - + Text(mem.displayName) + + Text(displayName) .font(.largeTitle) ) .multilineTextAlignment(.center) .lineLimit(2) .padding(.bottom, 2) } else { - Text(mem.displayName) + Text(displayName) .font(.largeTitle) .multilineTextAlignment(.center) .lineLimit(2) .padding(.bottom, 2) } - if mem.fullName != "" && mem.fullName != mem.displayName { + if fullName != "" && fullName != displayName && fullName != mem.memberProfile.displayName.trimmingCharacters(in: .whitespacesAndNewlines) { Text(mem.fullName) .font(.title2) .multilineTextAlignment(.center) + .lineLimit(3) + .padding(.bottom, 2) + } + if let descr = mem.memberProfile.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { + Text(descr) + .font(.subheadline) + .multilineTextAlignment(.center) .lineLimit(4) } } .frame(maxWidth: .infinity, alignment: .center) } + struct MemberInfoSupportChatNavLink: View { + @EnvironmentObject var theme: AppTheme + var groupInfo: GroupInfo + var member: GMember + @Binding var scrollToItemId: ChatItem.ID? + @State private var navLinkActive = false + + var body: some View { + let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: member.wrapped) + NavigationLink(isActive: $navLinkActive) { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } label: { + Label("Chat with member", systemImage: "flag") + } + .onChange(of: navLinkActive) { active in + if active { + ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo)) + } + } + } + } + private func verifyCodeButton(_ code: String) -> some View { let member = groupMember.wrapped return NavigationLink { @@ -542,7 +576,11 @@ struct GroupMemberInfoView: View { } } if canRemove { - removeMemberButton(mem) + if mem.memberStatus == .memRemoved || mem.memberStatus == .memLeft { + deleteMemberMessagesButton(mem) + } else { + removeMemberButton(mem) + } } } } @@ -597,38 +635,32 @@ struct GroupMemberInfoView: View { private func removeMemberButton(_ mem: GroupMember) -> some View { Button(role: .destructive) { - alert = .removeMemberAlert(mem: mem) + showRemoveMemberAlert(groupInfo, mem, dismiss: dismiss) } label: { Label("Remove member", systemImage: "trash") .foregroundColor(.red) } } - private func removeMemberAlert(_ mem: GroupMember) -> 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(label), - primaryButton: .destructive(Text("Remove")) { - Task { - do { - let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId) - await MainActor.run { - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) - dismiss() - } - } catch let error { - logger.error("apiRemoveMember error: \(responseError(error))") - let a = getErrorAlert(error, "Error removing member") - alert = .error(title: a.title, error: a.message) - } - } - }, - secondaryButton: .cancel() + private func deleteMemberMessagesButton(_ mem: GroupMember) -> some View { + Button(role: .destructive) { + showDeleteMemberMessagesAlert(mem) + } label: { + Label("Delete member messages", systemImage: "trash") + .foregroundColor(.red) + } + } + + func showDeleteMemberMessagesAlert(_ mem: GroupMember) { + showAlert( + NSLocalizedString("Delete member messages?", comment: "alert title"), + message: NSLocalizedString("Member messages will be deleted - this cannot be undone!", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Delete messages", comment: "alert action"), style: .destructive) { _ in + removeMember(groupInfo, mem, withMessages: true, dismiss: dismiss) + }, + cancelAlertAction + ]} ) } @@ -647,14 +679,16 @@ struct GroupMemberInfoView: View { 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) } @@ -806,12 +840,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))") } } } @@ -821,7 +857,8 @@ struct GroupMemberInfoView_Previews: PreviewProvider { GroupMemberInfoView( groupInfo: GroupInfo.sampleData, chat: Chat.sampleData, - groupMember: GMember.sampleData + groupMember: GMember.sampleData, + scrollToItemId: Binding.constant(nil) ) } } 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..cdbed7fe30 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -0,0 +1,271 @@ +// +// 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 im: ItemsModel + 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(theme.colors.background) + + 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 + } + } + + func contextMemberFilter(_ member: GroupMember) -> Bool { + switch im.secondaryIMFilter { + case nil: + return true + case let .groupChatScopeContext(groupScopeInfo): + switch (groupScopeInfo) { + case let .memberSupport(groupMember_): + if let scopeMember = groupMember_ { + return member.memberRole >= .moderator || member.groupMemberId == scopeMember.groupMemberId + } else { + return member.memberRole >= .moderator + } + case .reports: + return false + } + case .msgContentTagContext: + return false + } + } + + private func filteredMembers() -> [GMember] { + let s = mentionName.lowercased() + return sortedMembers.filter { + contextMemberFilter($0.wrapped) + && (s.isEmpty || $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 9ef53258aa..55b1dc6d2e 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -30,6 +30,14 @@ struct GroupPreferencesView: View { let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members" VStack { List { + Section { + MemberAdmissionButton( + groupInfo: $groupInfo, + admission: groupInfo.groupProfile.memberAdmission_, + currentAdmission: groupInfo.groupProfile.memberAdmission_, + creatingGroup: creatingGroup + ) + } featureSection(.timedMessages, $preferences.timedMessages.enable) featureSection(.fullDelete, $preferences.fullDelete.enable) featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role) @@ -37,6 +45,7 @@ struct GroupPreferencesView: View { featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) featureSection(.files, $preferences.files.enable, $preferences.files.role) featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) + featureSection(.reports, $preferences.reports.enable) featureSection(.history, $preferences.history.enable) if groupInfo.isOwner { @@ -89,6 +98,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, @@ -138,6 +148,66 @@ struct GroupPreferencesView: View { } } +struct MemberAdmissionButton: View { + @Binding var groupInfo: GroupInfo + @State var admission: GroupMemberAdmission + @State var currentAdmission: GroupMemberAdmission + var creatingGroup: Bool = false + + var body: some View { + NavigationLink { + MemberAdmissionView( + groupInfo: $groupInfo, + admission: $admission, + currentAdmission: currentAdmission, + creatingGroup: creatingGroup, + saveAdmission: saveAdmission + ) + .navigationBarTitle("Member admission") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + .onDisappear { + let saveText = NSLocalizedString( + creatingGroup ? "Save" : "Save and notify group members", + comment: "alert button" + ) + + if groupInfo.groupProfile.memberAdmission_ != admission { + showAlert( + title: NSLocalizedString("Save admission settings?", comment: "alert title"), + buttonTitle: saveText, + buttonAction: { saveAdmission() }, + cancelButton: true + ) + } + } + } label: { + if creatingGroup { + Text("Set member admission") + } else { + Label("Member admission", systemImage: "switch.2") + } + } + } + + private func saveAdmission() { + Task { + do { + var gp = groupInfo.groupProfile + gp.memberAdmission = admission + let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp) + await MainActor.run { + groupInfo = gInfo + ChatModel.shared.updateGroup(gInfo) + currentAdmission = admission + } + } catch { + logger.error("MemberAdmissionView apiUpdateGroup error: \(responseError(error))") + } + } + } +} + struct GroupPreferencesView_Previews: PreviewProvider { static var previews: some View { GroupPreferencesView( diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 1617edd11f..69587c0152 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -26,6 +26,8 @@ struct GroupProfileView: View { @Environment(\.dismiss) var dismiss: DismissAction @Binding var groupInfo: GroupInfo @State var groupProfile: GroupProfile + @State private var shortDescr: String = "" + @State private var currentProfileHash: Int? @State private var showChooseSource = false @State private var showImagePicker = false @State private var showTakePhoto = false @@ -34,60 +36,54 @@ struct GroupProfileView: View { @FocusState private var focusDisplayName var body: some View { - return VStack(alignment: .leading) { - Text("Group profile is stored on members' devices, not on the servers.") - .padding(.vertical) + List { + EditProfileImage(profileImage: $groupProfile.image, showChooseSource: $showChooseSource) + .if(!focusDisplayName) { $0.padding(.top) } - ZStack(alignment: .center) { - ZStack(alignment: .topTrailing) { - profileImageView(groupProfile.image) - if groupProfile.image != nil { - Button { - groupProfile.image = nil - } label: { - Image(systemName: "multiply") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12) - } - } - } - - editImageButton { showChooseSource = true } - } - .frame(maxWidth: .infinity, alignment: .center) - - VStack(alignment: .leading) { - ZStack(alignment: .topLeading) { - if !validNewProfileName() { + Section { + HStack { + TextField("Group display name", text: $groupProfile.displayName) + .focused($focusDisplayName) + if !validNewProfileName { Button { alert = .invalidName(validName: mkValidName(groupProfile.displayName)) } label: { Image(systemName: "exclamationmark.circle").foregroundColor(.red) } - } else { - Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - profileNameTextEdit("Group display name", $groupProfile.displayName) - .focused($focusDisplayName) } - .padding(.bottom) let fullName = groupInfo.groupProfile.fullName if fullName != "" && fullName != groupProfile.displayName { - profileNameTextEdit("Group full name (optional)", $groupProfile.fullName) - .padding(.bottom) + TextField("Group full name (optional)", text: $groupProfile.fullName) } - HStack(spacing: 20) { - Button("Cancel") { dismiss() } - Button("Save group profile") { saveProfile() } - .disabled(!canUpdateProfile()) + HStack { + TextField("Short description", text: $shortDescr) + if !shortDescrFitsLimit() { + Button { + showAlert(NSLocalizedString("Description too large", comment: "alert title")) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } } + } footer: { + Text("Group profile is stored on members' devices, not on the servers.") } - .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) + Section { + Button("Reset") { + groupProfile = groupInfo.groupProfile + shortDescr = groupInfo.groupProfile.shortDescr ?? "" + currentProfileHash = groupProfile.hashValue + } + .disabled( + currentProfileHash == groupProfile.hashValue && + (groupInfo.groupProfile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces) + ) + Button("Save group profile", action: saveProfile) + .disabled(!canUpdateProfile) + } } - .padding() - .frame(maxHeight: .infinity, alignment: .top) .confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true @@ -95,6 +91,11 @@ struct GroupProfileView: View { Button("Choose from library") { showImagePicker = true } + if UIPasteboard.general.hasImages { + Button("Paste image") { + chosenImage = UIPasteboard.general.image + } + } } .fullScreenCover(isPresented: $showTakePhoto) { ZStack { @@ -120,8 +121,21 @@ struct GroupProfileView: View { } } .onAppear { + currentProfileHash = groupProfile.hashValue + shortDescr = groupInfo.groupProfile.shortDescr ?? "" DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - focusDisplayName = true + withAnimation { focusDisplayName = true } + } + } + .onDisappear { + if canUpdateProfile { + showAlert( + title: NSLocalizedString("Save group profile?", comment: "alert title"), + message: NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save (and notify members)", comment: "alert button"), + buttonAction: saveProfile, + cancelButton: true + ) } } .alert(item: $alert) { a in @@ -135,30 +149,39 @@ struct GroupProfileView: View { return createInvalidNameAlert(name, $groupProfile.displayName) } } - .contentShape(Rectangle()) - .onTapGesture { hideKeyboard() } + .navigationBarTitle("Group profile") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(focusDisplayName ? .inline : .large) } - private func canUpdateProfile() -> Bool { - groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName() + private var canUpdateProfile: Bool { + ( + currentProfileHash != groupProfile.hashValue || + (groupProfile.shortDescr ?? "") != shortDescr.trimmingCharacters(in: .whitespaces) + ) && + groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && + validNewProfileName && + shortDescrFitsLimit() } - private func validNewProfileName() -> Bool { + private var validNewProfileName: Bool { groupProfile.displayName == groupInfo.groupProfile.displayName || validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces)) } - func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View { - TextField(label, text: name) - .padding(.leading, 32) + private func shortDescrFitsLimit() -> Bool { + chatJsonLength(shortDescr) <= MAX_BIO_LENGTH_BYTES } func saveProfile() { Task { do { groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces) + groupProfile.fullName = groupProfile.fullName.trimmingCharacters(in: .whitespaces) + groupProfile.shortDescr = shortDescr.trimmingCharacters(in: .whitespaces) let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile) await MainActor.run { + currentProfileHash = groupProfile.hashValue groupInfo = gInfo chatModel.updateGroup(gInfo) dismiss() @@ -174,6 +197,9 @@ struct GroupProfileView: View { struct GroupProfileView_Previews: PreviewProvider { static var previews: some View { - GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData) + GroupProfileView( + groupInfo: Binding.constant(GroupInfo.sampleData), + groupProfile: GroupProfile.sampleData + ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index 8dfc32f6ea..f58f2c213d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -18,6 +18,7 @@ struct GroupWelcomeView: View { @State private var editMode = true @FocusState private var keyboardVisible: Bool @State private var showSaveDialog = false + @State private var showSecrets: Set = [] let maxByteCount = 1200 @@ -58,7 +59,8 @@ struct GroupWelcomeView: View { } private func textPreview() -> some View { - messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false, secondaryColor: theme.colors.secondary) + let r = markdownText(welcomeText, showSecrets: showSecrets, backgroundColor: theme.colors.background) + return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) .frame(minHeight: 130, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .leading) } @@ -155,6 +157,9 @@ struct GroupWelcomeView: View { struct GroupWelcomeView_Previews: PreviewProvider { static var previews: some View { - GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData) + GroupProfileView( + groupInfo: Binding.constant(GroupInfo.sampleData), + groupProfile: GroupProfile.sampleData + ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift b/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift new file mode 100644 index 0000000000..d80615b5d2 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift @@ -0,0 +1,93 @@ +// +// MemberAdmissionView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.04.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private let memberCriterias: [(criteria: MemberCriteria?, text: LocalizedStringKey)] = [ + (nil, "off"), + (.all, "all") +] + +struct MemberAdmissionView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var groupInfo: GroupInfo + @Binding var admission: GroupMemberAdmission + var currentAdmission: GroupMemberAdmission + let creatingGroup: Bool + let saveAdmission: () -> Void + @State private var showSaveDialogue = false + + var body: some View { + let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members" + VStack { + List { + admissionSection( + NSLocalizedString("Review members", comment: "admission stage"), + NSLocalizedString("Review members before admitting (\"knocking\").", comment: "admission stage description"), + $admission.review + ) + + if groupInfo.isOwner { + Section { + Button("Reset") { admission = currentAdmission } + Button(saveText) { saveAdmission() } + } + .disabled(currentAdmission == admission) + } + } + } + .modifier(BackButton(disabled: Binding.constant(false)) { + if currentAdmission == admission { + dismiss() + } else { + showSaveDialogue = true + } + }) + .confirmationDialog("Save admission settings?", isPresented: $showSaveDialogue) { + Button(saveText) { + saveAdmission() + dismiss() + } + Button("Exit without saving") { + admission = currentAdmission + dismiss() + } + } + } + + private func admissionSection(_ admissionStageStr: String, _ admissionStageDescrStr: String, _ memberCriteria: Binding) -> some View { + Section { + if groupInfo.isOwner { + Picker(admissionStageStr, selection: memberCriteria) { + ForEach(memberCriterias, id: \.criteria) { mc in + Text(mc.text) + } + } + .frame(height: 36) + } else { + infoRow(Text(admissionStageStr), memberCriteria.wrappedValue?.text ?? NSLocalizedString("off", comment: "member criteria value")) + } + } footer: { + Text(admissionStageDescrStr) + .foregroundColor(theme.colors.secondary) + } + } +} + +#Preview { + MemberAdmissionView( + groupInfo: Binding.constant(GroupInfo.sampleData), + admission: Binding.constant(GroupMemberAdmission.sampleData), + currentAdmission: GroupMemberAdmission.sampleData, + creatingGroup: false, + saveAdmission: {} + ) +} diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift new file mode 100644 index 0000000000..23001e64bf --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift @@ -0,0 +1,44 @@ +// +// MemberSupportChatToolbar.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 01.05.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct MemberSupportChatToolbar: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + var groupMember: GroupMember + var imageSize: CGFloat = 32 + + var body: some View { + return HStack { + MemberProfileImage(groupMember, size: imageSize) + .padding(.trailing, 4) + let t = Text(groupMember.chatViewName).font(.headline) + (groupMember.verified ? memberVerifiedShield + t : t) + .lineLimit(1) + } + .foregroundColor(theme.colors.onBackground) + .frame(width: 220) + } + + private var memberVerifiedShield: Text { + (Text(Image(systemName: "checkmark.shield")) + textSpace) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .baselineOffset(1) + .kerning(-2) + } +} + +#Preview { + MemberSupportChatToolbar( + groupMember: GroupMember.sampleData + ) + .environmentObject(CurrentColors.toAppTheme()) +} diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift new file mode 100644 index 0000000000..75a6840c4e --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift @@ -0,0 +1,297 @@ +// +// MemberSupportView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.04.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct MemberSupportView: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var searchText: String = "" + @FocusState private var searchFocussed + var groupInfo: GroupInfo + @Binding var scrollToItemId: ChatItem.ID? + + var body: some View { + viewBody() + .onAppear { + Task { + await chatModel.loadGroupMembers(groupInfo) + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + Task { + await chatModel.loadGroupMembers(groupInfo) + } + } label: { + Image(systemName: "arrow.clockwise") + } + } + } + } + + @ViewBuilder private func viewBody() -> some View { + let membersWithChats = sortedMembersWithChats() + let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase + let filteredMembersWithChats = s == "" + ? membersWithChats + : membersWithChats.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } + + if membersWithChats.isEmpty { + Text("No chats with members") + .foregroundColor(.secondary) + } else { + List { + searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary) + .padding(.leading, 8) + ForEach(filteredMembersWithChats) { memberWithChat in + MemberSupportChatNavLink( + groupInfo: groupInfo, + memberWithChat: memberWithChat, + scrollToItemId: $scrollToItemId + ) + } + } + } + } + + struct MemberSupportChatNavLink: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var memberSupportChatNavLinkActive = false + var groupInfo: GroupInfo + var memberWithChat: GMember + @Binding var scrollToItemId: ChatItem.ID? + + var body: some View { + ZStack { + let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: memberWithChat.wrapped) + Button { + ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo)) { + memberSupportChatNavLinkActive = true + } + } label: { + SupportChatRowView(groupMember: memberWithChat, groupInfo: groupInfo) + } + + NavigationLink(isActive: $memberSupportChatNavLinkActive) { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + .if(!memberWithChat.wrapped.memberPending && memberWithChat.wrapped.supportChatNotRead) { v in + v.swipeActions(edge: .leading, allowsFullSwipe: true) { + Button { + Task { await markSupportChatRead(groupInfo, memberWithChat.wrapped) } + } label: { + Label("Read", systemImage: "checkmark") + } + .tint(theme.colors.primary) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + if memberWithChat.wrapped.memberPending { + Button { + showAcceptMemberAlert(groupInfo, memberWithChat.wrapped) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + } else { + Button { + showDeleteMemberSupportChatAlert(groupInfo, memberWithChat.wrapped) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + } + } + } + + func sortedMembersWithChats() -> [GMember] { + chatModel.groupMembers + .filter { + $0.wrapped.supportChat != nil && + $0.wrapped.memberStatus != .memLeft && + $0.wrapped.memberStatus != .memRemoved + } + .sorted { (m0: GMember, m1: GMember) -> Bool in + if m0.wrapped.memberPending != m1.wrapped.memberPending { + return m0.wrapped.memberPending + } + + let mentions0 = (m0.wrapped.supportChat?.mentions ?? 0) > 0 + let mentions1 = (m1.wrapped.supportChat?.mentions ?? 0) > 0 + if mentions0 != mentions1 { + return mentions0 + } + + let attention0 = (m0.wrapped.supportChat?.memberAttention ?? 0) > 0 + let attention1 = (m1.wrapped.supportChat?.memberAttention ?? 0) > 0 + if attention0 != attention1 { + return attention0 + } + + let unread0 = (m0.wrapped.supportChat?.unread ?? 0) > 0 + let unread1 = (m1.wrapped.supportChat?.unread ?? 0) > 0 + if unread0 != unread1 { + return unread0 + } + + return (m0.wrapped.supportChat?.chatTs ?? .distantPast) > (m1.wrapped.supportChat?.chatTs ?? .distantPast) + } + } + + private struct SupportChatRowView: View { + @EnvironmentObject var chatModel: ChatModel + @ObservedObject var groupMember: GMember + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + var groupInfo: GroupInfo + + var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize } + + var body: some View { + let member = groupMember.wrapped + HStack{ + MemberProfileImage(member, size: 38) + .padding(.trailing, 2) + VStack(alignment: .leading) { + let t = Text(member.chatViewName).foregroundColor(theme.colors.onBackground) + (member.verified ? memberVerifiedShield + t : t) + .lineLimit(1) + Text(memberStatus(member)) + .lineLimit(1) + .font(.caption) + .foregroundColor(theme.colors.secondary) + } + + Spacer() + + if member.memberPending { + Image(systemName: "flag.fill") + .resizable() + .scaledToFill() + .frame(width: dynamicChatInfoSize * 0.8, height: dynamicChatInfoSize * 0.8) + .foregroundColor(theme.colors.primary) + } + if let supportChat = member.supportChat { + SupportChatUnreadIndicator(supportChat: supportChat) + } + } + } + + private func memberStatus(_ member: GroupMember) -> LocalizedStringKey { + if member.activeConn?.connDisabled ?? false { + return "disabled" + } else if member.activeConn?.connInactive ?? false { + return "inactive" + } else if member.memberPending { + return member.memberStatus.text + } else { + return LocalizedStringKey(member.memberRole.text) + } + } + + struct SupportChatUnreadIndicator: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + var supportChat: GroupSupportChat + + var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize } + + private var indicatorTint: Color { + if supportChat.mentions > 0 || supportChat.memberAttention > 0 { + return theme.colors.primary + } else { + return theme.colors.secondary + } + } + + var body: some View { + HStack(alignment: .center, spacing: 2) { + if supportChat.unread > 0 || supportChat.mentions > 0 || supportChat.memberAttention > 0 { + if supportChat.mentions > 0 && supportChat.unread > 1 { + Text("\(MENTION_START)") + .font(userFont <= .xxxLarge ? .body : .callout) + .foregroundColor(indicatorTint) + .frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize) + .cornerRadius(dynamicSize(userFont).unreadCorner) + .padding(.bottom, 1) + } + let singleUnreadIsMention = supportChat.mentions > 0 && supportChat.unread == 1 + (singleUnreadIsMention ? Text("\(MENTION_START)") : unreadCountText(supportChat.unread)) + .font(userFont <= .xxxLarge ? .caption : .caption2) + .foregroundColor(.white) + .padding(.horizontal, dynamicSize(userFont).unreadPadding) + .frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize) + .background(indicatorTint) + .cornerRadius(dynamicSize(userFont).unreadCorner) + } + } + .frame(height: dynamicChatInfoSize) + .frame(minWidth: 22) + } + } + + private var memberVerifiedShield: Text { + (Text(Image(systemName: "checkmark.shield")) + textSpace) + .font(.caption) + .baselineOffset(2) + .kerning(-2) + .foregroundColor(theme.colors.secondary) + } + } +} + +func showDeleteMemberSupportChatAlert(_ groupInfo: GroupInfo, _ member: GroupMember) { + showAlert( + title: NSLocalizedString("Delete chat with member?", comment: "alert title"), + buttonTitle: "Delete", + buttonAction: { deleteMemberSupportChat(groupInfo, member) }, + cancelButton: true + ) +} + +func deleteMemberSupportChat(_ groupInfo: GroupInfo, _ member: GroupMember) { + Task { + do { + let (gInfo, updatedMember) = try await apiDeleteMemberSupportChat(groupInfo.groupId, member.groupMemberId) + await MainActor.run { + _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + ChatModel.shared.updateGroup(gInfo) + } + // TODO member row doesn't get removed from list (upsertGroupMember correctly sets supportChat to nil) - this repopulates list to fix it + await ChatModel.shared.loadGroupMembers(gInfo) + } catch let error { + logger.error("apiDeleteMemberSupportChat error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error deleting chat", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + +#Preview { + MemberSupportView( + groupInfo: GroupInfo.sampleData, + scrollToItemId: Binding.constant(nil) + ) +} diff --git a/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift new file mode 100644 index 0000000000..e2092f7a24 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift @@ -0,0 +1,44 @@ +// +// SecondaryChatView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 29.04.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct SecondaryChatView: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var chatModel: ChatModel + @ObservedObject var chat: Chat + @Binding var scrollToItemId: ChatItem.ID? + + var body: some View { + if let im = chatModel.secondaryIM { + ChatView( + chat: chat, + im: im, + mergedItems: BoxedValue(MergedItems.create(im, [])), + floatingButtonModel: FloatingButtonModel(im: im), + scrollToItemId: $scrollToItemId + ) + .modifier(BackButton(disabled: Binding.constant(false)) { + chatModel.secondaryIM = nil + dismiss() + }) + } + } +} + +#Preview { + SecondaryChatView( + chat: Chat( + chatInfo: .group(groupInfo: GroupInfo.sampleData, groupChatScope: .memberSupport(groupMember_: GroupMember.sampleData)), + chatItems: [], + chatStats: ChatStats() + ), + scrollToItemId: Binding.constant(nil) + ) +} diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift deleted file mode 100644 index e33adcef58..0000000000 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ /dev/null @@ -1,371 +0,0 @@ -// -// ReverseList.swift -// SimpleX (iOS) -// -// Created by Levitating Pineapple on 11/06/2024. -// Copyright © 2024 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import Combine -import SimpleXChat - -/// A List, which displays it's items in reverse order - from bottom to top -struct ReverseList: UIViewControllerRepresentable { - let items: Array - - @Binding var scrollState: ReverseListScrollModel.State - - /// Closure, that returns user interface for a given item - let content: (ChatItem) -> Content - - let loadPage: () -> Void - - func makeUIViewController(context: Context) -> Controller { - Controller(representer: self) - } - - func updateUIViewController(_ controller: Controller, context: Context) { - controller.representer = self - if case let .scrollingTo(destination) = scrollState, !items.isEmpty { - controller.view.layer.removeAllAnimations() - switch destination { - case .nextPage: - controller.scrollToNextPage() - case let .item(id): - controller.scroll(to: items.firstIndex(where: { $0.id == id }), position: .bottom) - case .bottom: - controller.scroll(to: 0, position: .top) - } - } else { - controller.update(items: items) - } - } - - /// Controller, which hosts SwiftUI cells - class Controller: UITableViewController { - private enum Section { case main } - var representer: ReverseList - private var dataSource: UITableViewDiffableDataSource! - private var itemCount: Int = 0 - private let updateFloatingButtons = PassthroughSubject() - private var bag = Set() - - init(representer: ReverseList) { - self.representer = representer - super.init(style: .plain) - - // 1. Style - tableView = InvertedTableView() - tableView.separatorStyle = .none - tableView.transform = .verticalFlip - tableView.backgroundColor = .clear - - // 2. Register cells - if #available(iOS 16.0, *) { - tableView.register( - UITableViewCell.self, - forCellReuseIdentifier: cellReuseId - ) - } else { - tableView.register( - HostingCell.self, - forCellReuseIdentifier: cellReuseId - ) - } - - // 3. Configure data source - self.dataSource = UITableViewDiffableDataSource( - tableView: tableView - ) { (tableView, indexPath, item) -> UITableViewCell? in - if indexPath.item > self.itemCount - 8 { - self.representer.loadPage() - } - let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) - if #available(iOS 16.0, *) { - cell.contentConfiguration = UIHostingConfiguration { self.representer.content(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.representer.content(item), parent: self) - } else { - fatalError("Unexpected Cell Type for: \(item)") - } - } - cell.transform = .verticalFlip - cell.selectionStyle = .none - cell.backgroundColor = .clear - return cell - } - - // 4. External state changes will require manual layout updates - NotificationCenter.default - .addObserver( - self, - selector: #selector(updateLayout), - name: notificationName, - object: nil - ) - - updateFloatingButtons - .throttle(for: 0.2, scheduler: DispatchQueue.global(qos: .background), latest: true) - .sink { - if let listState = DispatchQueue.main.sync(execute: { [weak self] in self?.getListState() }) { - ChatView.FloatingButtonModel.shared.updateOnListChange(listState) - } - } - .store(in: &bag) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError() } - - deinit { NotificationCenter.default.removeObserver(self) } - - @objc private func updateLayout() { - if #available(iOS 16.0, *) { - tableView.setNeedsLayout() - tableView.layoutIfNeeded() - } else { - tableView.reloadData() - } - } - - /// Hides keyboard, when user begins to scroll. - /// Equivalent to `.scrollDismissesKeyboard(.immediately)` - override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - UIApplication.shared - .sendAction( - #selector(UIResponder.resignFirstResponder), - to: nil, - from: nil, - for: nil - ) - NotificationCenter.default.post(name: .chatViewWillBeginScrolling, object: nil) - } - - override func viewDidAppear(_ animated: Bool) { - tableView.clipsToBounds = false - parent?.viewIfLoaded?.clipsToBounds = false - } - - /// Scrolls up - func scrollToNextPage() { - tableView.setContentOffset( - CGPoint( - x: tableView.contentOffset.x, - y: tableView.contentOffset.y + tableView.bounds.height - ), - animated: true - ) - Task { representer.scrollState = .atDestination } - } - - /// Scrolls to Item at index path - /// - Parameter indexPath: Item to scroll to - will scroll to beginning of the list, if `nil` - func scroll(to index: Int?, position: UITableView.ScrollPosition) { - var animated = false - if #available(iOS 16.0, *) { - animated = true - } - if let index, tableView.numberOfRows(inSection: 0) != 0 { - tableView.scrollToRow( - at: IndexPath(row: index, section: 0), - at: position, - animated: animated - ) - } else { - tableView.setContentOffset( - CGPoint(x: .zero, y: -InvertedTableView.inset), - animated: animated - ) - } - Task { representer.scrollState = .atDestination } - } - - func update(items: [ChatItem]) { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(items) - dataSource.defaultRowAnimation = .none - dataSource.apply( - snapshot, - animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1 - ) - // Sets content offset on initial load - if itemCount == 0 { - tableView.setContentOffset( - CGPoint(x: 0, y: -InvertedTableView.inset), - animated: false - ) - } - itemCount = items.count - updateFloatingButtons.send() - } - - override func scrollViewDidScroll(_ scrollView: UIScrollView) { - updateFloatingButtons.send() - } - - func getListState() -> ListState? { - if let visibleRows = tableView.indexPathsForVisibleRows, - visibleRows.last?.item ?? 0 < representer.items.count { - let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset - let topItemDate: Date? = - if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) { - representer.items[lastVisible.item].meta.itemTs - } else { - nil - } - let bottomItemId: ChatItem.ID? = - if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }) { - representer.items[firstVisible.item].id - } else { - nil - } - return (scrollOffset: scrollOffset, topItemDate: topItemDate, bottomItemId: bottomItemId) - } - return nil - } - - private func isVisible(indexPath: IndexPath) -> Bool { - if let relativeFrame = tableView.superview?.convert( - tableView.rectForRow(at: indexPath), - from: tableView - ) { - relativeFrame.maxY > InvertedTableView.inset && - relativeFrame.minY < tableView.frame.height - InvertedTableView.inset - } else { false } - } - } - - /// `UIHostingConfiguration` back-port for iOS14 and iOS15 - /// Implemented as a `UITableViewCell` that wraps and manages a generic `UIHostingController` - private final class HostingCell: UITableViewCell { - 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 !contentView.subviews.contains(hostingController.view) { - contentView.addSubview(hostingController.view) - hostingView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - hostingView.leadingAnchor - .constraint(equalTo: contentView.leadingAnchor), - hostingView.trailingAnchor - .constraint(equalTo: contentView.trailingAnchor), - hostingView.topAnchor - .constraint(equalTo: contentView.topAnchor), - hostingView.bottomAnchor - .constraint(equalTo: contentView.bottomAnchor) - ]) - } - if hostingController.parent != parent { hostingController.didMove(toParent: parent) } - } else { - fatalError("Hosting View not loaded \(hostingController)") - } - } - - override func prepareForReuse() { - super.prepareForReuse() - hostingController.rootView = nil - } - } -} - -typealias ListState = ( - scrollOffset: Double, - topItemDate: Date?, - bottomItemId: ChatItem.ID? -) - -/// Manages ``ReverseList`` scrolling -class ReverseListScrollModel: ObservableObject { - /// Represents Scroll State of ``ReverseList`` - enum State: Equatable { - enum Destination: Equatable { - case nextPage - case item(ChatItem.ID) - case bottom - } - - case scrollingTo(Destination) - case atDestination - } - - @Published var state: State = .atDestination - - func scrollToNextPage() { - state = .scrollingTo(.nextPage) - } - - func scrollToBottom() { - state = .scrollingTo(.bottom) - } - - func scrollToItem(id: ChatItem.ID) { - state = .scrollingTo(.item(id)) - } -} - -fileprivate let cellReuseId = "hostingCell" - -fileprivate let notificationName = NSNotification.Name(rawValue: "reverseListNeedsLayout") - -fileprivate extension CGAffineTransform { - /// Transform that vertically flips the view, preserving it's location - static let verticalFlip = CGAffineTransform(scaleX: 1, y: -1) -} - -extension NotificationCenter { - static func postReverseListNeedsLayout() { - NotificationCenter.default.post( - name: notificationName, - object: 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() - } -} - -class InvertedTableView: UITableView { - static let inset = CGFloat(100) - - static let insets = UIEdgeInsets( - top: inset, - left: .zero, - bottom: inset, - right: .zero - ) - - override var contentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior { - get { .never } - set { } - } - - override var contentInset: UIEdgeInsets { - get { Self.insets } - set { } - } - - override var adjustedContentInset: UIEdgeInsets { - Self.insets - } -} 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 index 81498ee497..4855c3ca8d 100644 --- a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift +++ b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift @@ -25,17 +25,20 @@ struct SelectedItemsTopToolbar: View { struct SelectedItemsBottomToolbar: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var theme: AppTheme - let chatItems: [ChatItem] + let im: ItemsModel @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 @@ -50,7 +53,11 @@ struct SelectedItemsBottomToolbar: View { HStack(alignment: .center) { Button { - deleteItems(deleteForEveryoneEnabled) + if canArchiveReports { + archiveItems() + } else { + deleteItems(deleteForEveryoneEnabled) + } } label: { Image(systemName: "trash") .resizable() @@ -68,9 +75,9 @@ struct SelectedItemsBottomToolbar: View { .resizable() .scaledToFit() .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red) + .foregroundColor(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : .red) } - .disabled(!moderateEnabled || deleteCountProhibited) + .disabled(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil) .opacity(canModerate ? 1 : 0) Spacer() @@ -81,24 +88,24 @@ struct SelectedItemsBottomToolbar: View { .resizable() .scaledToFit() .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary) + .foregroundColor(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : theme.colors.primary) } - .disabled(!forwardEnabled || forwardCountProhibited) + .disabled(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil) } .frame(maxHeight: .infinity) .padding([.leading, .trailing], 12) } .onAppear { - recheckItems(chatInfo, chatItems, selectedChatItems) + recheckItems(chatInfo, im.reversedChatItems, selectedChatItems) } .onChange(of: chatInfo) { info in - recheckItems(info, chatItems, selectedChatItems) + recheckItems(info, im.reversedChatItems, selectedChatItems) } - .onChange(of: chatItems) { items in + .onChange(of: im.reversedChatItems) { items in recheckItems(chatInfo, items, selectedChatItems) } .onChange(of: selectedChatItems) { selected in - recheckItems(chatInfo, chatItems, selected) + recheckItems(chatInfo, im.reversedChatItems, selected) } .frame(height: 55.5) .background(.thinMaterial) @@ -109,19 +116,25 @@ struct SelectedItemsBottomToolbar: View { 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, me, onlyOwnGroupItems, forwardEnabled, selectedChatItems) = chatItems.reduce((true, true, true, true, true, [])) { (r, ci) in + (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, me, onlyOwnGroupItems, fe, sel) = r + 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, me, onlyOwnGroupItems, fe, sel) + return (de, dee, ar, me, onlyOwnGroupItems, fe, sel) } else { return r } @@ -132,8 +145,8 @@ struct SelectedItemsBottomToolbar: View { private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool { return switch chatInfo { - case let .group(groupInfo): - groupInfo.membership.memberRole >= .admin + case let .group(groupInfo, _): + groupInfo.membership.memberRole >= .moderator default: false } } diff --git a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift index 7b01fe0300..373311073a 100644 --- a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift +++ b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift @@ -24,85 +24,70 @@ struct VerifyCodeView: View { } private func verifyCodeView(_ code: String) -> some View { - ScrollView { - let splitCode = splitToParts(code, length: 24) - VStack(alignment: .leading) { - Group { + let splitCode = splitToParts(code, length: 24) + return List { + Section { + QRCode(uri: code, small: true) + + Text(splitCode) + .multilineTextAlignment(.leading) + .font(.body.monospaced()) + .lineLimit(20) + .frame(maxWidth: .infinity, alignment: .center) + } header: { + if connectionVerified { HStack { - if connectionVerified { - Image(systemName: "checkmark.shield") - .foregroundColor(theme.colors.secondary) - Text("\(displayName) is verified") - } else { - Text("\(displayName) is not verified") - } + Image(systemName: "checkmark.shield").foregroundColor(theme.colors.secondary) + Text("\(displayName) is verified").textCase(.none) } - .frame(height: 24) - - QRCode(uri: code) - .padding(.horizontal) - - Text(splitCode) - .multilineTextAlignment(.leading) - .font(.body.monospaced()) - .lineLimit(20) - .padding(.bottom, 8) + } else { + Text("\(displayName) is not verified").textCase(.none) } - .frame(maxWidth: .infinity, alignment: .center) - + } footer: { Text("To verify end-to-end encryption with your contact compare (or scan) the code on your devices.") - .padding(.bottom) + } - Group { - if connectionVerified { - Button { - verifyCode(nil) - } label: { - Label("Clear verification", systemImage: "shield") - } - .padding() - } else { - HStack { - NavigationLink { - ScanCodeView(connectionVerified: $connectionVerified, verify: verify) - .navigationBarTitleDisplayMode(.large) - .navigationTitle("Scan code") - .modifier(ThemedBackground()) - } label: { - Label("Scan code", systemImage: "qrcode") - } - .padding() - Button { - verifyCode(code) { verified in - if !verified { showCodeError = true } - } - } label: { - Label("Mark verified", systemImage: "checkmark.shield") - } - .padding() - .alert(isPresented: $showCodeError) { - Alert(title: Text("Incorrect security code!")) - } - } - } - } - .frame(maxWidth: .infinity, alignment: .center) - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { + Section { + if connectionVerified { Button { - showShareSheet(items: [splitCode]) + verifyCode(nil) } label: { - Image(systemName: "square.and.arrow.up") + Label("Clear verification", systemImage: "shield") + } + } else { + NavigationLink { + ScanCodeView(connectionVerified: $connectionVerified, verify: verify) + .navigationBarTitleDisplayMode(.large) + .navigationTitle("Scan code") + .modifier(ThemedBackground()) + } label: { + Label("Scan code", systemImage: "qrcode") + } + Button { + verifyCode(code) { verified in + if !verified { showCodeError = true } + } + } label: { + Label("Mark verified", systemImage: "checkmark.shield") + } + .alert(isPresented: $showCodeError) { + Alert(title: Text("Incorrect security code!")) } } } - .onChange(of: connectionVerified) { _ in - if connectionVerified { dismiss() } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showShareSheet(items: [splitCode]) + } label: { + Image(systemName: "square.and.arrow.up") + } } } + .onChange(of: connectionVerified) { _ in + if connectionVerified { dismiss() } + } } private func verifyCode(_ code: String?, _ cb: ((Bool) -> Void)? = nil) { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index f1ee4e4c42..4937bca20e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -66,7 +66,7 @@ struct ChatListNavLink: View { switch chat.chatInfo { case let .direct(contact): contactNavLink(contact) - case let .group(groupInfo): + case let .group(groupInfo, _): groupNavLink(groupInfo) case let .local(noteFolder): noteFolderNavLink(noteFolder) @@ -90,11 +90,11 @@ struct ChatListNavLink: View { .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 && contact.active { + if contact.isContactCard { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) - .frame(height: dynamicRowHeight) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { deleteContactDialog( @@ -121,31 +121,83 @@ struct ChatListNavLink: View { selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) } ) - .swipeActions(edge: .leading, allowsFullSwipe: true) { - markReadButton() - toggleFavoriteButton() - toggleNtfsButton(chat: chat) + .frameCompat(height: dynamicRowHeight) + .if(!contact.nextAcceptContactRequest) { v in + v.swipeActions(edge: .leading, allowsFullSwipe: true) { + markReadButton() + toggleFavoriteButton() + toggleNtfsButton(chat: chat) + } } .swipeActions(edge: .trailing, allowsFullSwipe: true) { - tagChatButton(chat) - if !chat.chatItems.isEmpty { - clearChatButton() + if contact.nextAcceptContactRequest { + if let contactRequestId = contact.contactRequestId { + Button { + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) } + } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } + .tint(theme.colors.primary) + if !ChatModel.shared.addressShortLinkDataSet { + Button { + Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) } + } label: { + SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + } + .tint(.indigo) + } + Button { + AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequestId)) + } label: { + SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply", inverted: oneHandUI) + } + .tint(.red) + } else if let groupDirectInv = contact.groupDirectInv, !groupDirectInv.memberRemoved { + Button { + acceptMemberContactRequest(contact) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + Button { + showRejectMemberContactRequestAlert(contact) + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) + } else { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + deleteLabel + } + .tint(.red) + } + } else { + tagChatButton(chat) + if !chat.chatItems.isEmpty { + clearChatButton() + } + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + deleteLabel + } + .tint(.red) } - Button { - deleteContactDialog( - chat, - contact, - dismissToChatList: false, - showAlert: { alert = $0 }, - showActionSheet: { actionSheet = $0 }, - showSheetContent: { sheet = $0 } - ) - } label: { - deleteLabel - } - .tint(.red) } - .frame(height: dynamicRowHeight) } } .alert(item: $alert) { $0.alert } @@ -163,7 +215,7 @@ struct ChatListNavLink: View { switch (groupInfo.membership.memberStatus) { case .memInvited: ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout) - .frame(height: dynamicRowHeight) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { joinGroupButton() if groupInfo.canDelete { @@ -183,13 +235,13 @@ struct ChatListNavLink: View { .disabled(inProgress) case .memAccepted: ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) - .frame(height: dynamicRowHeight) + .frameCompat(height: dynamicRowHeight) .onTapGesture { AlertManager.shared.showAlert(groupInvitationAcceptedAlert()) } .swipeActions(edge: .trailing) { tagChatButton(chat) - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { leaveGroupChatButton(groupInfo) } if groupInfo.canDelete { @@ -203,7 +255,7 @@ struct ChatListNavLink: View { label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !groupInfo.ready ) - .frame(height: dynamicRowHeight) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() toggleFavoriteButton() @@ -211,37 +263,46 @@ struct ChatListNavLink: View { } .swipeActions(edge: .trailing, allowsFullSwipe: true) { 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 + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0) + let showLeaveGroup = groupInfo.membership.memberCurrentOrPending + let totalNumberOfButtons = 1 + (showReportsButton ? 1 : 0) + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0) - if showClearButton, totalNumberOfButtons <= 3 { + if showClearButton && totalNumberOfButtons <= 3 { clearChatButton() } - if (showLeaveGroup) { + + if showReportsButton && totalNumberOfButtons <= 3 { + archiveAllReportsButton() + } + + if showLeaveGroup { leaveGroupChatButton(groupInfo) } - - if showDeleteGroup { - if totalNumberOfButtons <= 3 { + + if showDeleteGroup && totalNumberOfButtons <= 3 { + deleteGroupChatButton(groupInfo) + } else if totalNumberOfButtons > 3 { + if showDeleteGroup && !groupInfo.membership.memberActive { deleteGroupChatButton(groupInfo) + moreOptionsButton(false, chat, groupInfo) } else { - moreOptionsButton(chat, groupInfo) + moreOptionsButton(true, chat, groupInfo) } } } } } - @ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View { + private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View { NavLinkPlain( chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !noteFolder.ready ) - .frame(height: dynamicRowHeight) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() } @@ -267,7 +328,7 @@ struct ChatListNavLink: View { @ViewBuilder private func markReadButton() -> some View { if chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat { Button { - Task { await markChatRead(chat) } + Task { await markChatRead(ItemsModel.shared, chat) } } label: { SwipeLabel(NSLocalizedString("Read", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } @@ -302,14 +363,22 @@ struct ChatListNavLink: View { } @ViewBuilder private func toggleNtfsButton(chat: Chat) -> some View { - Button { - toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) - } label: { - if chat.chatInfo.ntfsEnabled { - SwipeLabel(NSLocalizedString("Mute", comment: "swipe action"), systemImage: "speaker.slash.fill", inverted: oneHandUI) - } else { - SwipeLabel(NSLocalizedString("Unmute", comment: "swipe action"), systemImage: "speaker.wave.2.fill", inverted: oneHandUI) + 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) } } @@ -354,15 +423,20 @@ struct ChatListNavLink: View { ) } - private func moreOptionsButton(_ chat: Chat, _ groupInfo: GroupInfo?) -> some View { + private func moreOptionsButton(_ canShowGroupDelete: Bool, _ chat: Chat, _ groupInfo: GroupInfo?) -> some View { Button { - var buttons: [Alert.Button] = [ - .default(Text("Clear")) { - AlertManager.shared.showAlert(clearChatAlert()) - } - ] - - if let gi = groupInfo, gi.canDelete { + 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)) }) @@ -372,7 +446,7 @@ struct ChatListNavLink: View { actionSheet = SomeActionSheet( actionSheet: ActionSheet( - title: Text("Clear or delete group?"), + title: canShowGroupDelete ? Text("Clear or delete group?") : Text("Clear group?"), buttons: buttons ), id: "other options" @@ -411,36 +485,41 @@ struct ChatListNavLink: View { 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) } + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } .tint(theme.colors.primary) - Button { - Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } - } label: { - SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + if !ChatModel.shared.addressShortLinkDataSet { + Button { + Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } + } label: { + SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + } + .tint(.indigo) } - .tint(.indigo) Button { - AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest)) + AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest.apiId)) } label: { - SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply.fill", inverted: oneHandUI) + SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply", inverted: oneHandUI) } .tint(.red) } - .frame(height: dynamicRowHeight) .contentShape(Rectangle()) .onTapGesture { showContactRequestDialog = true } .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) } } + Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } } + if !ChatModel.shared.addressShortLinkDataSet { + Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } } + } + Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } } } } 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 @@ -458,14 +537,11 @@ struct ChatListNavLink: View { } .tint(theme.colors.primary) } - .frame(height: dynamicRowHeight) .appSheet(isPresented: $showContactConnectionInfo) { - Group { - if case let .contactConnection(contactConnection) = chat.chatInfo { - ContactConnectionInfo(contactConnection: contactConnection) - .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) - .modifier(ThemedBackground(grouped: true)) - } + if case let .contactConnection(contactConnection) = chat.chatInfo { + ContactConnectionInfo(contactConnection: contactConnection) + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + .modifier(ThemedBackground(grouped: true)) } } .contentShape(Rectangle()) @@ -490,6 +566,27 @@ struct ChatListNavLink: View { ) } + 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 { Alert( title: Text("Clear conversation?"), @@ -536,14 +633,14 @@ struct ChatListNavLink: View { ) } - private func invalidJSONPreview(_ json: String) -> some View { + private func invalidJSONPreview(_ json: Data?) -> some View { Text("invalid chat data") .foregroundColor(.red) .padding(4) - .frame(height: dynamicRowHeight) + .frameCompat(height: dynamicRowHeight) .onTapGesture { showInvalidJSON = true } .appSheet(isPresented: $showInvalidJSON) { - invalidJSONView(json) + invalidJSONView(dataToString(json)) .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) } } @@ -552,19 +649,38 @@ struct ChatListNavLink: View { Task { let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { AlertManager.shared.showAlert($0) }) if ok { - ItemsModel.shared.loadOpenChat(contact.id) - AlertManager.shared.showAlert(connReqSentAlert(.contact)) + ItemsModel.shared.loadOpenChat(contact.id) { + AlertManager.shared.showAlert(connReqSentAlert(.contact)) + } } } } } -func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { +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(_ contactRequestId: Int64) -> Alert { Alert( title: Text("Reject contact request"), message: Text("The sender will NOT be notified"), primaryButton: .destructive(Text("Reject")) { - Task { await rejectContactRequest(contactRequest) } + Task { await rejectContactRequest(contactRequestId) } }, secondaryButton: .cancel() ) @@ -614,16 +730,17 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { Task { logger.debug("joinGroup") do { - let r = try await apiJoinGroup(groupId) - switch r { - case let .joined(groupInfo): - await MainActor.run { ChatModel.shared.updateGroup(groupInfo) } - case .invitationRemoved: - AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.") - await deleteGroup() - case .groupNotFound: - AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") - await deleteGroup() + if let r = try await apiJoinGroup(groupId) { + switch r { + case let .joined(groupInfo): + await MainActor.run { ChatModel.shared.updateGroup(groupInfo) } + case .invitationRemoved: + AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.") + await deleteGroup() + case .groupNotFound: + AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") + await deleteGroup() + } } await onComplete() } catch let error { @@ -645,7 +762,7 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { } func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert { - if let r = error as? ChatResponse, + if let r = error as? ChatError, let alert = getNetworkErrorAlert(r) { return alert } else { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 68e0c57c75..0450bd439c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -137,6 +137,7 @@ struct UserPickerSheetView: View { struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel + @StateObject private var connectProgressManager = ConnectProgressManager.shared @EnvironmentObject var theme: AppTheme @Binding var activeUserPickerSheet: UserPickerSheet? @State private var searchMode = false @@ -148,7 +149,11 @@ struct ChatListView: View { @State private var userPickerShown: Bool = false @State private var sheet: SomeSheet? = nil @StateObject private var chatTagsModel = ChatTagsModel.shared - + @State private var scrollToItemId: ChatItem.ID? = nil + + // 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 @@ -203,7 +208,17 @@ struct ChatListView: View { .navigationBarHidden(searchMode || oneHandUI) } .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) - .onDisappear() { activeUserPickerSheet = nil } + .onAppear { + if #unavailable(iOS 16.0), !viewOnScreen { + viewOnScreen = true + } + } + .onDisappear { + activeUserPickerSheet = nil + if #unavailable(iOS 16.0) { + viewOnScreen = false + } + } .refreshable { AlertManager.shared.showAlert(Alert( title: Text("Reconnect servers?"), @@ -258,7 +273,7 @@ struct ChatListView: View { } } else { if oneHandUI { - content().toolbar { bottomToolbarGroup } + content().toolbar { bottomToolbarGroup() } } else { content().toolbar { topToolbar } } @@ -286,9 +301,9 @@ struct ChatListView: View { } } - @ToolbarContentBuilder var bottomToolbarGroup: some ToolbarContent { + @ToolbarContentBuilder func bottomToolbarGroup() -> some ToolbarContent { let padding: Double = Self.hasHomeIndicator ? 0 : 14 - ToolbarItemGroup(placement: .bottomBar) { + ToolbarItemGroup(placement: viewOnScreen ? .bottomBar : .principal) { leadingToolbarItem.padding(.bottom, padding) Spacer() SubsStatusIndicator().padding(.bottom, padding) @@ -322,9 +337,9 @@ struct ChatListView: View { } } - @ViewBuilder private var chatList: some View { + private var chatList: some View { let cs = filteredChats() - ZStack { + return ZStack { ScrollViewReader { scrollProxy in List { if !chatModel.chats.isEmpty { @@ -354,13 +369,7 @@ struct ChatListView: View { .offset(x: -8) } else { ForEach(cs, id: \.viewId) { chat in - VStack(spacing: .zero) { - Divider() - .padding(.leading, 16) - ChatListNavLink(chat: chat, parentSheet: $sheet) - .padding(.horizontal, 8) - .padding(.vertical, 6) - } + ChatListNavLink(chat: chat, parentSheet: $sheet) .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets()) @@ -439,7 +448,14 @@ struct ChatListView: View { @ViewBuilder private func chatView() -> some View { if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) { - ChatView(chat: chat) + let im = ItemsModel.shared + ChatView( + chat: chat, + im: im, + mergedItems: BoxedValue(MergedItems.create(im, [])), + floatingButtonModel: FloatingButtonModel(im: im), + scrollToItemId: $scrollToItemId + ) } } @@ -480,7 +496,7 @@ struct ChatListView: View { 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.chatStats.unreadChat || chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0 + case .unread: chat.unreadTag case .none: true } } @@ -557,6 +573,7 @@ struct ChatListSearchBar: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @EnvironmentObject var chatTagsModel: ChatTagsModel + @StateObject private var connectProgressManager = ConnectProgressManager.shared @Binding var searchMode: Bool @FocusState.Binding var searchFocussed: Bool @Binding var searchText: String @@ -564,8 +581,6 @@ struct ChatListSearchBar: View { @Binding var searchChatFilteredBySimplexLink: String? @Binding var parentSheet: SomeSheet? @State private var ignoreSearchTextChange = false - @State private var alert: PlanAndConnectAlert? - @State private var sheet: PlanAndConnectActionSheet? var body: some View { VStack(spacing: 12) { @@ -578,6 +593,9 @@ struct ChatListSearchBar: View { .disabled(searchShowingSimplexLink) .focused($searchFocussed) .frame(maxWidth: .infinity) + if connectProgressManager.showConnectProgress != nil { + ProgressView() + } if !searchText.isEmpty { Image(systemName: "xmark.circle.fill") .onTapGesture { @@ -611,7 +629,7 @@ struct ChatListSearchBar: View { } else { if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue searchFocussed = false - if case let .simplexLink(linkType, _, smpHosts) = link.format { + if case let .simplexLink(_, linkType, _, smpHosts) = link.format { ignoreSearchTextChange = true searchText = simplexLinkText(linkType, smpHosts) } @@ -621,6 +639,8 @@ struct ChatListSearchBar: View { } else { if t != "" { // if some other text is pasted, enter search mode searchFocussed = true + } else { + ConnectProgressManager.shared.cancelConnectProgress() } searchShowingSimplexLink = false searchChatFilteredBySimplexLink = nil @@ -630,12 +650,6 @@ struct ChatListSearchBar: View { .onChange(of: chatTagsModel.activeFilter) { _ in searchText = "" } - .alert(item: $alert) { a in - planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" }) - } - .actionSheet(item: $sheet) { s in - planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" }) - } } private func toggleFilterButton() -> some View { @@ -661,10 +675,12 @@ struct ChatListSearchBar: View { private func connect(_ link: String) { planAndConnect( link, - showAlert: { alert = $0 }, - showActionSheet: { sheet = $0 }, + theme: theme, dismiss: false, - incognito: nil, + cleanup: { + searchText = "" + searchFocussed = false + }, filterKnownContact: { searchChatFilteredBySimplexLink = $0.id }, filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id } ) @@ -791,7 +807,7 @@ struct TagsView: View { } } - @ViewBuilder private func expandedPresetTagsFiltersView() -> some View { + private func expandedPresetTagsFiltersView() -> some View { ForEach(PresetTag.allCases, id: \.id) { tag in if (chatTagsModel.presetTags[tag] ?? 0) > 0 { expandedTagFilterView(tag) @@ -882,15 +898,15 @@ func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo, _ chatStats: C chatInfo.chatSettings?.favorite == true case .contacts: switch chatInfo { - case let .direct(contact): !(contact.activeConn == nil && contact.profile.contactLink != nil && contact.active) && !contact.chatDeleted + case let .direct(contact): !contact.isContactCard && !contact.chatDeleted case .contactRequest: true case .contactConnection: true - case let .group(groupInfo): groupInfo.businessChat?.chatType == .customer + case let .group(groupInfo, _): groupInfo.businessChat?.chatType == .customer default: false } case .groups: switch chatInfo { - case let .group(groupInfo): groupInfo.businessChat == nil + case let .group(groupInfo, _): groupInfo.businessChat == nil default: false } case .business: diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 654bb56441..be2c456802 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -24,75 +24,83 @@ struct ChatPreviewView: View { 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: dynamicSize(userFont).profileImageSize) - chatPreviewImageOverlayIcon() - .padding([.bottom, .trailing], 1) - } - .padding(.leading, 4) - - 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) + 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) + .padding(.leading, 4) - ZStack(alignment: .topTrailing) { - let chat = activeContentPreview?.chat ?? chat - let ci = activeContentPreview?.ci ?? chat.chatItems.last - let mc = ci?.content.msgContent + let chatTs = if let cItem { + cItem.meta.itemTs + } else { + chat.chatInfo.chatTs + } + VStack(spacing: 0) { 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) + 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) + } } - 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) } - .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(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 @@ -133,8 +141,9 @@ struct ChatPreviewView: View { } else { EmptyView() } - case let .group(groupInfo): + case let .group(groupInfo, _): switch (groupInfo.membership.memberStatus) { + case .memRejected: inactiveIcon() case .memLeft: inactiveIcon() case .memRemoved: inactiveIcon() case .memGroupDeleted: inactiveIcon() @@ -145,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))) @@ -155,14 +164,26 @@ struct ChatPreviewView: View { let t = Text(chat.chatInfo.chatViewName).font(.title3).fontWeight(.bold) switch chat.chatInfo { case let .direct(contact): - previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(deleting ? Color.secondary : nil) - case let .group(groupInfo): - let v = previewTitle(t) - switch (groupInfo.membership.memberStatus) { - case .memInvited: v.foregroundColor(deleting ? theme.colors.secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary) - case .memAccepted: v.foregroundColor(theme.colors.secondary) - default: if deleting { v.foregroundColor(theme.colors.secondary) } else { v } + let color = + deleting + ? theme.colors.secondary + : (contact.nextAcceptContactRequest && !(contact.groupDirectInv?.memberRemoved ?? false)) || contact.sendMsgToConnect + ? theme.colors.primary + : !contact.sndReady + ? theme.colors.secondary + : nil + previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(color) + case let .group(groupInfo, _): + let color = if deleting { + theme.colors.secondary + } else { + switch (groupInfo.membership.memberStatus) { + case .memInvited: chat.chatInfo.incognito ? .indigo : theme.colors.primary + case .memAccepted, .memRejected: theme.colors.secondary + default: groupInfo.nextConnectPrepared ? theme.colors.primary : nil + } } + previewTitle(t).foregroundColor(color) default: previewTitle(t) } } @@ -178,14 +199,17 @@ struct ChatPreviewView: View { .kerning(-2) } - private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: 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(userFont <= .xxxLarge ? 2 : 1) .multilineTextAlignment(.leading) + .if(hasSecrets, transform: hiddenSecretsView) .frame(maxWidth: .infinity, alignment: .topLeading) .padding(.leading, hasFilePreview ? 0 : 8) - .padding(.trailing, hasFilePreview ? 38 : 36) + .padding(.trailing, mentionWidth + (hasFilePreview ? 38 : 36)) .offset(x: hasFilePreview ? -2 : 0) .fixedSize(horizontal: false, vertical: true) if !showChatPreviews && !draft { @@ -200,19 +224,34 @@ struct ChatPreviewView: View { @ViewBuilder private func chatInfoIcon(_ chat: Chat) -> some View { let s = chat.chatStats if s.unreadCount > 0 || s.unreadChat { - unreadCountText(s.unreadCount) - .font(userFont <= .xxxLarge ? .caption : .caption2) - .foregroundColor(.white) - .padding(.horizontal, dynamicSize(userFont).unreadPadding) - .frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize) - .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary) - .cornerRadius(dynamicSize(userFont).unreadCorner) - } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { - Image(systemName: "speaker.slash.fill") + 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: dynamicChatInfoSize, height: dynamicChatInfoSize) - .foregroundColor(theme.colors.secondary) + .frame(width: iconSize, height: iconSize) + .foregroundColor(iconColor) } else if chat.chatInfo.chatSettings?.favorite ?? false { Image(systemName: "star.fill") .resizable() @@ -225,11 +264,21 @@ struct ChatPreviewView: View { } } - private func messageDraft(_ draft: ComposeState) -> Text { + 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: theme.colors.primary) - + attachment() - + messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) + let r = markdownText(msg, preview: true, mentions: draft.mentions, backgroundColor: 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) + textSpace @@ -245,10 +294,11 @@ 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: nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary, prefix: prefix()) + let r = messageText(itemText, itemFormattedText, sender: cItem.meta.showGroupAsSender ? nil : cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, backgroundColor: UIColor(theme.colors.background), prefix: prefix()) + return (Text(AttributedString(r.string)), r.hasSecrets) // 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 @@ -274,46 +324,67 @@ struct ChatPreviewView: View { default: return nil } } - - func prefix() -> Text { + + func prefix() -> NSAttributedString? { switch cItem.content.msgContent { - case let .report(_, reason): return Text(!itemText.isEmpty ? "\(reason.text): " : reason.text).italic().foregroundColor(Color.red) - default: return Text("") + case let .report(_, reason): reason.attrString + default: nil } } } @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, hasFilePreview) + let (t, hasSecrets) = messageDraft(draft) + chatPreviewLayout(t, draft: true, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets) + } else if cItem?.content.hasMsgContent != true, let previewText = chatPreviewInfoText() { + chatPreviewInfoTextLayout(previewText) } else if let cItem = cItem { - chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem), hasFilePreview) - } else { - switch (chat.chatInfo) { - case let .direct(contact): - if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active { - chatPreviewInfoText("Tap to Connect") - .foregroundColor(theme.colors.primary) - } else if !contact.sndReady && contact.activeConn != nil { - if contact.nextSendGrpInv { - chatPreviewInfoText("send direct message") - } else if contact.active { - chatPreviewInfoText("connecting…") - } - } - case let .group(groupInfo): - switch (groupInfo.membership.memberStatus) { - case .memInvited: groupInvitationPreviewText(groupInfo) - case .memAccepted: chatPreviewInfoText("connecting…") - default: EmptyView() - } - default: EmptyView() + let (t, hasSecrets) = chatItemPreview(cItem) + chatPreviewLayout(itemStatusMark(cItem) + t, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets) + } + } + + private func chatPreviewInfoText() -> Text? { + switch (chat.chatInfo) { + case let .direct(contact): + if contact.isContactCard { + Text("Tap to Connect") + .foregroundColor(theme.colors.primary) + } else if contact.isBot && contact.nextConnectPrepared { + Text("Open to use bot") + } else if contact.sendMsgToConnect { + Text("Open to connect") + } else if contact.nextAcceptContactRequest { + Text("Open to accept") + } else if !contact.sndReady && contact.activeConn != nil && contact.active { + (contact.preparedContact?.uiConnLinkType == .con && !contact.isBot) || contact.contactGroupMemberId != nil + ? Text("contact should accept…") + : Text("connecting…") + } else { + nil } + case let .group(groupInfo, _): + if groupInfo.nextConnectPrepared { + if groupInfo.businessChat?.chatType == .business { + Text("Open to connect") + } else { + Text("Open to join") + } + } else { + switch (groupInfo.membership.memberStatus) { + case .memRejected: Text("rejected") + case .memInvited: groupInvitationPreviewText(groupInfo) + case .memAccepted: Text("connecting…") + case .memPendingReview, .memPendingApproval: Text("reviewed by admins") + default: nil + } + } + default: nil } } @ViewBuilder func chatItemContentPreview(_ chat: Chat, _ ci: ChatItem) -> some View { - let linkClicksEnabled = privacyChatListOpenLinksDefault.get() != PrivacyChatListOpenLinksMode.no let mc = ci.content.msgContent switch mc { case let .link(_, preview): @@ -335,28 +406,16 @@ struct ChatPreviewView: View { .cornerRadius(8) } .onTapGesture { - switch privacyChatListOpenLinksDefault.get() { - case .yes: UIApplication.shared.open(preview.uri) - case .no: ItemsModel.shared.loadOpenChat(chat.id) - case .ask: AlertManager.shared.showAlert( - Alert(title: Text("Open web link?"), - message: Text(preview.uri.absoluteString), - primaryButton: .default(Text("Open chat"), action: { ItemsModel.shared.loadOpenChat(chat.id) }), - secondaryButton: .default(Text("Open link"), action: { UIApplication.shared.open(preview.uri) }) - ) - ) - } + openBrowserAlert(uri: preview.uri) } } case let .image(_, image): smallContentPreview(size: dynamicMediaSize) { CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery) - .environmentObject(ReverseListScrollModel()) } case let .video(_,image, duration): smallContentPreview(size: dynamicMediaSize) { CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery) - .environmentObject(ReverseListScrollModel()) } case let .voice(_, duration): smallContentPreviewVoice(size: dynamicMediaSize) { @@ -371,14 +430,14 @@ struct ChatPreviewView: View { } - @ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View { + private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> Text { groupInfo.membership.memberIncognito - ? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)") - : chatPreviewInfoText("you are invited to group") + ? Text("Join as \(groupInfo.membership.memberProfile.displayName)") + : Text("You are invited to group") } - @ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View { - Text(text) + private func chatPreviewInfoTextLayout(_ text: Text) -> some View { + text .frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) .padding([.leading, .trailing], 8) .padding(.bottom, 4) @@ -401,17 +460,15 @@ struct ChatPreviewView: View { @ViewBuilder private func chatStatusImage() -> some View { let size = dynamicSize(userFont).incognitoSize switch chat.chatInfo { - case let .direct(contact): - if contact.active && contact.activeConn != nil { - NetworkStatusView(contact: contact, size: size) - } else { - 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) + flagIcon(size: size * 0.8, color: .red) + } else if chat.supportUnreadCount > 0 { + flagIcon(size: size * 0.8, color: theme.colors.primary) + } else if chat.chatInfo.groupInfo?.membership.memberPending ?? false { + flagIcon(size: size * 0.8, color: theme.colors.secondary) } else { incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) } @@ -419,30 +476,6 @@ struct ChatPreviewView: View { 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, _ secondaryColor: Color, size: CGFloat) -> some View { @@ -457,12 +490,12 @@ struct ChatPreviewView: View { } } -@ViewBuilder func groupReportsIcon(size: CGFloat) -> some View { +func flagIcon(size: CGFloat, color: Color) -> some View { Image(systemName: "flag") .resizable() .scaledToFit() .frame(width: size, height: size) - .foregroundColor(.red) + .foregroundColor(color) } func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View { diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 0f64b632dc..124c5ee7ba 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -14,6 +14,7 @@ struct ContactConnectionInfo: View { @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 @@ -61,14 +62,19 @@ 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, theme.colors.secondary) - oneTimeLinkLearnMoreButton(theme.colors.secondary) + shareLinkButton(connLinkInv, short: showShortLink) + oneTimeLinkLearnMoreButton() } else { incognitoEnabled() - oneTimeLinkLearnMoreButton(theme.colors.secondary) + oneTimeLinkLearnMoreButton() + } + } header: { + if let connLinkInv = contactConnection.connLinkInv, connLinkInv.connShortLink != nil { + ToggleShortLinkHeader(text: Text(""), link: connLinkInv, short: $showShortLink) } } footer: { sharedProfileInfo(contactConnection.incognito) @@ -108,6 +114,7 @@ struct ContactConnectionInfo: View { .onAppear { localAlias = contactConnection.localAlias } + .onDisappear(perform: setConnectionAlias) } private func setConnectionAlias() { @@ -167,26 +174,22 @@ struct ContactConnectionInfo: View { } } -private func shareLinkButton(_ connReqInvitation: String, _ secondaryColor: Color) -> 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", color: secondaryColor) { - Text("Share 1-time link") - } + Label("Share 1-time link", systemImage: "square.and.arrow.up") } } -private func oneTimeLinkLearnMoreButton(_ secondaryColor: Color) -> some View { +private func oneTimeLinkLearnMoreButton() -> some View { NavigationLink { AddContactLearnMore(showTitle: false) .navigationTitle("One-time invitation link") .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.large) } label: { - settingsRow("info.circle", color: secondaryColor) { - Text("Learn more") - } + Label("Learn more", systemImage: "info.circle") } } diff --git a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift index aa802c1af9..ee7605dbd2 100644 --- a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -245,7 +245,7 @@ struct ServersSummaryView: View { } } - @ViewBuilder private func smpServersListView( + private func smpServersListView( _ servers: [SMPServerSummary], _ statsStartedAt: Date, _ header: LocalizedStringKey? = nil, @@ -256,7 +256,7 @@ struct ServersSummaryView: View { ? serverAddress($0.smpServer) < serverAddress($1.smpServer) : $0.hasSubs && !$1.hasSubs } - Section { + return Section { ForEach(sortedServers) { server in smpServerView(server, statsStartedAt) } @@ -318,14 +318,14 @@ struct ServersSummaryView: View { return onionHosts == .require ? .indigo : .accentColor } - @ViewBuilder private func xftpServersListView( + 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) } - Section { + return Section { ForEach(sortedServers) { server in xftpServerView(server, statsStartedAt) } @@ -412,7 +412,7 @@ struct SubscriptionStatusIndicatorView: View { var hasSess: Bool var body: some View { - let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage( + let (color, variableValue, opacity) = subscriptionStatusInfo( online: m.networkInfo.online, usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil, subs: subs, @@ -431,25 +431,19 @@ struct SubscriptionStatusIndicatorView: View { 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)))%") + let statusPercent = subscriptionStatusPercent(online: m.networkInfo.online, subs: subs, hasSess: hasSess) + let percentText: String = subs.total > 0 || hasSess ? "\(Int(floor(statusPercent * 100)))%" : "%" + Text(percentText) .foregroundColor(.secondary) .font(.caption) } } -func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double, Double) { +func subscriptionStatusInfo(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double) { func roundedToQuarter(_ n: Double) -> Double { n >= 1 ? 1 : n <= 0 ? 0 @@ -457,26 +451,28 @@ func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: S } let activeColor: Color = usesProxy ? .indigo : primaryColor - let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0) + let noConnColorAndPercent: (Color, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1) 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 - ) - ) + : subs.total == 0 && !hasSess + ? (activeColor, 0, 0.33) // On freshly installed app (without chats) and on app start + : subs.ssActive == 0 + ? ( + hasSess ? (activeColor, activeSubsRounded, subs.shareOfActive) : noConnColorAndPercent ) + : ( // ssActive > 0 + hasSess + ? (activeColor, activeSubsRounded, subs.shareOfActive) + : (.orange, activeSubsRounded, subs.shareOfActive) // This would mean implementation error + ) +} + +func subscriptionStatusPercent(online: Bool, subs: SMPServerSubs, hasSess: Bool) -> Double { + online && (hasSess || (subs.total > 0 && subs.ssActive > 0)) + ? subs.shareOfActive + : 0 } struct SMPServerSummaryView: View { @@ -587,7 +583,7 @@ struct SMPStatsView: View { } header: { Text("Statistics") } footer: { - Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.") + Text("Starting from \(localTimestamp(statsStartedAt)).") + textNewLine + Text("All data is kept private on your device.") } } } @@ -703,7 +699,7 @@ struct XFTPStatsView: View { } header: { Text("Statistics") } footer: { - Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.") + Text("Starting from \(localTimestamp(statsStartedAt)).") + textNewLine + Text("All data is kept private on your device.") } } } diff --git a/apps/ios/Shared/Views/ChatList/TagListView.swift b/apps/ios/Shared/Views/ChatList/TagListView.swift index 8811234f52..79d122eabf 100644 --- a/apps/ios/Shared/Views/ChatList/TagListView.swift +++ b/apps/ios/Shared/Views/ChatList/TagListView.swift @@ -61,12 +61,9 @@ struct TagListView: View { Button { showAlert( NSLocalizedString("Delete list?", comment: "alert title"), - message: NSLocalizedString("All chats will be removed from the list \(text), and the list deleted.", comment: "alert message"), + 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 - ), + cancelAlertAction, UIAlertAction( title: NSLocalizedString("Delete", comment: "alert action"), style: .destructive, @@ -138,7 +135,7 @@ struct TagListView: View { } } - @ViewBuilder private func radioButton(selected: Bool) -> some View { + 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/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index dbe10ad997..b1cd4015c6 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -97,7 +97,7 @@ struct UserPicker: View { } .onAppear { // This check prevents the call of listUsers after the app is suspended, and the database is closed. - if case .active = scenePhase { + if case .active = scenePhase, hasChatCtrl() { currentUser = m.currentUser?.userId Task { do { @@ -124,7 +124,7 @@ struct UserPicker: View { 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) + userUnreadBadge(u, theme: theme).offset(x: 4, y: -4) } } .padding(.trailing, 6) @@ -171,19 +171,27 @@ struct UserPicker: View { } } +@inline(__always) +func userUnreadBadge(_ userInfo: UserInfo, theme: AppTheme) -> some View { + UnreadBadge( + count: userInfo.unreadCount, + color: userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary + ) +} + struct UnreadBadge: View { - var userInfo: UserInfo - @EnvironmentObject var theme: AppTheme @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + var count: Int + var color: Color var body: some View { let size = dynamicSize(userFont).chatInfoSize - unreadCountText(userInfo.unreadCount) + unreadCountText(count) .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) + .background(color) .cornerRadius(dynamicSize(userFont).unreadCorner) } } diff --git a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift index 242b492e83..fcfcde2c07 100644 --- a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -20,20 +20,17 @@ struct ContactListNavLink: View { @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: + if contact.nextAcceptContactRequest { + contactWithRequestNavLink(contact) + } else if contact.isContactCard { contactCardNavLink(contact) - default: - EmptyView() + } else if contact.chatDeleted { + deletedChatNavLink(contact) + } else if contact.active { + recentContactNavLink(contact) } case let .contactRequest(contactRequest): contactRequestNavLink(contactRequest) @@ -59,7 +56,7 @@ struct ContactListNavLink: View { ItemsModel.shared.loadOpenChat(contact.id) } } label: { - contactPreview(contact, titleColor: theme.colors.onBackground) + contactPreview(contact, titleColor: contact.sendMsgToConnect ? theme.colors.primary : theme.colors.onBackground) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { @@ -78,6 +75,67 @@ struct ContactListNavLink: View { } } + func contactWithRequestNavLink(_ contact: Contact) -> some View { + Button { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(contact.id) + } + } label: { + contactRequestPreview(color: contact.groupDirectInv?.memberRemoved == true ? theme.colors.secondary : theme.colors.primary) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + if let contactRequestId = contact.contactRequestId { + Button { + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) } + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + if !ChatModel.shared.addressShortLinkDataSet { + Button { + Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) } + } label: { + Label("Accept incognito", systemImage: "theatermasks") + } + .tint(.indigo) + } + Button { + alert = SomeAlert(alert: rejectContactRequestAlert(contactRequestId), id: "rejectContactRequestAlert") + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) + } else if let groupDirectInv = contact.groupDirectInv, !groupDirectInv.memberRemoved { + Button { + acceptMemberContactRequest(contact) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + Button { + showRejectMemberContactRequestAlert(contact) + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) + } else { + 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 { @@ -140,9 +198,9 @@ struct ContactListNavLink: View { } } - @ViewBuilder private func previewTitle(_ contact: Contact, titleColor: Color) -> some View { + private func previewTitle(_ contact: Contact, titleColor: Color) -> some View { let t = Text(chat.chatInfo.chatViewName).foregroundColor(titleColor) - ( + return ( contact.verified == true ? verifiedIcon + t : t @@ -179,8 +237,14 @@ struct ContactListNavLink: View { .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) } + if !contact.profileChangeProhibited { + Button("Use current profile") { connectContactViaAddress_(contact, false) } + Button("Use new incognito profile") { connectContactViaAddress_(contact, true) } + } else if !contact.contactConnIncognito { + Button("Use current profile") { connectContactViaAddress_(contact, false) } + } else { + Button("Use incognito profile") { connectContactViaAddress_(contact, true) } + } } } @@ -188,8 +252,7 @@ struct ContactListNavLink: View { Task { let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { alert = SomeAlert(alert: $0, id: "ContactListNavLink connectContactViaAddress") }) if ok { - ItemsModel.shared.loadOpenChat(contact.id) - DispatchQueue.main.async { + ItemsModel.shared.loadOpenChat(contact.id) { dismissAllSheets(animated: true) { AlertManager.shared.showAlert(connReqSentAlert(.contact)) } @@ -220,39 +283,43 @@ struct ContactListNavLink: View { Button { showContactRequestDialog = true } label: { - contactRequestPreview(contactRequest) + contactRequestPreview(color: theme.colors.primary) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { - Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } } label: { Label("Accept", systemImage: "checkmark") } .tint(theme.colors.primary) - Button { - Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } - } label: { - Label("Accept incognito", systemImage: "theatermasks") + if !ChatModel.shared.addressShortLinkDataSet { + Button { + Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } + } label: { + Label("Accept incognito", systemImage: "theatermasks") + } + .tint(.indigo) } - .tint(.indigo) Button { - alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest), id: "rejectContactRequestAlert") + alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest.apiId), 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) } } + Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } } + if !ChatModel.shared.addressShortLinkDataSet { + Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } } + } + Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } } } } - func contactRequestPreview(_ contactRequest: UserContactRequest) -> some View { + func contactRequestPreview(color: Color) -> some View { HStack{ - ProfileImage(imageStr: contactRequest.image, size: 30) + ProfileImage(imageStr: chat.chatInfo.image, size: 30) Text(chat.chatInfo.chatViewName) - .foregroundColor(.accentColor) + .foregroundColor(color) .lineLimit(1) Spacer() @@ -261,7 +328,7 @@ struct ContactListNavLink: View { .resizable() .scaledToFill() .frame(width: 14, height: 14) - .foregroundColor(.accentColor) + .foregroundColor(color) } } } diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift index 3cd37e4930..441a164f8a 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -173,7 +173,7 @@ struct DatabaseEncryptionView: View { } return true } catch let error { - if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse { + if case .errorDatabase(.errorExport(.errorNotADatabase)) = error as? ChatError { await operationEnded(.currentPassphraseError) } else { await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))")) diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index 6222a28fb4..02a1b87826 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -28,7 +28,7 @@ struct DatabaseErrorView: View { } } - @ViewBuilder private func databaseErrorView() -> some View { + private func databaseErrorView() -> some View { VStack(alignment: .center, spacing: 20) { switch status { case let .errorNotADatabase(dbFile): @@ -141,7 +141,7 @@ struct DatabaseErrorView: View { } private func migrationsText(_ ms: [String]) -> some View { - (Text("Migrations:").font(.subheadline) + Text(verbatim: "\n") + Text(ms.joined(separator: "\n")).font(.caption)) + (Text("Migrations:").font(.subheadline) + textNewLine + Text(ms.joined(separator: "\n")).font(.caption)) .multilineTextAlignment(.center) .padding(.horizontal, 25) } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 4c05434eb6..a7e61b3105 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -21,7 +21,7 @@ enum DatabaseAlert: Identifiable { case deleteLegacyDatabase case deleteFilesAndMedia case setChatItemTTL(ttl: ChatItemTTL) - case error(title: LocalizedStringKey, error: String = "") + case error(title: String, error: String = "") var id: String { switch self { @@ -279,7 +279,7 @@ struct DatabaseView: View { case let .archiveExportedWithErrors(archivePath, errs): return Alert( title: Text("Chat database exported"), - message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)), + 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]) } @@ -456,7 +456,7 @@ struct DatabaseView: View { } } catch let error { await MainActor.run { - alert = .error(title: "Error exporting chat database", error: responseError(error)) + alert = .error(title: NSLocalizedString("Error exporting chat database", comment: "alert title"), error: responseError(error)) progressIndicator = false } } @@ -492,10 +492,10 @@ struct DatabaseView: View { return migration } } catch let error { - await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert) + await operationEnded(.error(title: NSLocalizedString("Error importing chat database", comment: "alert title"), error: responseError(error)), progressIndicator, alert) } } catch let error { - await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert) + await operationEnded(.error(title: NSLocalizedString("Error deleting chat database", comment: "alert title"), error: responseError(error)), progressIndicator, alert) } } else { showAlert("Error accessing database file") @@ -513,7 +513,7 @@ struct DatabaseView: View { await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert) return true } catch let error { - await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert) + await DatabaseView.operationEnded(.error(title: NSLocalizedString("Error deleting database", comment: "alert title"), error: responseError(error)), $progressIndicator, $alert) return false } } @@ -522,7 +522,7 @@ struct DatabaseView: View { if removeLegacyDatabaseAndFiles() { legacyDatabase = false } else { - alert = .error(title: "Error deleting old database") + alert = .error(title: NSLocalizedString("Error deleting old database", comment: "alert title")) } } @@ -546,7 +546,7 @@ struct DatabaseView: View { let (title, message) = chatDeletedAlertText() showAlert(title, message: message, actions: { [okAlertActionWaiting] }) } else if case let .error(title, error) = dbAlert { - showAlert("\(title)", message: error, actions: { [okAlertActionWaiting] }) + showAlert(title, message: error, actions: { [okAlertActionWaiting] }) } else { alert.wrappedValue = dbAlert cont.resume() @@ -567,7 +567,7 @@ struct DatabaseView: View { } } catch { await MainActor.run { - alert = .error(title: "Error changing setting", error: responseError(error)) + alert = .error(title: NSLocalizedString("Error changing setting", comment: "alert title"), error: responseError(error)) chatItemTTL = currentChatItemTTL afterSetCiTTL() } diff --git a/apps/ios/Shared/Views/Helpers/AppSheet.swift b/apps/ios/Shared/Views/Helpers/AppSheet.swift index 1e334367e8..17fe95a058 100644 --- a/apps/ios/Shared/Views/Helpers/AppSheet.swift +++ b/apps/ios/Shared/Views/Helpers/AppSheet.swift @@ -33,7 +33,7 @@ extension View { func appSheet( isPresented: Binding, onDismiss: (() -> Void)? = nil, - content: @escaping () -> Content + @ViewBuilder content: @escaping () -> Content ) -> some View where Content: View { sheet(isPresented: isPresented, onDismiss: onDismiss) { content().modifier(PrivacySensitive()) @@ -43,7 +43,7 @@ extension View { func appSheet( item: Binding, onDismiss: (() -> Void)? = nil, - content: @escaping (T) -> Content + @ViewBuilder content: @escaping (T) -> Content ) -> some View where T: Identifiable, Content: View { sheet(item: item, onDismiss: onDismiss) { it in content(it).modifier(PrivacySensitive()) diff --git a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift index 9aa6ac86cf..980308f13c 100644 --- a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift +++ b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift @@ -76,7 +76,7 @@ struct ChatTailPadding: ViewModifier { } } -private let msgRectMaxRadius: Double = 18 +let msgRectMaxRadius: Double = 18 private let msgBubbleMaxRadius: Double = msgRectMaxRadius * 1.2 private let msgTailWidth: Double = 9 private let msgTailMinHeight: Double = msgTailWidth * 1.254 // ~56deg diff --git a/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift b/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift index 09aae1cb15..edb10ef87d 100644 --- a/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift +++ b/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift @@ -220,6 +220,35 @@ struct DropdownCustomTimePicker: View { } } +struct WrappedPicker: View { + var selection: Binding + @ViewBuilder var content: () -> Content + @ViewBuilder var label: () -> Label + + init(_ title: LocalizedStringKey, selection: Binding, @ViewBuilder content: @escaping () -> Content) where Label == Text { + self.selection = selection + self.content = content + self.label = { Text(title) } + } + + init(selection: Binding, @ViewBuilder content: @escaping () -> Content, @ViewBuilder label: @escaping () -> Label) { + self.selection = selection + self.content = content + self.label = label + } + + var body: some View { + HStack(alignment: .firstTextBaseline) { + label() + Spacer() + Picker(selection: selection, content: content) { + EmptyView() + } + .frame(height: 36) + } + } +} + struct CustomTimePicker_Previews: PreviewProvider { static var previews: some View { CustomTimePicker( diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index 3eedd56441..9c2916880c 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -27,8 +27,13 @@ struct ProfileImage: View { Image(systemName: iconName) .resizable() .foregroundColor(c) + .scaledToFit() .frame(width: size, height: size) - .background(Circle().fill(backgroundColor != nil ? backgroundColor! : .clear)) + .background( + Circle() + .fill(backgroundColor != nil ? backgroundColor! : .clear) + .frame(width: size - 2, height: size - 2) // less than size of Image to avoid slightly visible border + ) } } } diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index b8de0e4ceb..86a5dc7aaa 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -65,6 +65,252 @@ func showAlert( } } +func showSheet( + _ title: String?, + message: String? = nil, + actions: () -> [UIAlertAction] = { [okAlertAction] }, + sourceView: UIView? = nil // For iPad support +) { + if let topController = getTopViewController() { + let sheet = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) + for action in actions() { sheet.addAction(action) } + + // Required for iPad: Configure popover presentation + if let popover = sheet.popoverPresentationController { + popover.sourceView = sourceView ?? topController.view + popover.sourceRect = sourceView?.bounds ?? CGRect(x: topController.view.bounds.midX, y: topController.view.bounds.midY, width: 0, height: 0) + popover.permittedArrowDirections = [] + } + + topController.present(sheet, animated: true) + } +} + let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default) let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) + +let alertProfileImageSize: CGFloat = 103 + +let alertWidth: CGFloat = 270 + +let alertButtonHeight: CGFloat = 44 + +class OpenChatAlertViewController: UIViewController { + private let profileName: String + private let profileFullName: String + private let profileImage: UIView + private let cancelTitle: String + private let confirmTitle: String + private let onCancel: () -> Void + private let onConfirm: () -> Void + + init( + profileName: String, + profileFullName: String, + profileImage: UIView, + cancelTitle: String = "Cancel", + confirmTitle: String = "Open", + onCancel: @escaping () -> Void, + onConfirm: @escaping () -> Void + ) { + self.profileName = profileName + self.profileFullName = profileFullName + self.profileImage = profileImage + self.cancelTitle = cancelTitle + self.confirmTitle = confirmTitle + self.onCancel = onCancel + self.onConfirm = onConfirm + super.init(nibName: nil, bundle: nil) + + modalPresentationStyle = .overFullScreen + modalTransitionStyle = .crossDissolve + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.black.withAlphaComponent(0.3) + + // Container view + let containerView = UIView() + containerView.backgroundColor = .systemBackground + containerView.layer.cornerRadius = 12 + containerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(containerView) + + // Profile image sizing + profileImage.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + profileImage.widthAnchor.constraint(equalToConstant: alertProfileImageSize), + profileImage.heightAnchor.constraint(equalToConstant: alertProfileImageSize) + ]) + + // Name label + let nameLabel = UILabel() + nameLabel.text = profileName + nameLabel.font = UIFont.preferredFont(forTextStyle: .headline) + nameLabel.textColor = .label + nameLabel.numberOfLines = 2 + nameLabel.textAlignment = .center + nameLabel.translatesAutoresizingMaskIntoConstraints = false + + var profileViews = [profileImage, nameLabel] + + // Full name label + if !profileFullName.isEmpty && profileFullName != profileName { + let fullNameLabel = UILabel() + fullNameLabel.text = profileFullName + fullNameLabel.font = UIFont.preferredFont(forTextStyle: .subheadline) + fullNameLabel.textColor = .label + fullNameLabel.numberOfLines = 2 + fullNameLabel.textAlignment = .center + fullNameLabel.translatesAutoresizingMaskIntoConstraints = false + profileViews.append(fullNameLabel) + } + + // Horizontal stack for image + name + let stack = UIStackView(arrangedSubviews: profileViews) + stack.axis = .vertical + stack.spacing = 12 + stack.alignment = .center + stack.translatesAutoresizingMaskIntoConstraints = false + + let topRowContainer = UIView() + topRowContainer.translatesAutoresizingMaskIntoConstraints = false + topRowContainer.addSubview(stack) + + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: topRowContainer.topAnchor), + stack.bottomAnchor.constraint(equalTo: topRowContainer.bottomAnchor), + stack.leadingAnchor.constraint(equalTo: topRowContainer.leadingAnchor, constant: 20), + stack.trailingAnchor.constraint(equalTo: topRowContainer.trailingAnchor, constant: -20) + ]) + + // Buttons + let cancelButton = UIButton(type: .system) + cancelButton.setTitle(cancelTitle, for: .normal) + let bodyDescr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body) + cancelButton.titleLabel?.font = UIFont(descriptor: bodyDescr.withSymbolicTraits(.traitBold) ?? bodyDescr, size: 0) + cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside) + + let confirmButton = UIButton(type: .system) + confirmButton.setTitle(confirmTitle, for: .normal) + confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) + + let verticalButtons = cancelButton.intrinsicContentSize.width + 20 >= alertWidth / 2 || confirmButton.intrinsicContentSize.width + 20 >= alertWidth / 2 + + // Button stack with equal width buttons + let buttonStack = UIStackView(arrangedSubviews: verticalButtons ? [confirmButton, cancelButton] : [cancelButton, confirmButton]) + buttonStack.axis = verticalButtons ? .vertical : .horizontal + buttonStack.distribution = .fillEqually + buttonStack.spacing = 0 // no spacing, use divider instead + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * (verticalButtons ? 2 : 1)).isActive = true + + // Vertical stack containing hStack and buttonStack + let vStack = UIStackView(arrangedSubviews: [topRowContainer, buttonStack]) + vStack.axis = .vertical + vStack.spacing = 16 + vStack.alignment = .fill // important: buttons stretch full width + vStack.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(vStack) + + // Add horizontal divider above buttons + let horizontalDivider = UIView() + horizontalDivider.backgroundColor = UIColor.separator + horizontalDivider.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(horizontalDivider) + + // Add divider between buttons + let buttonDivider = UIView() + buttonDivider.backgroundColor = UIColor.separator + buttonDivider.translatesAutoresizingMaskIntoConstraints = false + buttonStack.addSubview(buttonDivider) + + // Constraints + let buttonDividerConstraints = if verticalButtons { + [ + buttonDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + buttonDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + buttonDivider.centerYAnchor.constraint(equalTo: buttonStack.centerYAnchor), + buttonDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + } else { + [ + buttonDivider.topAnchor.constraint(equalTo: buttonStack.topAnchor), + buttonDivider.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + buttonDivider.centerXAnchor.constraint(equalTo: buttonStack.centerXAnchor), + buttonDivider.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + } + + NSLayoutConstraint.activate([ + // Container view centering and fixed width + containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + containerView.widthAnchor.constraint(equalToConstant: alertWidth), + + // Vertical stack padding inside containerView + vStack.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20), + vStack.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0), + vStack.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0), + vStack.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0), + + // Center hStack horizontally inside vStack's padded width + stack.centerXAnchor.constraint(equalTo: vStack.centerXAnchor), + + // Horizontal divider above buttons + horizontalDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + horizontalDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + horizontalDivider.bottomAnchor.constraint(equalTo: buttonStack.topAnchor), + horizontalDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + buttonDividerConstraints) + } + + @objc private func cancelTapped() { + dismiss(animated: true) { + self.onCancel() + } + } + + @objc private func confirmTapped() { + dismiss(animated: true) { + self.onConfirm() + } + } +} + + +func showOpenChatAlert( + profileName: String, + profileFullName: String, + profileImage: Content, + theme: AppTheme, + cancelTitle: String = "Cancel", + confirmTitle: String = "Open", + onCancel: @escaping () -> Void = {}, + onConfirm: @escaping () -> Void +) { + let themedView = profileImage.environmentObject(theme) + let hostingController = UIHostingController(rootView: themedView) + let hostedView = hostingController.view! + hostedView.backgroundColor = .clear + + if let topVC = getTopViewController() { + let alertVC = OpenChatAlertViewController( + profileName: profileName, + profileFullName: profileFullName, + profileImage: hostedView, + cancelTitle: cancelTitle, + confirmTitle: confirmTitle, + onCancel: onCancel, + onConfirm: onConfirm + ) + topVC.present(alertVC, animated: true) + } +} diff --git a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift index c790b9cff2..85ef85c611 100644 --- a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift +++ b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift @@ -9,6 +9,7 @@ import SwiftUI extension View { + @inline(__always) @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { if condition { transform(self) @@ -36,9 +37,9 @@ struct PrivacyBlur: ViewModifier { .overlay { if (blurred && enabled) { Color.clear.contentShape(Rectangle()) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { blurred = false - } + }) } } .onReceive(NotificationCenter.default.publisher(for: .chatViewWillBeginScrolling)) { _ in diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift index 27bb95b599..c21ff9be8b 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -65,6 +65,9 @@ struct LocalAuthView: View { // Clear sensitive data on screen just in case app fails to hide its views while new database is created m.chatId = nil ItemsModel.shared.reversedChatItems = [] + ItemsModel.shared.chatState.clear() + ChatModel.shared.secondaryIM?.reversedChatItems = [] + ChatModel.shared.secondaryIM?.chatState.clear() m.updateChats([]) m.users = [] _ = kcAppPassword.set(password) diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift index 609943bcb6..4a6f8e7549 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift @@ -28,7 +28,7 @@ struct PasscodeEntry: View { } } - @ViewBuilder private func passwordView() -> some View { + private func passwordView() -> some View { Text( password == "" ? " " diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index eb8df5fb04..0af8fa7ad8 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -177,7 +177,7 @@ struct MigrateFromDevice: View { case let .archiveExportedWithErrors(archivePath, errs): return Alert( title: Text("Chat database exported"), - message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)), + 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) } } @@ -520,15 +520,15 @@ 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 @@ -546,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: @@ -691,7 +691,7 @@ 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(responseError(error))) @@ -733,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 @@ -752,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) diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index 2d83cdc7c8..93fe19cf33 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -496,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 { @@ -509,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: @@ -539,7 +539,7 @@ struct MigrateToDevice: View { chatInitControllerRemovingDatabases() } else if ChatModel.shared.chatRunning == true { // cannot delete storage if chat is running - try await apiStopChat() + try await stopChatAsync() } try await apiDeleteStorage() try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) @@ -623,7 +623,7 @@ struct MigrateToDevice: View { AlertManager.shared.showAlert( Alert( title: Text("Error migrating settings"), - message: Text ("Some app settings were not migrated.") + Text("\n") + Text(responseError(error))) + message: Text ("Some app settings were not migrated.") + textNewLine + Text(responseError(error))) ) } hideView() @@ -632,6 +632,8 @@ struct MigrateToDevice: View { private func hideView() { onboardingStageDefault.set(.onboardingComplete) m.onboardingStage = .onboardingComplete + m.migrationState = nil + MigrationToDeviceState.save(nil) dismiss() } @@ -749,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 @@ -770,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/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 0c7f6136ff..901b2deeab 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -23,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: GroupLink? @State private var groupLinkMemberRole: GroupMemberRole = .member var body: some View { @@ -104,7 +104,9 @@ struct AddGroupView: View { } .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) - .onTapGesture(perform: hideKeyboard) + .onTapGesture { + focusDisplayName = false + } } } .onAppear() { @@ -161,7 +163,8 @@ struct AddGroupView: View { } else { Image(systemName: "pencil").foregroundColor(theme.colors.secondary) } - textField("Enter group name…", text: $profile.displayName) + TextField("Enter group name…", text: $profile.displayName) + .padding(.leading, 36) .focused($focusDisplayName) .submitLabel(.continue) .onSubmit { @@ -170,11 +173,6 @@ struct AddGroupView: View { } } - func textField(_ placeholder: LocalizedStringKey, text: Binding) -> some View { - TextField(placeholder, text: text) - .padding(.leading, 36) - } - func sharedGroupProfileInfo(_ incognito: Bool) -> Text { let name = ChatModel.shared.currentUser?.displayName ?? "" return Text( @@ -185,19 +183,15 @@ struct AddGroupView: View { } func createGroup() { - hideKeyboard() + focusDisplayName = false do { profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) profile.groupPreferences = GroupPreferences(history: GroupPreference(enable: .on)) let gInfo = try apiNewGroup(incognito: incognitoDefault, groupProfile: profile) Task { - let groupMembers = await apiListMembers(gInfo.groupId) - await MainActor.run { - m.groupMembers = groupMembers.map { GMember.init($0) } - m.populateGroupMembersIndexes() - } + await m.loadGroupMembers(gInfo) } - let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: []) + let c = Chat(chatInfo: .group(groupInfo: gInfo, groupChatScope: nil), chatItems: []) m.addChat(c) withAnimation { groupInfo = gInfo @@ -221,6 +215,8 @@ struct AddGroupView: View { } } +// Using this method may freeze the app in some cases, so it should be avoided when possible, especially when combined with .focussed modifier. +// It also must only be called from background thread. func hideKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 39656c1534..2e3119a8b8 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -9,10 +9,6 @@ import SwiftUI import SimpleXChat -enum ContactType: Int { - case card, request, recent, chatDeleted, unlisted -} - struct NewChatMenuButton: View { // do not use chatModel here because it prevents showing AddGroupMembersView after group creation and QR code after link creation on iOS 16 // @EnvironmentObject var chatModel: ChatModel @@ -20,7 +16,8 @@ struct NewChatMenuButton: View { @State private var alert: SomeAlert? = nil var body: some View { - Button { + Button { + ConnectProgressManager.shared.cancelConnectProgress() showNewChatSheet = true } label: { Image(systemName: "square.and.pencil") @@ -42,7 +39,6 @@ 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 @@ -60,7 +56,7 @@ struct NewChatSheet: View { @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 showArchive = chatModel.chats.contains { $0.chatInfo.contact?.chatDeleted == true } let v = NavigationView { viewBody(showArchive) .navigationTitle("New message") @@ -70,6 +66,8 @@ struct NewChatSheet: View { .alert(item: $alert) { a in return a.alert } + }.onDisappear { + ConnectProgressManager.shared.cancelConnectProgress() } if #available(iOS 16.0, *), oneHandUI { let sheetHeight: CGFloat = showArchive ? 575 : 500 @@ -85,7 +83,7 @@ struct NewChatSheet: View { } } - @ViewBuilder private func viewBody(_ showArchive: Bool) -> some View { + private func viewBody(_ showArchive: Bool) -> some View { List { HStack { ContactsListSearchBar( @@ -125,7 +123,7 @@ struct NewChatSheet: View { } NavigationLink { AddGroupView() - .navigationTitle("Create secret group") + .navigationTitle("Create group") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { @@ -145,7 +143,7 @@ struct NewChatSheet: View { } ContactsList( - baseContactTypes: $baseContactTypes, + chatPredicate: contactListChatPredicate, searchMode: $searchMode, searchText: $searchText, header: "Your Contacts", @@ -156,7 +154,15 @@ struct NewChatSheet: View { ) } } - + + private func contactListChatPredicate(_ chat: Chat, _ withSearch: Bool) -> Bool { + switch chat.chatInfo { + case .contactRequest: true + case let .direct(contact): contact.isContactCard || contact.active || (contact.chatDeleted && withSearch) + default: false + } + } + /// Extends label's tap area to match `.insetGrouped` list row insets private func navigateOnTap(_ label: L, setActive: @escaping () -> Void) -> some View { label @@ -186,35 +192,24 @@ struct NewChatSheet: View { } } -func chatContactType(_ chat: Chat) -> ContactType { +func chatOrderRank(_ chat: Chat) -> Int { switch chat.chatInfo { - case .contactRequest: - return .request + case .contactRequest: 4 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)) + contact.isContactCard ? 5 + : contact.nextAcceptContactRequest ? 4 + : contact.nextConnectPrepared ? 3 + : contact.active ? 2 + : contact.chatDeleted ? 1 + : 0 + default: 0 } } struct ContactsList: View { @EnvironmentObject var theme: AppTheme @EnvironmentObject var chatModel: ChatModel - @Binding var baseContactTypes: [ContactType] + var chatPredicate: (Chat, Bool) -> Bool // (chat, search) -> show @Binding var searchMode: Bool @Binding var searchText: String var header: String? = nil @@ -225,8 +220,7 @@ struct ContactsList: View { @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 contactChats = chatModel.chats.filter { chat in chatPredicate(chat, !searchText.isEmpty) } let filteredContactChats = filteredContactChats( showUnreadAndFavorites: showUnreadAndFavorites, searchShowingSimplexLink: searchShowingSimplexLink, @@ -258,7 +252,7 @@ struct ContactsList: View { } } - @ViewBuilder private func noResultSection(text: String) -> some View { + private func noResultSection(text: String) -> some View { Section { Text(text) .foregroundColor(theme.colors.secondary) @@ -269,26 +263,11 @@ struct ContactsList: View { .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 chatComparator(chat1: Chat, chat2: Chat) -> Bool { + let r1 = chatOrderRank(chat1) + let r2 = chatOrderRank(chat2) + return r1 > r2 ? true : r1 < r2 ? false : chat1.chatInfo.chatTs > chat2.chatInfo.chatTs } private func filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Bool) -> Bool { @@ -333,12 +312,13 @@ struct ContactsList: View { } } - return filteredChats.sorted(by: chatsByTypeComparator) + return filteredChats.sorted(by: chatComparator) } } struct ContactsListSearchBar: View { @EnvironmentObject var m: ChatModel + @StateObject private var connectProgressManager = ConnectProgressManager.shared @EnvironmentObject var theme: AppTheme @Binding var searchMode: Bool @FocusState.Binding var searchFocussed: Bool @@ -346,8 +326,6 @@ struct ContactsListSearchBar: View { @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 { @@ -364,6 +342,9 @@ struct ContactsListSearchBar: View { .disabled(searchShowingSimplexLink) .focused($searchFocussed) .frame(maxWidth: .infinity) + if connectProgressManager.showConnectProgress != nil { + ProgressView() + } if !searchText.isEmpty { Image(systemName: "xmark.circle.fill") .resizable() @@ -400,7 +381,7 @@ struct ContactsListSearchBar: View { } else { if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue searchFocussed = false - if case let .simplexLink(linkType, _, smpHosts) = link.format { + if case let .simplexLink(_, linkType, _, smpHosts) = link.format { ignoreSearchTextChange = true searchText = simplexLinkText(linkType, smpHosts) } @@ -410,18 +391,14 @@ struct ContactsListSearchBar: View { } else { if t != "" { // if some other text is pasted, enter search mode searchFocussed = true + } else { + connectProgressManager.cancelConnectProgress() } 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 { @@ -442,10 +419,12 @@ struct ContactsListSearchBar: View { private func connect(_ link: String) { planAndConnect( link, - showAlert: { alert = $0 }, - showActionSheet: { sheet = $0 }, + theme: theme, dismiss: true, - incognito: nil, + cleanup: { + searchText = "" + searchFocussed = false + }, filterKnownContact: { searchChatFilteredBySimplexLink = $0.id } ) } @@ -453,7 +432,6 @@ struct ContactsListSearchBar: View { struct DeletedChats: View { - @State private var baseContactTypes: [ContactType] = [.chatDeleted] @State private var searchMode = false @FocusState var searchFocussed: Bool @State private var searchText = "" @@ -475,7 +453,7 @@ struct DeletedChats: View { .frame(maxWidth: .infinity) ContactsList( - baseContactTypes: $baseContactTypes, + chatPredicate: { chat, _ in chat.chatInfo.contact?.chatDeleted == true }, searchMode: $searchMode, searchText: $searchText, searchFocussed: $searchFocussed, diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 6e898f4cdf..3de1fdb972 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -29,11 +29,9 @@ struct SomeSheet: Identifiable { } private enum NewChatViewAlert: Identifiable { - case planAndConnectAlert(alert: PlanAndConnectAlert) case newChatSomeAlert(alert: SomeAlert) var id: String { switch self { - case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" case let .newChatSomeAlert(alert): return "newChatSomeAlert \(alert.id)" } } @@ -81,7 +79,8 @@ struct NewChatView: View { @State var selection: NewChatOption @State var showQRCodeScanner = false @State private var invitationUsed: Bool = false - @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 = "" @@ -164,8 +163,6 @@ struct NewChatView: View { } .alert(item: $alert) { a in switch(a) { - case let .planAndConnectAlert(alert): - return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" }) case let .newChatSomeAlert(a): return a.alert } @@ -174,11 +171,12 @@ struct NewChatView: View { 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 { @@ -190,16 +188,16 @@ 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(pcc: pcc, connChatUsed: false) - connReqInvitation = connReq + connLinkInvitation = connLink contactConnection = pcc } } else { @@ -243,7 +241,8 @@ private struct InviteView: View { @EnvironmentObject var theme: AppTheme @Binding var invitationUsed: Bool @Binding var contactConnection: PendingContactConnection? - @Binding 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 @@ -261,7 +260,7 @@ private struct InviteView: View { NavigationLink { ActiveProfilePicker( contactConnection: $contactConnection, - connReqInvitation: $connReqInvitation, + connLinkInvitation: $connLinkInvitation, incognitoEnabled: $incognitoDefault, choosingProfile: $choosingProfile, selectedProfile: selectedProfile @@ -296,7 +295,7 @@ private struct InviteView: View { private func shareLinkView() -> some View { HStack { - let link = simplexChatLink(connReqInvitation) + let link = connLinkInvitation.simplexChatUri(short: showShortLink) linkTextView(link) Button { showShareSheet(items: [link]) @@ -310,9 +309,9 @@ private struct InviteView: View { } private func qrCodeView() -> some View { - Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) { - SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed) - .id("simplex-qrcode-view-for-\(connReqInvitation)") + Section { + SimpleXCreatedLinkQRCode(link: connLinkInvitation, short: $showShortLink, onShare: setInvitationUsed) + .id("simplex-qrcode-view-for-\(connLinkInvitation.simplexChatUri(short: showShortLink))") .padding() .background( RoundedRectangle(cornerRadius: 12, style: .continuous) @@ -322,6 +321,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) } } @@ -343,7 +344,7 @@ private struct ActiveProfilePicker: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @Binding var contactConnection: PendingContactConnection? - @Binding var connReqInvitation: String + @Binding var connLinkInvitation: CreatedConnLink @Binding var incognitoEnabled: Bool @Binding var choosingProfile: Bool @State private var alert: SomeAlert? @@ -366,7 +367,6 @@ private struct ActiveProfilePicker: View { .onAppear { profiles = chatModel.users .map { $0.user } - .sorted { u, _ in u.activeUser } } .onChange(of: incognitoEnabled) { incognito in if profileSwitchStatus != .switchingIncognito { @@ -417,15 +417,14 @@ private struct ActiveProfilePicker: View { do { if let contactConn = contactConnection, let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) { - await MainActor.run { contactConnection = conn - connReqInvitation = conn.connReqInv ?? "" + connLinkInvitation = conn.connLinkInv ?? CreatedConnLink(connFullLink: "", connShortLink: nil) incognitoEnabled = false chatModel.updateContactConnection(conn) } do { - try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil ) + try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil) await MainActor.run { profileSwitchStatus = .idle dismiss() @@ -436,7 +435,7 @@ private struct ActiveProfilePicker: View { 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.") + message: Text("Your connection was moved to \(profile.chatViewName) but an error happened when switching profile.") ), id: "switchingProfileError" ) @@ -475,8 +474,8 @@ private struct ActiveProfilePicker: View { IncognitoHelp() } } - - + + @ViewBuilder private func viewBody() -> some View { profilePicker() .allowsHitTesting(!switchingProfileByTimeout) @@ -489,11 +488,11 @@ private struct ActiveProfilePicker: View { } } } - + 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 @@ -501,8 +500,8 @@ private struct ActiveProfilePicker: View { return correctPassword(u, s) } } - - @ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View { + + private func profilerPickerUserOption(_ user: User) -> some View { Button { if selectedProfile == user && incognitoEnabled { incognitoEnabled = false @@ -527,7 +526,7 @@ private struct ActiveProfilePicker: View { } } } - + @ViewBuilder private func profilePicker() -> some View { let incognitoOption = Button { if !incognitoEnabled { @@ -553,14 +552,16 @@ private struct ActiveProfilePicker: View { } } } - + 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 } - + let otherProfiles = filteredProfiles + .filter { u in u.userId != activeProfile?.userId } + .sorted(using: KeyPathComparator(\.activeOrder, order: .reverse)) + if incognitoFirst { incognitoOption profilerPickerUserOption(selectedProfile) @@ -568,7 +569,7 @@ private struct ActiveProfilePicker: View { profilerPickerUserOption(selectedProfile) incognitoOption } - + ForEach(otherProfiles) { p in profilerPickerUserOption(p) } @@ -584,12 +585,13 @@ private struct ActiveProfilePicker: View { } private struct ConnectView: View { + @StateObject private var connectProgressManager = ConnectProgressManager.shared @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var theme: AppTheme @Binding var showQRCodeScanner: Bool @Binding var pastedLink: String @Binding var alert: NewChatViewAlert? - @State private var sheet: PlanAndConnectActionSheet? + @State var scannerPaused: Bool = false @State private var pasteboardHasStrings = UIPasteboard.general.hasStrings var body: some View { @@ -598,39 +600,49 @@ private struct ConnectView: View { pasteLinkView() } Section(header: Text("Or scan QR code").foregroundColor(theme.colors.secondary)) { - ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode) + ScannerInView(showQRCodeScanner: $showQRCodeScanner, scannerPaused: $scannerPaused, processQRCode: processQRCode) } } - .actionSheet(item: $sheet) { s in - planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" }) + .onDisappear { + connectProgressManager.cancelConnectProgress() } } @ViewBuilder private func pasteLinkView() -> some View { if pastedLink == "" { - Button { - if let str = UIPasteboard.general.string { - if let link = strHasSingleSimplexLink(str.trimmingCharacters(in: .whitespaces)) { - pastedLink = link.text - // It would be good to hide it, but right now it is not clear how to release camera in CodeScanner - // https://github.com/twostraws/CodeScanner/issues/121 - // No known tricks worked (changing view ID, wrapping it in another view, etc.) - // showQRCodeScanner = false - connect(pastedLink) - } else { - alert = .newChatSomeAlert(alert: SomeAlert( - alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."), - id: "pasteLinkView: code is not a SimpleX link" - )) + ZStack(alignment: .trailing) { + Button { + if let str = UIPasteboard.general.string { + if let link = strHasSingleSimplexLink(str.trimmingCharacters(in: .whitespaces)) { + pastedLink = link.text + // It would be good to hide it, but right now it is not clear how to release camera in CodeScanner + // https://github.com/twostraws/CodeScanner/issues/121 + // No known tricks worked (changing view ID, wrapping it in another view, etc.) + // showQRCodeScanner = false + connect(pastedLink) + } else { + alert = .newChatSomeAlert(alert: SomeAlert( + alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."), + id: "pasteLinkView: code is not a SimpleX link" + )) + } } + } label: { + Text("Tap to paste link") + } + .disabled(!pasteboardHasStrings) + .frame(maxWidth: .infinity, alignment: .center) + if connectProgressManager.showConnectProgress != nil { + ProgressView() } - } label: { - Text("Tap to paste link") } - .disabled(!pasteboardHasStrings) - .frame(maxWidth: .infinity, alignment: .center) } else { - linkTextView(pastedLink) + HStack { + linkTextView(pastedLink) + if connectProgressManager.showConnectProgress != nil { + ProgressView() + } + } } } @@ -656,18 +668,22 @@ private struct ConnectView: View { } private func connect(_ link: String) { + scannerPaused = true planAndConnect( link, - showAlert: { alert = .planAndConnectAlert(alert: $0) }, - showActionSheet: { sheet = $0 }, + theme: theme, dismiss: true, - incognito: nil + cleanup: { + pastedLink = "" + scannerPaused = false + } ) } } struct ScannerInView: View { @Binding var showQRCodeScanner: Bool + var scannerPaused: Binding? = nil let processQRCode: (_ resp: Result) -> Void @State private var cameraAuthorizationStatus: AVAuthorizationStatus? var scanMode: ScanMode = .continuous @@ -675,7 +691,7 @@ struct ScannerInView: View { var body: some View { Group { if showQRCodeScanner, case .authorized = cameraAuthorizationStatus { - CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode) + CodeScannerView(codeTypes: [.qr], scanMode: scanMode, isPaused: scannerPaused?.wrappedValue ?? false, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) .listRowBackground(Color.clear) @@ -835,311 +851,570 @@ 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?) - - 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)" - } - } +private func showInvitationLinkConnectingAlert(cleanup: (() -> Void)?) { + showAlert( + NSLocalizedString("Already connecting!", comment: "new chat sheet title"), + message: NSLocalizedString("You are already connecting via this one-time link!", comment: "new chat sheet message"), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} + ) } -func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (() -> Void)? = nil) -> Alert { - switch alert { - case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Connect to yourself?"), - message: Text("This is your own one-time link!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case .invitationLinkConnecting: - return Alert( - title: Text("Already connecting!"), - message: Text("You are already connecting via this one-time link!"), - dismissButton: .default(Text("OK")) { cleanup?() } - ) - case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Connect to yourself?"), - message: Text("This is your own SimpleX address!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Repeat connection request?"), - message: Text("You have already requested connection via this address!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Join group?"), - message: Text("You will connect to all group members."), - primaryButton: .default( - Text(incognito ? "Join incognito" : "Join"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Repeat join request?"), - message: Text("You are already joining the group via this link!"), - primaryButton: .destructive( - Text(incognito ? "Join incognito" : "Join"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConnecting(_, groupInfo): - if let groupInfo = groupInfo { - return groupInfo.businessChat == nil - ? Alert( - title: Text("Group already exists!"), - message: Text("You are already joining the group \(groupInfo.displayName)."), - dismissButton: .default(Text("OK")) { cleanup?() } - ) - : Alert( - title: Text("Chat already exists!"), - message: Text("You are already connecting to \(groupInfo.displayName)."), - dismissButton: .default(Text("OK")) { cleanup?() } +private func showGroupLinkConnectingAlert(groupInfo: GroupInfo?, cleanup: (() -> Void)?) { + if let groupInfo = groupInfo { + if groupInfo.businessChat == nil { + showAlert( + NSLocalizedString("Group already exists!", comment: "new chat sheet title"), + message: + String.localizedStringWithFormat( + NSLocalizedString("You are already joining the group %@.", comment: "new chat sheet message"), + groupInfo.displayName + ), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} ) } else { - return Alert( - title: Text("Already joining the group!"), - message: Text("You are already joining the group via this link."), - dismissButton: .default(Text("OK")) { cleanup?() } + showAlert( + NSLocalizedString("Chat already exists!", comment: "new chat sheet title"), + message: + String.localizedStringWithFormat( + NSLocalizedString("You are already connecting to %@.", comment: "new chat sheet message"), + groupInfo.displayName + ), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} ) } + } else { + showAlert( + NSLocalizedString("Already joining the group!", comment: "new chat sheet title"), + message: NSLocalizedString("You are already joining the group via this link.", comment: "new chat sheet message"), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} + ) } } -enum PlanAndConnectActionSheet: Identifiable { - case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact) - case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) - - var id: String { - switch self { - case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)" - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)" - case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)" - case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)" +private func okCleanupAlertAction(cleanup: (() -> Void)?) -> UIAlertAction { + UIAlertAction( + title: NSLocalizedString("Ok", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() } - } + ) } -func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool, cleanup: (() -> Void)? = nil) -> ActionSheet { - switch sheet { - case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title): - return ActionSheet( - title: Text(title), - buttons: [ - .default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title): - return ActionSheet( - title: Text(title), - buttons: [ - .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): - return ActionSheet( - title: Text("Connect with \(contact.chatViewName)"), - buttons: [ - .default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo): - if let incognito = incognito { - return ActionSheet( - title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), - buttons: [ - .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, - .destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] +private func showAskCurrentOrIncognitoProfileSheet( + title: String, + actionStyle: UIAlertAction.Style = .default, + connectionLink: CreatedConnLink, + connectionPlan: ConnectionPlan?, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + title, + actions: {[ + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: actionStyle, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: actionStyle, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } ) - } else { - return ActionSheet( - title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), - buttons: [ - .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, - .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] + ]} + ) +} + +private func showAskCurrentOrIncognitoProfileConnectContactViaAddressSheet( + contact: Contact, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + String.localizedStringWithFormat( + NSLocalizedString("Connect with %@", comment: "new chat action"), + contact.chatViewName + ), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: .default, + handler: { _ in + connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: .default, + handler: { _ in + connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } ) + ]} + ) +} + +private func showOwnGroupLinkConfirmConnectSheet( + groupInfo: GroupInfo, + connectionLink: CreatedConnLink, + connectionPlan: ConnectionPlan?, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + String.localizedStringWithFormat( + NSLocalizedString("Join your group?\nThis is your link for group %@!", comment: "new chat action"), + groupInfo.displayName + ), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Open group", comment: "new chat action"), + style: .default, + handler: { _ in + openKnownGroup(groupInfo, dismiss: dismiss, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: .destructive, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: .destructive, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } + ) + ]} + ) +} + +private func showPrepareContactAlert( + connectionLink: CreatedConnLink, + contactShortLinkData: ContactShortLinkData, + theme: AppTheme, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showOpenChatAlert( + profileName: contactShortLinkData.profile.displayName, + profileFullName: contactShortLinkData.profile.fullName, + profileImage: + ProfileImage( + imageStr: contactShortLinkData.profile.image, + iconName: contactShortLinkData.business + ? "briefcase.circle.fill" + : contactShortLinkData.profile.peerType == .bot + ? "cube.fill" + : "person.crop.circle.fill", + size: alertProfileImageSize + ), + theme: theme, + cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), + confirmTitle: NSLocalizedString("Open new chat", comment: "new chat action"), + onCancel: { cleanup?() }, + onConfirm: { + Task { + do { + let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData) + await MainActor.run { + ChatModel.shared.addChat(Chat(chat)) + openKnownChat(chat.id, dismiss: dismiss, cleanup: cleanup) + } + } catch let error { + logger.error("showPrepareContactAlert apiPrepareContact error: \(error.localizedDescription)") + showAlert(NSLocalizedString("Error opening chat", comment: ""), message: responseError(error)) + await MainActor.run { + cleanup?() + } + } + } } - } + ) +} + +private func showPrepareGroupAlert( + connectionLink: CreatedConnLink, + groupShortLinkData: GroupShortLinkData, + theme: AppTheme, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showOpenChatAlert( + profileName: groupShortLinkData.groupProfile.displayName, + profileFullName: groupShortLinkData.groupProfile.fullName, + profileImage: ProfileImage(imageStr: groupShortLinkData.groupProfile.image, iconName: "person.2.circle.fill", size: alertProfileImageSize), + theme: theme, + cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), + confirmTitle: NSLocalizedString("Open new group", comment: "new chat action"), + onCancel: { cleanup?() }, + onConfirm: { + Task { + do { + let chat = try await apiPrepareGroup(connLink: connectionLink, groupShortLinkData: groupShortLinkData) + await MainActor.run { + ChatModel.shared.addChat(Chat(chat)) + openKnownChat(chat.id, dismiss: dismiss, cleanup: cleanup) + } + } catch let error { + logger.error("showPrepareGroupAlert apiPrepareGroup error: \(error.localizedDescription)") + showAlert(NSLocalizedString("Error opening group", comment: ""), message: responseError(error)) + await MainActor.run { + cleanup?() + } + } + } + } + ) +} + +private func showOpenKnownContactAlert( + _ contact: Contact, + theme: AppTheme, + dismiss: Bool +) { + showOpenChatAlert( + profileName: contact.profile.displayName, + profileFullName: contact.profile.fullName, + profileImage: + ProfileImage( + imageStr: contact.profile.image, + iconName: contact.chatIconName, + size: alertProfileImageSize + ), + theme: theme, + cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), + confirmTitle: + contact.nextConnectPrepared + ? NSLocalizedString("Open new chat", comment: "new chat action") + : NSLocalizedString("Open chat", comment: "new chat action"), + onConfirm: { + openKnownContact(contact, dismiss: dismiss, cleanup: nil) + } + ) +} + +private func showOpenKnownGroupAlert( + _ groupInfo: GroupInfo, + theme: AppTheme, + dismiss: Bool +) { + showOpenChatAlert( + profileName: groupInfo.groupProfile.displayName, + profileFullName: groupInfo.groupProfile.fullName, + profileImage: + ProfileImage( + imageStr: groupInfo.groupProfile.image, + iconName: groupInfo.chatIconName, + size: alertProfileImageSize + ), + theme: theme, + cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), + confirmTitle: + groupInfo.businessChat == nil + ? ( groupInfo.nextConnectPrepared + ? NSLocalizedString("Open new group", comment: "new chat action") + : NSLocalizedString("Open group", comment: "new chat action") + ) + : ( groupInfo.nextConnectPrepared + ? NSLocalizedString("Open new chat", comment: "new chat action") + : NSLocalizedString("Open chat", comment: "new chat action") + ), + onConfirm: { + openKnownGroup(groupInfo, dismiss: dismiss, cleanup: nil) + } + ) } func planAndConnect( - _ connectionLink: String, - showAlert: @escaping (PlanAndConnectAlert) -> Void, - showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void, + _ shortOrFullLink: String, + theme: AppTheme, dismiss: Bool, - incognito: Bool?, cleanup: (() -> Void)? = nil, filterKnownContact: ((Contact) -> Void)? = nil, filterKnownGroup: ((GroupInfo) -> Void)? = nil ) { - Task { - do { - let connectionPlan = try await apiConnectPlan(connReq: connectionLink) - switch connectionPlan { - case let .invitationLink(ilp): - switch ilp { - case .ok: - logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) - } - case .ownLink: - logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) - } - case let .connecting(contact_): - logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")") - if let contact = contact_ { - if let f = filterKnownContact { - f(contact) + ConnectProgressManager.shared.cancelConnectProgress() + let inProgress = BoxedValue(true) + connectTask(inProgress) + ConnectProgressManager.shared.startConnectProgress(NSLocalizedString("Loading profile…", comment: "in progress text")) { + inProgress.boxedValue = false + cleanup?() + } + + func connectTask(_ inProgress: BoxedValue) { + Task { + let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, inProgress: inProgress) + await MainActor.run { + ConnectProgressManager.shared.stopConnectProgress() + } + if !inProgress.boxedValue { return } + if let (connectionLink, connectionPlan) = result { + switch connectionPlan { + case let .invitationLink(ilp): + switch ilp { + case let .ok(contactSLinkData_): + if let contactSLinkData = contactSLinkData_ { + logger.debug("planAndConnect, .invitationLink, .ok, short link data present") + await MainActor.run { + showPrepareContactAlert( + connectionLink: connectionLink, + contactShortLinkData: contactSLinkData, + theme: theme, + dismiss: dismiss, + cleanup: cleanup + ) + } } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + logger.debug("planAndConnect, .invitationLink, .ok, no short link data") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } + } + case .ownLink: + logger.debug("planAndConnect, .invitationLink, .ownLink") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect to yourself?\nThis is your own one-time link!", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } + case let .connecting(contact_): + logger.debug("planAndConnect, .invitationLink, .connecting") + await MainActor.run { + if let contact = contact_ { + if let f = filterKnownContact { + f(contact) + } else { + showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss) + } + } else { + showInvitationLinkConnectingAlert(cleanup: cleanup) + } + } + case let .known(contact): + logger.debug("planAndConnect, .invitationLink, .known") + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss) + } + } + } + case let .contactAddress(cap): + switch cap { + case let .ok(contactSLinkData_): + if let contactSLinkData = contactSLinkData_ { + logger.debug("planAndConnect, .contactAddress, .ok, short link data present") + await MainActor.run { + showPrepareContactAlert( + connectionLink: connectionLink, + contactShortLinkData: contactSLinkData, + theme: theme, + dismiss: dismiss, + cleanup: cleanup + ) + } + } else { + logger.debug("planAndConnect, .contactAddress, .ok, no short link data") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } + } + case .ownLink: + logger.debug("planAndConnect, .contactAddress, .ownLink") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect to yourself?\nThis is your own SimpleX address!", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } + case .connectingConfirmReconnect: + logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("You have already requested connection!\nRepeat connection request?", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } + case let .connectingProhibit(contact): + logger.debug("planAndConnect, .contactAddress, .connectingProhibit") + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss) + } + } + case let .known(contact): + logger.debug("planAndConnect, .contactAddress, .known") + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss) + } + } + case let .contactViaAddress(contact): + logger.debug("planAndConnect, .contactAddress, .contactViaAddress") + await MainActor.run { + showAskCurrentOrIncognitoProfileConnectContactViaAddressSheet( + contact: contact, + dismiss: dismiss, + cleanup: cleanup + ) + } + } + case let .groupLink(glp): + switch glp { + case let .ok(groupSLinkData_): + if let groupSLinkData = groupSLinkData_ { + logger.debug("planAndConnect, .groupLink, .ok, short link data present") + await MainActor.run { + showPrepareGroupAlert( + connectionLink: connectionLink, + groupShortLinkData: groupSLinkData, + theme: theme, + dismiss: dismiss, + cleanup: cleanup + ) + } + } else { + logger.debug("planAndConnect, .groupLink, .ok, no short link data") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Join group", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } + } + case let .ownLink(groupInfo): + logger.debug("planAndConnect, .groupLink, .ownLink") + await MainActor.run { + if let f = filterKnownGroup { + f(groupInfo) + } + showOwnGroupLinkConfirmConnectSheet( + groupInfo: groupInfo, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } + case .connectingConfirmReconnect: + logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("You are already joining the group!\nRepeat join request?", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } + case let .connectingProhibit(groupInfo_): + logger.debug("planAndConnect, .groupLink, .connectingProhibit") + await MainActor.run { + showGroupLinkConnectingAlert(groupInfo: groupInfo_, cleanup: cleanup) + } + case let .known(groupInfo): + logger.debug("planAndConnect, .groupLink, .known") + await MainActor.run { + if let f = filterKnownGroup { + f(groupInfo) + } else { + showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss) + } + } + } + case let .error(chatError): + logger.debug("planAndConnect, .error \(chatErrorString(chatError))") + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via link", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: nil, + dismiss: dismiss, + cleanup: cleanup + ) + } + } else { + await MainActor.run { + if let alert { + dismissAllSheets(animated: true) { + AlertManager.shared.showAlert(alert) + cleanup?() } } 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)) } + cleanup?() } } - case let .contactAddress(cap): - switch cap { - case .ok: - logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) - } - case .ownLink: - logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) - } - case .connectingConfirmReconnect: - logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) - } - case let .connectingProhibit(contact): - logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") - 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)) } - } - 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)) - } - } - case let .groupLink(glp): - switch glp { - case .ok: - if let incognito = incognito { - showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group")) - } - case let .ownLink(groupInfo): - logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownGroup { - f(groupInfo) - } - showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) - case .connectingConfirmReconnect: - logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) - } - case let .connectingProhibit(groupInfo_): - logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") - showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) - case let .known(groupInfo): - logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownGroup { - f(groupInfo) - } else { - openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) } - } - } - } - } catch { - logger.debug("planAndConnect, plan error") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link")) } } } @@ -1161,22 +1436,22 @@ private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incogn } 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 { @@ -1198,41 +1473,29 @@ 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) { - ItemsModel.shared.loadOpenChat(c.id) - showAlreadyExistsAlert?() - } - } else { - ItemsModel.shared.loadOpenChat(c.id) - showAlreadyExistsAlert?() - } - } - } +func openKnownContact(_ contact: Contact, dismiss: Bool, cleanup: (() -> Void)?) { + if let c = ChatModel.shared.getContactChat(contact.contactId) { + openKnownChat(c.id, dismiss: dismiss, cleanup: cleanup) } } -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) { - ItemsModel.shared.loadOpenChat(g.id) - showAlreadyExistsAlert?() - } - } else { - ItemsModel.shared.loadOpenChat(g.id) - showAlreadyExistsAlert?() - } +func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, cleanup: (() -> Void)?) { + if let g = ChatModel.shared.getGroupChat(groupInfo.groupId) { + openKnownChat(g.id, dismiss: dismiss, cleanup: cleanup) + } +} + +func openKnownChat(_ chatId: ChatId, dismiss: Bool, cleanup: (() -> Void)?) { + if dismiss { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(chatId) { + cleanup?() } } + } else { + ItemsModel.shared.loadOpenChat(chatId) { + cleanup?() + } } } @@ -1269,11 +1532,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 } } diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index bc1dc4b5bc..c9054f30da 100644 --- a/apps/ios/Shared/Views/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/NewChat/QRCode.swift @@ -8,18 +8,30 @@ import SwiftUI import CoreImage.CIFilterBuiltins +import SimpleXChat struct MutableQRCode: View { @Binding var uri: String + var small: Bool = false var withLogo: Bool = true var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) var body: some View { - QRCode(uri: uri, withLogo: withLogo, tintColor: tintColor) + QRCode(uri: uri, small: small, withLogo: withLogo, tintColor: tintColor) .id("simplex-qrcode-view-for-\(uri)") } } +struct SimpleXCreatedLinkQRCode: View { + let link: CreatedConnLink + @Binding var short: Bool + var onShare: (() -> Void)? = nil + + var body: some View { + QRCode(uri: link.simplexChatUri(short: short), small: short && link.connShortLink != nil, onShare: onShare) + } +} + struct SimpleXLinkQRCode: View { let uri: String var withLogo: Bool = true @@ -27,56 +39,57 @@ struct SimpleXLinkQRCode: View { var onShare: (() -> Void)? = nil var body: some View { - QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor, onShare: onShare) + QRCode(uri: simplexChatLink(uri), small: uri.count < 200, withLogo: withLogo, tintColor: tintColor, onShare: onShare) } } -func simplexChatLink(_ uri: String) -> String { - uri.starts(with: "simplex:/") - ? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") - : uri -} +private let smallQRRatio: CGFloat = 0.63 struct QRCode: View { let uri: String + var small: Bool = false var withLogo: Bool = true var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) var onShare: (() -> Void)? = nil @State private var image: UIImage? = nil @State private var makeScreenshotFunc: () -> Void = {} + @State private var width: CGFloat = .infinity var body: some View { ZStack { if let image = image { - qrCodeImage(image) - GeometryReader { geo in + qrCodeImage(image).frame(width: width, height: width) + GeometryReader { g in + let w = g.size.width * (small ? smallQRRatio : 1) + let l = w * (small ? 0.195 : 0.16) + let m = w * 0.005 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) + .frame(width: l, height: l) + .frame(width: l + m, height: l + m) .background(.white) .clipShape(Circle()) } } .onAppear { + width = w makeScreenshotFunc = { let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale) - showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)]) + showShareSheet(items: [makeScreenshot(g.frame(in: .local).origin, size)]) onShare?() } } - .frame(width: geo.size.width, height: geo.size.height) + .frame(width: g.size.width, height: g.size.height) } } else { - Color.clear.aspectRatio(1, contentMode: .fit) + Color.clear.aspectRatio(small ? 1 / smallQRRatio : 1, contentMode: .fit) } } .onTapGesture(perform: makeScreenshotFunc) - .task { image = await generateImage(uri, tintColor: tintColor) } + .task { image = await generateImage(uri, tintColor: tintColor, errorLevel: small ? "M" : "L") } .frame(maxWidth: .infinity, maxHeight: .infinity) } } @@ -89,10 +102,11 @@ private func qrCodeImage(_ image: UIImage) -> some View { .textSelection(.enabled) } -private func generateImage(_ uri: String, tintColor: UIColor) async -> UIImage? { +private func generateImage(_ uri: String, tintColor: UIColor, errorLevel: String) async -> UIImage? { let context = CIContext() let filter = CIFilter.qrCodeGenerator() filter.message = Data(uri.utf8) + filter.correctionLevel = errorLevel if let outputImage = filter.outputImage, let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { return UIImage(cgImage: cgImage).replaceColor(UIColor.black, tintColor) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 1a0a736acd..33ffa04a50 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -43,110 +43,69 @@ struct OnboardingButtonStyle: ButtonStyle { } } -private enum ChooseServerOperatorsSheet: Identifiable { - case showInfo +private enum OnboardingConditionsViewSheet: Identifiable { case showConditions + case configureOperators var id: String { switch self { - case .showInfo: return "showInfo" case .showConditions: return "showConditions" + case .configureOperators: return "configureOperators" } } } -struct ChooseServerOperators: View { - @Environment(\.dismiss) var dismiss: DismissAction - @Environment(\.colorScheme) var colorScheme: ColorScheme +struct OnboardingConditionsView: View { @EnvironmentObject var theme: AppTheme - var onboarding: Bool @State private var serverOperators: [ServerOperator] = [] @State private var selectedOperatorIds = Set() - @State private var sheetItem: ChooseServerOperatorsSheet? = nil + @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 - ScrollView { + let v = ScrollView { VStack(alignment: .leading, spacing: 20) { - let title = Text("Server operators") + Text("Conditions of use") .font(.largeTitle) .bold() .frame(maxWidth: .infinity, alignment: .center) - - if onboarding { - title.padding(.top, 25) - } else { - title - } - - infoText() - .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 25) Spacer() - - ForEach(serverOperators) { srvOperator in - operatorCheckView(srvOperator) + + 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) } - 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) - + .padding(.horizontal, 4) + Spacer() - - let reviewForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } - let canReviewLater = reviewForOperators.allSatisfy { $0.conditionsAcceptance.usageAllowed } - let currEnabledOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) - - VStack(spacing: 8) { - if !reviewForOperators.isEmpty { - reviewConditionsButton() - } else if selectedOperatorIds != currEnabledOperatorIds && !selectedOperatorIds.isEmpty { - setOperatorsButton() - } else { - continueButton() + + VStack(spacing: 12) { + acceptConditionsButton() + + Button("Configure server operators") { + sheetItem = .configureOperators } - if onboarding { - Group { - if reviewForOperators.isEmpty { - Button("Conditions of use") { - sheetItem = .showConditions - } - } else { - Text("Conditions of use") - .foregroundColor(.clear) - } - } - .font(.system(size: 17, weight: .semibold)) - .frame(minHeight: 40) - } - } - - if !onboarding && !reviewForOperators.isEmpty { - VStack(spacing: 8) { - reviewLaterButton() - ( - Text("Conditions will be accepted for enabled operators after 30 days.") - + textSpace - + Text("You can configure operators in Network & servers settings.") - ) - .multilineTextAlignment(.center) - .font(.footnote) - .padding(.horizontal, 32) - } - .frame(maxWidth: .infinity) - .disabled(!canReviewLater) - .padding(.bottom) + .frame(minHeight: 40) } } + .padding(25) .frame(minHeight: g.size.height) } .onAppear { @@ -158,130 +117,28 @@ struct ChooseServerOperators: View { } .sheet(item: $sheetItem) { item in switch item { - case .showInfo: - ChooseServerOperatorsInfoView() case .showConditions: - UsageConditionsView( - currUserServers: Binding.constant([]), - userServers: Binding.constant([]) - ) - .modifier(ThemedBackground(grouped: true)) + 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) - .padding(onboarding ? 25 : 16) - } - - private func infoText() -> some View { - Button { - sheetItem = .showInfo - } label: { - Label("How it helps privacy", systemImage: "info.circle") - .font(.headline) - } - } - - @ViewBuilder 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) - 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 reviewConditionsButton() -> some View { - NavigationLink("Review conditions") { - reviewConditionsView() - .navigationTitle("Conditions of use") - .navigationBarTitleDisplayMode(.large) - .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } - .modifier(ThemedBackground(grouped: true)) - } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) - } - - private func setOperatorsButton() -> some View { - notificationsModeNavLinkButton { - Button { - Task { - if let enabledOperators = enabledOperators(serverOperators) { - let r = try await setServerOperators(operators: enabledOperators) - await MainActor.run { - ChatModel.shared.conditions = r - continueToNextStep() - } - } else { - await MainActor.run { - continueToNextStep() - } - } - } - } label: { - Text("Update") - } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) - } - } - - private func continueButton() -> some View { - notificationsModeNavLinkButton { - Button { - continueToNextStep() - } label: { - Text("Continue") - } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) - } - } - - private func reviewLaterButton() -> some View { - notificationsModeNavLinkButton { - Button { - continueToNextStep() - } label: { - Text("Review later") - } - .buttonStyle(.borderless) - } + .navigationBarHidden(true) // necessary on iOS 15 } private func continueToNextStep() { - if onboarding { - onboardingStageDefault.set(.step4_SetNotificationsMode) - notificationsModeNavLinkActive = true - } else { - dismiss() - } + onboardingStageDefault.set(.step4_SetNotificationsMode) + notificationsModeNavLinkActive = true } func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View { @@ -304,34 +161,13 @@ struct ChooseServerOperators: View { .modifier(ThemedBackground()) } - @ViewBuilder private func reviewConditionsView() -> some View { - let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted } - let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } - VStack(alignment: .leading, spacing: 20) { - if !operatorsWithConditionsAccepted.isEmpty { - Text("Conditions are already accepted for these operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") - Text("The same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") - } else { - Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") - } - ConditionsTextView() - .frame(maxHeight: .infinity) - acceptConditionsButton() - .padding(.bottom) - .padding(.bottom) - } - .padding(.horizontal, 25) - } - 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) + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: Array(selectedOperatorIds)) await MainActor.run { ChatModel.shared.conditions = r } @@ -356,9 +192,10 @@ struct ChooseServerOperators: View { } } } label: { - Text("Accept conditions") + Text("Accept") } - .buttonStyle(OnboardingButtonStyle()) + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) } } @@ -393,6 +230,126 @@ struct ChooseServerOperators: View { } } +private enum ChooseServerOperatorsSheet: Identifiable { + case showInfo + + var id: String { + switch self { + case .showInfo: return "showInfo" + } + } +} + +struct ChooseServerOperators: View { + @Environment(\.dismiss) var dismiss: DismissAction + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + var serverOperators: [ServerOperator] + @Binding var selectedOperatorIds: Set + @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 { @@ -447,5 +404,5 @@ struct ChooseServerOperatorsInfoView: View { } #Preview { - ChooseServerOperators(onboarding: true) + OnboardingConditionsView() } diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 409cb859ea..f119beec50 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -25,10 +25,13 @@ enum UserProfileAlert: Identifiable { } } +let MAX_BIO_LENGTH_BYTES = 160 + struct CreateProfile: View { @Environment(\.dismiss) var dismiss @EnvironmentObject var theme: AppTheme @State private var displayName: String = "" + @State private var profileBio: String = "" @FocusState private var focusDisplayName @State private var alert: UserProfileAlert? @@ -37,12 +40,13 @@ struct CreateProfile: View { Section { TextField("Enter your name…", text: $displayName) .focused($focusDisplayName) + TextField("Bio", text: $profileBio) Button { createProfile() } label: { Label("Create profile", systemImage: "checkmark") } - .disabled(!canCreateProfile(displayName)) + .disabled(!canCreateProfile(displayName) || !bioFitsLimit()) } header: { HStack { Text("Your profile") @@ -52,18 +56,20 @@ struct CreateProfile: View { let validName = mkValidName(name) if name != validName { Spacer() - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .onTapGesture { - alert = .invalidNameError(validName: validName) - } + validationErrorIndicator { + alert = .invalidNameError(validName: validName) + } + } else if !bioFitsLimit() { + Spacer() + validationErrorIndicator { + showAlert(NSLocalizedString("Bio too large", comment: "alert title")) + } } } .frame(height: 20) } footer: { VStack(alignment: .leading, spacing: 8) { - Text("Your profile, 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) @@ -79,11 +85,25 @@ struct CreateProfile: View { } } + private func validationErrorIndicator(_ onTap: @escaping () -> Void) -> some View { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .onTapGesture { + onTap() + } + } + + private func bioFitsLimit() -> Bool { + chatJsonLength(profileBio) <= MAX_BIO_LENGTH_BYTES + } + private func createProfile() { hideKeyboard() + let shortDescr: String? = if profileBio.isEmpty { nil } else { profileBio } let profile = Profile( displayName: displayName.trimmingCharacters(in: .whitespaces), - fullName: "" + fullName: "", + shortDescr: shortDescr ) let m = ChatModel.shared do { @@ -118,25 +138,22 @@ struct CreateFirstProfile: View { @State private var nextStepNavLinkActive = false var body: some View { - VStack(alignment: .leading, spacing: 20) { - VStack(alignment: .center, spacing: 20) { - Text("Create your profile") + let v = VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .center, spacing: 16) { + Text("Create profile") .font(.largeTitle) .bold() .multilineTextAlignment(.center) - - Text("Your profile, contacts and delivered messages are stored on your device.") - .font(.callout) - .foregroundColor(theme.colors.secondary) - .multilineTextAlignment(.center) - - Text("The profile is only shared with your contacts.") + + 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) @@ -145,6 +162,7 @@ struct CreateFirstProfile: View { TextField("Enter your name…", text: $displayName) .focused($focusDisplayName) .padding(.horizontal) + .padding(.trailing, 20) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 10, style: .continuous) @@ -173,12 +191,23 @@ struct CreateFirstProfile: View { } } .onAppear() { - focusDisplayName = true + if #available(iOS 16, *) { + focusDisplayName = true + } else { + // it does not work before animation completes on iOS 15 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + focusDisplayName = true + } + } } .padding(.horizontal, 25) - .padding(.top, 10) .padding(.bottom, 25) .frame(maxWidth: .infinity, alignment: .leading) + if #available(iOS 16, *) { + return v.padding(.top, 10) + } else { + return v.padding(.top, 75).ignoresSafeArea(.all, edges: .top) + } } func createProfileButton() -> some View { @@ -206,7 +235,7 @@ struct CreateFirstProfile: View { } private func nextStepDestinationView() -> some View { - ChooseServerOperators(onboarding: true) + OnboardingConditionsView() .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) } @@ -235,15 +264,15 @@ private func showCreateProfileAlert( _ error: Error ) { let m = ChatModel.shared - switch error as? ChatResponse { - case .chatCmdError(_, .errorStore(.duplicateName)), - .chatCmdError(_, .error(.userExists)): + switch error as? ChatError { + case .errorStore(.duplicateName), + .error(.userExists): if m.currentUser == nil { AlertManager.shared.showAlert(duplicateUserAlert) } else { showAlert(.duplicateUserError) } - case .chatCmdError(_, .error(.invalidDisplayName)): + case .error(.invalidDisplayName): if m.currentUser == nil { AlertManager.shared.showAlert(invalidDisplayNameAlert) } else { diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index befb34b318..03b0fcba1a 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,10 @@ struct CreateSimpleXAddress: View { progressIndicator = true Task { do { - let connReqContact = try await apiCreateUserAddress() - DispatchQueue.main.async { - m.userAddress = UserContactLink(connReqContact: connReqContact) + if let connLinkContact = try await apiCreateUserAddress() { + DispatchQueue.main.async { + m.userAddress = UserContactLink(connLinkContact) + } } await MainActor.run { progressIndicator = false } } catch let error { @@ -121,7 +122,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 +190,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/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index b2b1b8fa68..8f448dc508 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -23,7 +23,7 @@ struct OnboardingView: View { case .step3_CreateSimpleXAddress: // deprecated CreateSimpleXAddress() case .step3_ChooseServerOperators: - ChooseServerOperators(onboarding: true) + OnboardingConditionsView() .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) case .step4_SetNotificationsMode: @@ -44,7 +44,7 @@ enum OnboardingStage: String, Identifiable { case step1_SimpleXInfo case step2_CreateProfile // deprecated case step3_CreateSimpleXAddress // deprecated - case step3_ChooseServerOperators + 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 97e1f49382..31865e7af9 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -17,7 +17,7 @@ struct SetNotificationsMode: View { var body: some View { GeometryReader { g in - ScrollView { + let v = ScrollView { VStack(alignment: .center, spacing: 20) { Text("Push notifications") .font(.largeTitle) @@ -57,11 +57,17 @@ struct SetNotificationsMode: View { .padding(25) .frame(minHeight: g.size.height) } + if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v + } } .frame(maxHeight: .infinity) .sheet(isPresented: $showInfo) { NotificationsInfoView() } + .navigationBarHidden(true) // necessary on iOS 15 } private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 40dd29db53..9f41a37b1d 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -18,7 +18,7 @@ struct SimpleXInfo: View { var body: some View { GeometryReader { g in - ScrollView { + let v = ScrollView { VStack(alignment: .leading) { VStack(alignment: .center, spacing: 10) { Image(colorScheme == .light ? "logo" : "logo-light") @@ -36,7 +36,7 @@ struct SimpleXInfo: View { .font(.headline) } } - + Spacer() VStack(alignment: .leading) { @@ -66,6 +66,9 @@ struct SimpleXInfo: View { } } } + .padding(.horizontal, 25) + .padding(.top, 75) + .padding(.bottom, 25) .frame(minHeight: g.size.height) } .sheet(isPresented: Binding( @@ -88,14 +91,17 @@ struct SimpleXInfo: View { createProfileNavLinkActive: $createProfileNavLinkActive ) } + if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v + } } .onAppear() { setLastVersionDefault() } .frame(maxHeight: .infinity) - .padding(.horizontal, 25) - .padding(.top, 75) - .padding(.bottom, 25) + .navigationBarHidden(true) // necessary on iOS 15 } private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { @@ -129,6 +135,7 @@ struct SimpleXInfo: View { NavigationLink(isActive: $createProfileNavLinkActive) { CreateFirstProfile() + .modifier(ThemedBackground()) } label: { EmptyView() } @@ -140,6 +147,8 @@ struct SimpleXInfo: View { let textSpace = Text(verbatim: " ") +let textNewLine = Text(verbatim: "\n") + struct SimpleXInfo_Previews: PreviewProvider { static var previews: some View { SimpleXInfo(onboarding: true) diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 182c5652d7..916e3f9e78 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -539,7 +539,98 @@ private let versionDescriptions: [VersionDescription] = [ 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.") + ] + )), + ] + ), + VersionDescription( + version: "v6.4", + post: URL(string: "https://simplex.chat/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.html"), + features: [ + .feature(Description( + icon: "person", + title: "Connect faster! 🚀", + description: "Message instantly once you tap Connect." + )), + .feature(Description( + icon: { if #available(iOS 17, *) {"person.bubble"} else {"person.crop.square"} }(), + title: "Review group members", + description: "Chat with members before they join." + )), + .feature(Description( + icon: { if #available(iOS 16, *) {"questionmark.bubble"} else {"questionmark.square"} }(), + title: "Chat with admins", + description: "Send your private feedback to groups." + )), + .feature(Description( + icon: "flag", + title: "New group role: Moderator", + description: "Removes messages and blocks members." + )), + .feature(Description( + icon: "battery.50", + title: "Improved message delivery", + description: "Less traffic on mobile networks." + )), + ] + ), + VersionDescription( + version: "v6.4.1", + post: URL(string: "https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html"), + features: [ + .feature(Description( + icon: "hand.wave", + title: "Welcome your contacts 👋", + description: "Set profile bio and welcome message." + )), + .feature(Description( + icon: "stopwatch", + title: "Keep your chats clean", + description: "Enable disappearing messages by default." + )), + .view(FeatureView( + icon: nil, + title: "Short SimpleX address", + view: { CreateUpdateAddressShortLink() } + )) + ] + ), ] private let lastVersion = versionDescriptions.last!.version @@ -555,8 +646,6 @@ func shouldShowWhatsNew() -> Bool { } fileprivate struct NewOperatorsView: View { - @State private var showOperatorsSheet = false - var body: some View { VStack(alignment: .leading) { Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo) @@ -567,16 +656,52 @@ fileprivate struct NewOperatorsView: View { .multilineTextAlignment(.leading) .lineLimit(10) HStack { - Button("Enable Flux") { - showOperatorsSheet = true - } - Text("for better metadata privacy.") + Text("Enable Flux in Network & servers settings for better metadata privacy.") } } - .sheet(isPresented: $showOperatorsSheet) { + } +} + +fileprivate struct CreateUpdateAddressShortLink: View { + @EnvironmentObject private var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var showAddressSheet = false + @State private var progressIndicator = false + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 4) { + Image(systemName: "link") + .symbolRenderingMode(.monochrome) + .foregroundColor(theme.colors.secondary) + .frame(minWidth: 30, alignment: .center) + Text("Short SimpleX address").font(.title3).bold() + } + Group { + if let addr = chatModel.userAddress { + if addr.shouldBeUpgraded { + HStack(spacing: 8) { + Button("Upgrade your address") { upgradeAndShareAddressAlert(progressIndicator: $progressIndicator) } + if progressIndicator { + ProgressView() + } + } + } else { + Button("Share your address") { addr.shareAddress(short: true) } + } + } else { + Button("Create your address") { showAddressSheet = true } + } + } + .multilineTextAlignment(.leading) + .lineLimit(10) + } + .sheet(isPresented: $showAddressSheet) { NavigationView { - ChooseServerOperators(onboarding: false) - .modifier(ThemedBackground()) + UserAddressView(autoCreate: true) + .navigationTitle("SimpleX address") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) } } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 67020e09e7..01b25baed8 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -456,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 23e1f783f7..ac143fe044 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,10 +98,12 @@ struct TerminalView: View { SendMessageView( composeState: $composeState, + selectedRange: $selectedRange, sendMessage: { _ in consoleSendMessage() }, showVoiceMessageButton: false, onMediaAdded: { _ in }, - keyboardVisible: $keyboardVisible + keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate ) .padding(.horizontal, 12) } @@ -141,18 +145,18 @@ struct TerminalView: View { } 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 } } } } @@ -160,12 +164,38 @@ struct TerminalView: View { } } +func sendTerminalCmd(_ cmd: String) async { + let start: Date = .now + await withCheckedContinuation { (cont: CheckedContinuation) in + let d = sendSimpleXCmdStr(cmd, retryNum: 0) + 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: "newChatItems", 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/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index 00532c0a8e..44e0b20958 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -38,7 +38,6 @@ extension AppSettings { privacyLinkPreviewsGroupDefault.set(val) def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) } - if let val = privacyChatListOpenLinks { privacyChatListOpenLinksDefault.set(val) } 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) } @@ -78,7 +77,6 @@ extension AppSettings { c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get() c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) - c.privacyChatListOpenLinks = privacyChatListOpenLinksDefault.get() c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index 4c61d592ac..02dec5a618 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -367,13 +367,13 @@ struct ChatThemePreview: View { 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) + ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: alice, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) .modifier(ChatItemClipped(alice, tailVisible: true)) Spacer() } HStack { Spacer() - ChatItemView(chat: Chat.sampleData, chatItem: bob) + ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: bob, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) .modifier(ChatItemClipped(bob, tailVisible: true)) .frame(alignment: .trailing) } diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 54454b7cef..6df2d5422e 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -14,6 +14,7 @@ struct DeveloperView: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false @State private var hintsUnchanged = hintDefaultsUnchanged() + @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @Environment(\.colorScheme) var colorScheme @@ -65,6 +66,21 @@ struct DeveloperView: View { Text("Developer options") } } + Section("Deprecated options") { + settingsRow("link", color: theme.colors.secondary) { + Picker("SimpleX links", selection: $simplexLinkMode) { + ForEach( + SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) + ) { mode in + Text(mode.text) + } + } + } + .frame(height: 36) + .onChange(of: simplexLinkMode) { mode in + privacySimplexLinkModeDefault.set(mode) + } + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift index cf9cada592..71c284e9ab 100644 --- a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift @@ -19,7 +19,7 @@ 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) @@ -39,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/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift index 754ca3cf6b..3a536c7b17 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift @@ -67,7 +67,7 @@ struct AdvancedNetworkSettings: View { Text(netCfg.smpProxyMode.label) } } - + NavigationLink { List { Section { @@ -192,25 +192,38 @@ struct AdvancedNetworkSettings: View { netCfg.requiredHostMode = requiredHostMode } } - + if developerTools { Section { - Picker("Transport isolation", selection: $netCfg.sessionMode) { + WrappedPicker("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 { + WrappedPicker("Use web port", selection: $netCfg.smpWebPortServers) { + ForEach(SMPWebPortServers.allCases, id: \.self) { Text($0.text) } + } + } 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("TCP connection timeout", selection: $netCfg.tcpConnectTimeout.interactiveTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) + timeoutSettingPicker("TCP connection bg timeout", selection: $netCfg.tcpConnectTimeout.backgroundTimeout, values: [30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel) + timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout.interactiveTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000], label: secondsLabel) + timeoutSettingPicker("Protocol background timeout", selection: $netCfg.tcpTimeout.backgroundTimeout, values: [15_000000, 20_000000, 30_000000, 45_000000, 60_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) @@ -230,7 +243,7 @@ struct AdvancedNetworkSettings: View { .foregroundColor(theme.colors.secondary) } } - + Section { Button("Reset to defaults") { updateNetCfgView(NetCfg.defaults, NetworkProxy.def) @@ -241,7 +254,7 @@ struct AdvancedNetworkSettings: View { updateNetCfgView(netCfg.withProxyTimeouts, netProxy) } .disabled(netCfg.hasProxyTimeouts) - + Button("Save and reconnect") { showSettingsAlert = .update } @@ -338,16 +351,15 @@ struct AdvancedNetworkSettings: View { } private func timeoutSettingPicker(_ title: LocalizedStringKey, selection: Binding, values: [Int], label: String) -> some View { - Picker(title, selection: selection) { + WrappedPicker(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." @@ -360,12 +372,12 @@ struct AdvancedNetworkSettings: View { 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 + Text("\n") + Text("New SOCKS credentials will be used every time you start the app.") - case .server: userMode + Text("\n") + Text("New SOCKS credentials will be used for each server.") + 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." @@ -374,7 +386,7 @@ struct AdvancedNetworkSettings: View { 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." diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 16aa98bc5f..6f4710396a 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -238,22 +238,23 @@ struct UsageConditionsView: View { var body: some View { VStack(alignment: .leading, spacing: 20) { - HStack { - Text("Conditions of use").font(.largeTitle).bold() - Spacer() - conditionsLinkButton() - } - .padding(.top) - .padding(.top) - 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) { @@ -265,6 +266,10 @@ struct UsageConditionsView: View { .multilineTextAlignment(.center) .frame(maxWidth: .infinity, alignment: .center) .padding(.horizontal, 32) + conditionsDiffButton(.footnote) + } else { + conditionsDiffButton() + .padding(.top) } } .padding(.bottom) @@ -272,6 +277,9 @@ struct UsageConditionsView: View { 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) @@ -312,6 +320,43 @@ struct UsageConditionsView: View { } } } + + @ViewBuilder private func conditionsDiffButton(_ font: Font? = nil) -> some View { + let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit + if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") { + Link(destination: commitUrl) { + 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]>) { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift index 17a0ffdd1c..c8cb2349e7 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift @@ -65,7 +65,7 @@ struct NewServerView: View { useServerSection(valid) if valid { Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { - MutableQRCode(uri: $serverToEdit.server) + MutableQRCode(uri: $serverToEdit.server, small: true) .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index 24da6a94a8..afbccc109c 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -38,9 +38,9 @@ struct OperatorView: View { .allowsHitTesting(!testing) } - @ViewBuilder private func operatorView() -> some View { + private func operatorView() -> some View { let duplicateHosts = findDuplicateHosts(serverErrors) - VStack { + return VStack { List { Section { infoViewLink() @@ -500,14 +500,14 @@ struct SingleOperatorUsageConditionsView: View { } } - @ViewBuilder private func acceptConditionsButton() -> some View { + 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 } - Button { + return Button { acceptForOperators(operatorIds, operatorIndex) } label: { Text("Accept conditions") diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift index 13d01874ed..97bfd360cb 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift @@ -110,7 +110,7 @@ struct ProtocolServerView: View { useServerSection(valid) if valid { Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { - MutableQRCode(uri: $serverToEdit.server) + MutableQRCode(uri: $serverToEdit.server, small: true) .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift index ed3c5c773c..b9737914ec 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift @@ -38,9 +38,9 @@ struct YourServersView: View { .allowsHitTesting(!testing) } - @ViewBuilder private func yourServersView() -> some View { + private func yourServersView() -> some View { let duplicateHosts = findDuplicateHosts(serverErrors) - List { + return List { if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty { Section { ForEach($userServers[operatorIndex].smpServers) { srv in diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index 4e7f826f4f..c4d0588987 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -13,7 +13,7 @@ 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 @@ -25,7 +25,7 @@ struct NotificationsView: View { ProgressView().scaleEffect(2) } } - .alert(item: $showAlert) { alert in + .alert(item: $ntfAlert) { alert in if let token = m.deviceToken { return notificationAlert(alert, token) } else { @@ -41,7 +41,7 @@ struct NotificationsView: View { List { Section { SelectionListView(list: NotificationsMode.values, selection: $notificationMode) { mode in - showAlert = .setMode(mode: mode) + ntfAlert = .setMode(mode: mode) } } footer: { VStack(alignment: .leading) { @@ -95,7 +95,7 @@ struct NotificationsView: View { if let server = m.notificationServer { smpServers("Push server", [server], theme.colors.secondary) - testServerButton(server) + testTokenButton(server) } } header: { Text("Push notifications") @@ -163,7 +163,7 @@ struct NotificationsView: View { 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: @@ -181,19 +181,19 @@ struct NotificationsView: View { 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 testServerButton(_ server: String) -> some View { + private func testTokenButton(_ server: String) -> some View { HStack { - Button("Test server") { + Button("Test notifications") { testing = true Task { - await testServer(server) + await testServerAndToken(server) await MainActor.run { testing = false } } } @@ -215,22 +215,61 @@ struct NotificationsView: View { } } - private func testServer(_ server: String) async { + private func testServerAndToken(_ server: String) async { do { let r = try await testProtoServer(server: server) switch r { case .success: - await MainActor.run { - testedSuccess = true + 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 { - showAlert = .testFailure(testFailure: f) + ntfAlert = .testFailure(testFailure: f) testedSuccess = false } } } catch let error { - logger.error("testServerConnection \(responseError(error))") + await MainActor.run { + let err = responseError(error) + logger.error("testServerConnection \(err)") + ntfAlert = .error(title: "Error testing server connection", error: err) + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift index bd8171623a..eced372124 100644 --- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift +++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift @@ -19,7 +19,7 @@ struct PreferencesView: View { var body: some View { VStack { List { - timedMessagesFeatureSection($preferences.timedMessages.allow) + timedMessagesFeatureSection($preferences.timedMessages.allow, $preferences.timedMessages.ttl) featureSection(.fullDelete, $preferences.fullDelete.allow) featureSection(.reactions, $preferences.reactions.allow) featureSection(.voice, $preferences.voice.allow) @@ -60,20 +60,35 @@ struct PreferencesView: View { } - private func timedMessagesFeatureSection(_ allowFeature: Binding) -> some View { + @ViewBuilder private func timedMessagesFeatureSection(_ allowFeature: Binding, _ ttl: Binding) -> some View { + let allow = Binding( + get: { allowFeature.wrappedValue == .always || allowFeature.wrappedValue == .yes }, + set: { yes, _ in allowFeature.wrappedValue = yes ? .yes : .no } + ) Section { - let allow = Binding( - get: { allowFeature.wrappedValue == .always || allowFeature.wrappedValue == .yes }, - set: { yes, _ in allowFeature.wrappedValue = yes ? .yes : .no } - ) settingsRow(ChatFeature.timedMessages.icon, color: theme.colors.secondary) { Toggle(ChatFeature.timedMessages.text, isOn: allow) } + if allow.wrappedValue { + Picker("Delete after", selection: ttl) { + ForEach(TimedMessagesPreference.profileLevelTTLValues, id: \.self) { value in + Text(timeText(value)).tag(value) + } + } + .frame(height: 36) + } + } + footer: { + let featureFooterText = featureFooter(.timedMessages, allowFeature).foregroundColor(theme.colors.secondary) + if allow.wrappedValue && ttl.wrappedValue != nil { + featureFooterText + textNewLine + Text("Time to disappear is set only for new contacts.") + } else { + featureFooterText + } } - footer: { featureFooter(.timedMessages, allowFeature).foregroundColor(theme.colors.secondary) } } - private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding) -> some View { + private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding) -> Text { Text(feature.allowDescription(allowFeature.wrappedValue)) } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 0b9d1ef76c..eec820833c 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -14,12 +14,12 @@ struct PrivacySettings: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true - @State private var chatListOpenLinks = privacyChatListOpenLinksDefault.get() + @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true - @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @State private var currentLAMode = privacyLocalAuthModeDefault.get() @@ -32,6 +32,8 @@ struct PrivacySettings: View { @State private var groupReceiptsReset = false @State private var groupReceiptsOverrides = 0 @State private var groupReceiptsDialogue = false + @State private var autoAcceptMemberContacts = false + @State private var autoAcceptMemberContactsReset = false @State private var alert: PrivacySettingsViewAlert? enum PrivacySettingsViewAlert: Identifiable { @@ -73,18 +75,11 @@ struct PrivacySettings: View { Toggle("Send link previews", isOn: $useLinkPreviews) .onChange(of: useLinkPreviews) { linkPreviews in privacyLinkPreviewsGroupDefault.set(linkPreviews) + privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 } } - settingsRow("arrow.up.right.circle", color: theme.colors.secondary) { - Picker("Open links from chat list", selection: $chatListOpenLinks) { - ForEach(PrivacyChatListOpenLinksMode.allCases) { mode in - Text(mode.text) - } - } - } - .frame(height: 36) - .onChange(of: chatListOpenLinks) { mode in - privacyChatListOpenLinksDefault.set(mode) + settingsRow("link", color: theme.colors.secondary) { + Toggle("Remove link tracking", isOn: $privacySanitizeLinks) } settingsRow("message", color: theme.colors.secondary) { Toggle("Show last messages", isOn: $showChatPreviews) @@ -98,19 +93,6 @@ struct PrivacySettings: View { m.draftChatId = nil } } - settingsRow("link", color: theme.colors.secondary) { - Picker("SimpleX links", selection: $simplexLinkMode) { - ForEach( - SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) - ) { mode in - Text(mode.text) - } - } - } - .frame(height: 36) - .onChange(of: simplexLinkMode) { mode in - privacySimplexLinkModeDefault.set(mode) - } } header: { Text("Chats") .foregroundColor(theme.colors.secondary) @@ -130,7 +112,7 @@ struct PrivacySettings: View { } } settingsRow("circle.filled.pattern.diagonalline.rectangle", color: theme.colors.secondary) { - Picker("Blur media", selection: $privacyMediaBlurRadius) { + WrappedPicker("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 { @@ -144,7 +126,6 @@ struct PrivacySettings: View { } } } - .frame(height: 36) settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) { Toggle("Protect IP address", isOn: $askToApproveRelays) } @@ -161,6 +142,18 @@ struct PrivacySettings: View { } } + Section { + settingsRow("checkmark", color: theme.colors.secondary) { + Toggle("Auto-accept", isOn: $autoAcceptMemberContacts) + } + } header: { + Text("Contact requests from groups") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("This setting is for your current profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + } + Section { settingsRow("person", color: theme.colors.secondary) { Toggle("Contacts", isOn: $contactReceipts) @@ -219,6 +212,13 @@ struct PrivacySettings: View { setOrAskSendReceiptsGroups(groupReceipts) } } + .onChange(of: autoAcceptMemberContacts) { _ in + if autoAcceptMemberContactsReset { + autoAcceptMemberContactsReset = false + } else { + setAutoAcceptGrpDirectInvs(autoAcceptMemberContacts) + } + } .onAppear { if let u = m.currentUser { if contactReceipts != u.sendRcptsContacts { @@ -229,6 +229,10 @@ struct PrivacySettings: View { groupReceiptsReset = true groupReceipts = u.sendRcptsSmallGroups } + if autoAcceptMemberContacts != u.autoAcceptMemberContacts { + autoAcceptMemberContactsReset = true + autoAcceptMemberContacts = u.autoAcceptMemberContacts + } } } .alert(item: $alert) { alert in @@ -345,6 +349,23 @@ struct PrivacySettings: View { } } + private func setAutoAcceptGrpDirectInvs(_ enable: Bool) { + Task { + do { + if let currentUser = m.currentUser { + try await apiSetUserAutoAcceptMemberContacts(currentUser.userId, enable: enable) + await MainActor.run { + var updatedUser = currentUser + updatedUser.autoAcceptMemberContacts = enable + m.updateUser(updatedUser) + } + } + } catch let error { + alert = .error(title: "Error setting auto-accept", error: "Error: \(responseError(error))") + } + } + } + private func simplexLockRow(_ value: LocalizedStringKey) -> some View { HStack { Text("SimpleX Lock") @@ -457,7 +478,7 @@ struct SimplexLockView: View { Toggle("Allow sharing", isOn: $allowShareExtension) } } - + if performLA && laMode == .passcode { Section(header: Text("Self-destruct passcode").foregroundColor(theme.colors.secondary)) { Toggle(isOn: $selfDestruct) { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 138c3689f5..cb6fdf8597 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -29,7 +29,6 @@ let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers" let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents" let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES instead let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved to app group -let DEFAULT_PRIVACY_CHAT_LIST_OPEN_LINKS = "privacyChatListOpenLinks" let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode" let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" @@ -58,6 +57,7 @@ let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown" let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice" let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert" +let DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "showReportsInSupportChatAlert" let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion" let DEFAULT_ONBOARDING_STAGE = "onboardingStage" let DEFAULT_MIGRATION_TO_STAGE = "migrationToStage" @@ -116,6 +116,7 @@ let appDefaults: [String: Any] = [ DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true, DEFAULT_SHOW_MUTE_PROFILE_ALERT: true, + DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT: true, DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue, DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300, DEFAULT_SHOW_UNREAD_AND_FAVORITES: false, @@ -143,6 +144,7 @@ let hintDefaults = [ DEFAULT_LIVE_MESSAGE_ALERT_SHOWN, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE, DEFAULT_SHOW_MUTE_PROFILE_ALERT, + DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE, DEFAULT_SHOW_DELETE_CONTACT_NOTICE ] @@ -183,8 +185,6 @@ let connectViaLinkTabDefault = EnumDefault(defaults: UserDefa let privacySimplexLinkModeDefault = EnumDefault(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_SIMPLEX_LINK_MODE, withDefault: .description) -let privacyChatListOpenLinksDefault = EnumDefault(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_CHAT_LIST_OPEN_LINKS, withDefault: PrivacyChatListOpenLinksMode.ask) - let privacyLocalAuthModeDefault = EnumDefault(defaults: UserDefaults.standard, forKey: DEFAULT_LA_MODE, withDefault: .system) let privacyDeliveryReceiptsSet = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET) @@ -196,6 +196,8 @@ let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.sta let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE) let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE) +let showReportsInSupportChatAlertDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT) + /// 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) @@ -281,159 +283,159 @@ struct SettingsView: View { } } - @ViewBuilder func settingsView() -> some View { - let user = chatModel.currentUser - List { - 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) - - NavigationLink { - NetworkAndServers() - .navigationTitle("Network & servers") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - CallSettings() - .navigationTitle("Your calls") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - PrivacySettings() - .navigationTitle("Your privacy") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("lock", color: theme.colors.secondary) { Text("Privacy & security") } - } - .disabled(chatModel.chatRunning != true) - - if UIApplication.shared.supportsAlternateIcons { - NavigationLink { - AppearanceSettings() - .navigationTitle("Appearance") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("sun.max", color: theme.colors.secondary) { Text("Appearance") } - } - .disabled(chatModel.chatRunning != true) + 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(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") } - } + NavigationLink { + NetworkAndServers() + .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } } - - 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") } - } - } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + CallSettings() + .navigationTitle("Your calls") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + PrivacySettings() + .navigationTitle("Your privacy") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("lock", color: theme.colors.secondary) { Text("Privacy & security") } + } + .disabled(chatModel.chatRunning != true) + + if UIApplication.shared.supportsAlternateIcons { NavigationLink { - WhatsNewView(viaSettings: true, updatedConditions: false) - .modifier(ThemedBackground()) - .navigationBarTitleDisplayMode(.inline) + AppearanceSettings() + .navigationTitle("Appearance") + .modifier(ThemedBackground(grouped: true)) } label: { - settingsRow("plus", color: theme.colors.secondary) { Text("What's new") } + settingsRow("sun.max", color: theme.colors.secondary) { Text("Appearance") } } + .disabled(chatModel.chatRunning != true) + } + } + + 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 { - SimpleXInfo(onboarding: false) - .navigationBarTitle("", displayMode: .inline) + ChatHelp(dismissSettingsSheet: dismiss) + .navigationTitle("Welcome \(user.displayName)!") .modifier(ThemedBackground()) .frame(maxHeight: .infinity, alignment: .top) } label: { - settingsRow("info", color: theme.colors.secondary) { Text("About SimpleX Chat") } + settingsRow("questionmark", color: theme.colors.secondary) { Text("How to use it") } } - settingsRow("number", color: theme.colors.secondary) { - Button("Send questions and ideas") { - dismiss() - DispatchQueue.main.async { - UIApplication.shared.open(simplexTeamURL) - } + } + 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)") } } + .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) - } + 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) - } } + 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 ?? "?"))") - } + 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 = [] - } + } + .navigationTitle("Your settings") + .modifier(ThemedBackground(grouped: true)) + .onDisappear { + chatModel.showingTerminal = false + chatModel.terminalItems = [] + } } private func chatDatabaseRow() -> some View { @@ -477,7 +479,11 @@ struct SettingsView: View { case .registered: icon = "bolt.fill" color = theme.colors.secondary - case .invalid: + case .invalid: fallthrough + case .invalidBad: fallthrough + case .invalidTopic: fallthrough + case .invalidExpired: fallthrough + case .invalidUnregistered: icon = "bolt.slash" color = theme.colors.secondary case .confirmed: @@ -524,7 +530,7 @@ struct ProfilePreview: View { func profileName(_ profileOf: NamedChat) -> Text { var t = Text(profileOf.displayName).fontWeight(.semibold).font(.title2) if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { - t = t + Text(" (" + profileOf.fullName + ")") + t = t + Text(verbatim: " (" + profileOf.fullName + ")") // .font(.callout) } return t diff --git a/apps/ios/Shared/Views/UserSettings/StorageView.swift b/apps/ios/Shared/Views/UserSettings/StorageView.swift index 2cf63692a7..094c1cb3d6 100644 --- a/apps/ios/Shared/Views/UserSettings/StorageView.swift +++ b/apps/ios/Shared/Views/UserSettings/StorageView.swift @@ -33,7 +33,7 @@ struct StorageView: View { 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(" ") + Text("\(ByteCountFormatter.string(fromByteCount: value, countStyle: .binary))") + Text(key).bold() + Text(verbatim: " ") + Text((ByteCountFormatter.string(fromByteCount: value, countStyle: .binary))) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 7965215b49..d40ec116f4 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -8,7 +8,7 @@ import SwiftUI import MessageUI -import SimpleXChat +@preconcurrency import SimpleXChat struct UserAddressView: View { @Environment(\.dismiss) var dismiss: DismissAction @@ -16,8 +16,9 @@ struct UserAddressView: View { @EnvironmentObject var theme: AppTheme @State var shareViaProfile = false @State var autoCreate = false - @State private var aas = AutoAcceptState() - @State private var savedAAS = AutoAcceptState() + @State private var showShortLink = true + @State private var settings = AddressSettingsState() + @State private var savedSettings = AddressSettingsState() @State private var showMailView = false @State private var mailViewResult: Result? = nil @State private var alert: UserAddressAlert? @@ -65,8 +66,8 @@ struct UserAddressView: View { if let userAddress = chatModel.userAddress { existingAddressView(userAddress) .onAppear { - aas = AutoAcceptState(userAddress: userAddress) - savedAAS = aas + settings = AddressSettingsState(settings: userAddress.addressSettings) + savedSettings = AddressSettingsState(settings: userAddress.addressSettings) } } else { Section { @@ -135,28 +136,30 @@ struct UserAddressView: View { @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { Section { - SimpleXLinkQRCode(uri: userAddress.connReqContact) - .id("simplex-contact-address-qrcode-\(userAddress.connReqContact)") - shareQRCodeButton(userAddress) + SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: $showShortLink) + .id("simplex-contact-address-qrcode-\(userAddress.connLinkContact.simplexChatUri(short: showShortLink))") + if userAddress.shouldBeUpgraded { + upgradeAddressButton() + } + shareAddressButton(userAddress) // if MFMailComposeViewController.canSendMail() { // shareViaEmailButton(userAddress) // } settingsRow("briefcase", color: theme.colors.secondary) { - Toggle("Business address", isOn: $aas.business) - .onChange(of: aas.business) { ba in + Toggle("Business address", isOn: $settings.businessAddress) + .onChange(of: settings.businessAddress) { ba in if ba { - aas.enable = true - aas.incognito = false + settings.autoAccept = true + settings.autoAcceptIncognito = false } - saveAAS($aas, $savedAAS) + saveAddressSettings(settings, $savedSettings) } } addressSettingsButton(userAddress) } header: { - Text("For social media") - .foregroundColor(theme.colors.secondary) + ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink) } footer: { - if aas.business { + if settings.businessAddress { Text("Add your team members to the conversations.") .foregroundColor(theme.colors.secondary) } @@ -193,10 +196,12 @@ struct UserAddressView: View { progressIndicator = true Task { do { - let connReqContact = try await apiCreateUserAddress() + let connLinkContact = try await apiCreateUserAddress() DispatchQueue.main.async { - chatModel.userAddress = UserContactLink(connReqContact: connReqContact) - alert = .shareOnCreate + if let connLinkContact { + chatModel.userAddress = UserContactLink(connLinkContact) + alert = .shareOnCreate + } progressIndicator = false } } catch let error { @@ -208,6 +213,16 @@ struct UserAddressView: View { } } + private func upgradeAddressButton() -> some View { + Button { + upgradeAndShareAddressAlert(progressIndicator: $progressIndicator) + } label: { + settingsRow("arrow.up", color: theme.colors.primary) { + Text("Upgrade address") + } + } + } + private func createOneTimeLinkButton() -> some View { NavigationLink { NewChatView(selection: .invite) @@ -229,9 +244,13 @@ struct UserAddressView: View { } } - private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { - Button { - showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) + private func shareAddressButton(_ userAddress: UserContactLink) -> some View { + return Button { + if userAddress.shouldBeUpgraded { + upgradeAndShareAddressAlert(progressIndicator: $progressIndicator, shareAddress: { userAddress.shareAddress(short: showShortLink) }) + } else { + userAddress.shareAddress(short: showShortLink) + } } label: { settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Text("Share address") @@ -294,45 +313,90 @@ struct UserAddressView: View { } } -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 = "" +func upgradeAndShareAddressAlert(progressIndicator: Binding, shareAddress: (() -> Void)? = nil) { + showAlert( + NSLocalizedString("Upgrade address?", comment: "alert message"), + message: NSLocalizedString("The address will be short, and your profile will be shared via the address.", comment: "alert message"), + actions: { + var actions = [UIAlertAction(title: NSLocalizedString("Upgrade", comment: "alert button"), style: .default) { _ in + addShortLink(progressIndicator: progressIndicator, shareOnCompletion: shareAddress != nil) + }] + if let shareAddress { + actions.append(UIAlertAction(title: NSLocalizedString("Share old address", comment: "alert button"), style: .default) { _ in + shareAddress() + }) } - } else { - enable = false - incognito = false - business = false - welcomeText = "" + actions.append(cancelAlertAction) + return actions + } + ) +} + +private func addShortLink(progressIndicator: Binding, shareOnCompletion: Bool = false) { + progressIndicator.wrappedValue = true + Task { + do { + let userAddress = try await apiAddMyAddressShortLink() + await MainActor.run { + ChatModel.shared.userAddress = userAddress + progressIndicator.wrappedValue = false + if shareOnCompletion, let userAddress { + userAddress.shareAddress(short: true) + } + } + } catch let error { + logger.error("apiAddMyAddressShortLink: \(responseError(error))") + showAlert("Error adding short link", message: responseError(error)) + await MainActor.run { progressIndicator.wrappedValue = false } } } +} - 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) + +struct ToggleShortLinkHeader: View { + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + let text: Text + var link: CreatedConnLink + @Binding var short: Bool + + var body: some View { + if link.connShortLink == nil || !developerTools { + 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() } + } } - return nil + } +} + +struct AddressSettingsState: Equatable { + var businessAddress = false + var autoAccept = false + var autoAcceptIncognito = false + var autoReply = "" + + init() {} + + init(settings: AddressSettings) { + self.businessAddress = settings.businessAddress + self.autoAccept = settings.autoAccept != nil + self.autoAcceptIncognito = settings.autoAccept?.acceptIncognito == true + self.autoReply = settings.autoReply?.text ?? "" + } + + var addressSettings: AddressSettings { + AddressSettings( + businessAddress: self.businessAddress, + autoAccept: self.autoAccept ? AutoAccept(acceptIncognito: self.autoAcceptIncognito) : nil, + autoReply: self.autoReply.isEmpty ? nil : MsgContent.text(self.autoReply) + ) } } @@ -357,30 +421,32 @@ 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 settings = AddressSettingsState() + @State private var savedSettings = AddressSettingsState() @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 + settings = AddressSettingsState(settings: userAddress.addressSettings) + savedSettings = AddressSettingsState(settings: userAddress.addressSettings) } - .onChange(of: aas.enable) { aasEnabled in - if !aasEnabled { aas = AutoAcceptState() } + .onChange(of: settings.autoAccept) { autoAccept in + if !autoAccept { + settings.businessAddress = false + settings.autoReply = "" + } } .onDisappear { - if savedAAS != aas { + if savedSettings != settings { showAlert( - title: NSLocalizedString("Auto-accept settings", comment: "alert title"), + title: NSLocalizedString("SimpleX address settings", comment: "alert title"), message: NSLocalizedString("Settings were changed.", comment: "alert message"), buttonTitle: NSLocalizedString("Save", comment: "alert button"), - buttonAction: { saveAAS($aas, $savedAAS) }, + buttonAction: { saveAddressSettings(settings, $savedSettings) }, cancelButton: true ) } @@ -398,11 +464,22 @@ struct UserAddressSettingsView: View { List { Section { shareWithContactsButton() - autoAcceptToggle().disabled(aas.business) + autoAcceptToggle().disabled(settings.businessAddress) + if settings.autoAccept && !ChatModel.shared.addressShortLinkDataSet && !settings.businessAddress { + acceptIncognitoToggle() + } } - if aas.enable { - autoAcceptSection() + Section { + messageEditor(placeholder: NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"), text: $settings.autoReply) + } header: { + Text("Welcome message") + .foregroundColor(theme.colors.secondary) + } + + Section { + saveAddressSettingsButton() + .disabled(settings == savedSettings) } } } @@ -421,7 +498,7 @@ struct UserAddressSettingsView: View { actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default, + style: .cancel, handler: { _ in ignoreShareViaProfileChange = true shareViaProfile = !on @@ -443,7 +520,7 @@ struct UserAddressSettingsView: View { actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default, + style: .cancel, handler: { _ in ignoreShareViaProfileChange = true shareViaProfile = !on @@ -466,46 +543,31 @@ struct UserAddressSettingsView: View { 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) + Toggle("Auto-accept", isOn: $settings.autoAccept) + .onChange(of: settings.autoAccept) { _ in + saveAddressSettings(settings, $savedSettings) } } } - private func autoAcceptSection() -> some View { - Section { - 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 : theme.colors.secondary + settings.autoAcceptIncognito ? "theatermasks.fill" : "theatermasks", + color: settings.autoAcceptIncognito ? .indigo : theme.colors.secondary ) { - Toggle("Accept incognito", isOn: $aas.incognito) + Toggle("Accept incognito", isOn: $settings.autoAcceptIncognito) } } - private func welcomeMessageEditor() -> some View { + private func messageEditor(placeholder: String, text: Binding) -> some View { ZStack { Group { - if aas.welcomeText.isEmpty { - TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"))) + if text.wrappedValue.isEmpty { + TextEditor(text: Binding.constant(placeholder)) .foregroundColor(theme.colors.secondary) .disabled(true) } - TextEditor(text: $aas.welcomeText) - .focused($keyboardVisible) + TextEditor(text: text) } .padding(.horizontal, -5) .padding(.top, -8) @@ -514,27 +576,27 @@ struct UserAddressSettingsView: View { } } - private func saveAASButton() -> some View { + private func saveAddressSettingsButton() -> some View { Button { - keyboardVisible = false - saveAAS($aas, $savedAAS) + hideKeyboard() + saveAddressSettings(settings, $savedSettings) } label: { Text("Save") } } } -private func saveAAS(_ aas: Binding, _ savedAAS: Binding) { +private func saveAddressSettings(_ settings: AddressSettingsState, _ savedSettings: Binding) { Task { do { - if let address = try await userAddressAutoAccept(aas.wrappedValue.autoAccept) { + if let address = try await apiSetUserAddressSettings(settings.addressSettings) { await MainActor.run { ChatModel.shared.userAddress = address - savedAAS.wrappedValue = aas.wrappedValue + savedSettings.wrappedValue = settings } } } catch let error { - logger.error("userAddressAutoAccept error: \(responseError(error))") + logger.error("apiSetUserAddressSettings error: \(responseError(error))") } } } @@ -542,9 +604,8 @@ private func saveAAS(_ aas: Binding, _ savedAAS: Binding 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 bioFitsLimit() -> Bool { + chatJsonLength(shortDescr) <= MAX_BIO_LENGTH_BYTES + } + private var canSaveProfile: Bool { - currentProfileHash != profile.hashValue && + ( + currentProfileHash != profile.hashValue || + (chatModel.currentUser?.profile.shortDescr ?? "") != shortDescr.trimmingCharacters(in: .whitespaces) + ) && profile.displayName.trimmingCharacters(in: .whitespaces) != "" && - validDisplayName(profile.displayName) + validDisplayName(profile.displayName) && + bioFitsLimit() } private func saveProfile() { @@ -168,6 +150,7 @@ struct UserProfile: View { Task { do { profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) + profile.shortDescr = shortDescr.trimmingCharacters(in: .whitespaces) if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { await MainActor.run { chatModel.updateCurrentUser(newProfile) @@ -186,12 +169,59 @@ struct UserProfile: View { if let user = chatModel.currentUser { profile = fromLocalProfile(user.profile) currentProfileHash = profile.hashValue + shortDescr = profile.shortDescr ?? "" } } } -func profileImageView(_ imageStr: String?) -> some View { - ProfileImage(imageStr: imageStr, size: 192) +struct EditProfileImage: View { + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner + @Binding var profileImage: String? + @Binding var showChooseSource: Bool + + var body: some View { + Group { + if profileImage != nil { + ZStack(alignment: .bottomTrailing) { + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: profileImage, size: 160) + .onTapGesture { showChooseSource = true } + overlayButton("multiply", edge: .top) { profileImage = nil } + } + overlayButton("camera", edge: .bottom) { showChooseSource = true } + } + } else { + ZStack(alignment: .center) { + ProfileImage(imageStr: profileImage, size: 160) + editImageButton { showChooseSource = true } + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .contentShape(Rectangle()) + } + + 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) + } } func editImageButton(action: @escaping () -> Void) -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index 781ea4bc34..ddfe59e719 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -221,11 +221,11 @@ 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) @@ -350,7 +350,7 @@ struct UserProfilesView: View { Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground) } else { if userInfo.unreadCount > 0 { - UnreadBadge(userInfo: userInfo) + userUnreadBadge(userInfo, theme: theme) } if user.hidden { Image(systemName: "lock").foregroundColor(theme.colors.secondary) 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 ef91bb30fd..427430b833 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -49,7 +49,7 @@ %@ - %@ + %@ No comment provided by engineer. @@ -379,292 +379,356 @@ أضف إلى جهاز آخر 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 @@ -675,8 +739,9 @@ Connect via group link? No comment provided by engineer. - + Connect via link + تواصل عبر الرابط No comment provided by engineer. @@ -691,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. @@ -919,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. + الرسائل المباشرة بين الأعضاء ممنوعة. 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. + الرسائل المختفية ممنوعة. No comment provided by engineer. - + Disconnect + قطع الاتصال server test step @@ -1075,132 +1225,163 @@ 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. - + Error creating address + خطأ في إنشاء العنوان No comment provided by engineer. - + Error creating group + خطأ في إنشاء المجموعة No comment provided by engineer. @@ -1973,8 +2154,9 @@ We will be adding server redundancy to prevent lost messages. Open user profiles authentication reason - + Anybody can host servers. + يمكن لأي شخص استضافة الخوادم. No comment provided by engineer. @@ -2049,8 +2231,8 @@ We will be adding server redundancy to prevent lost messages. 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 + + Fingerprint in server address does not match certificate. server test error @@ -2377,96 +2559,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 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 @@ -2625,8 +2828,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. No comment provided by engineer. @@ -3417,7 +3620,7 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -3526,72 +3729,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 + + You are invited to group + أنت مدعو إلى المجموعة No comment provided by engineer. - + you changed address + غيّرتَ العنوان chat item text @@ -3606,16 +3824,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 +3877,7 @@ SimpleX servers cannot see your profile. # %@ - # %@ + # %@ copied message info title, # <title> @@ -3877,6 +4097,1764 @@ SimpleX servers cannot see your profile. 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 + اسأل + + + SimpleX address 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. + فشل إعادة التفاوض على التشفير. + + + Accept as member + اقبل كعضو + + + Accept as observer + اقبل كمراقب + + + Accept contact request + اقبل طلب الاتصال + + + Accept member + اقبل العضو + + + Add message + أضف رسالة + + + All servers + كل الخوادم + + + Bio + نبذة + + + Bio too large + النبذة كبيرة جدًا + + + Can't change profile + لا يمكن تغيير الحساب + + + Chat with admins + تحدث مع المدراء + + + Chat with member + تحدث مع العضو + + + Chat with members before they join. + تحدث مع الأعضاء قبل انضمامهم. + + + Chats with members + تحدث مع الأعضاء + + + Connect faster! 🚀 + اتصل بسرعة! 🚀 + + + Contact requests from groups + طلبات الاتصال من المجموعات + + + Create your address + أنشئ عنوانك + + + Delete chat with member? + حذف المحادثة مع العضو؟ + + + Description too large + الوصف كبير جدًا + + + Empty message! + رسالة فارغة! + + + Enable disappearing messages by default. + فعّل حذف الرسائل تلقائيا. + + + Error accepting member + خطأ في قبول العضو + + + Error adding short link + خطأ في إضافة الرابط القصير + + + Error changing chat profile + خطأ في تغيير حساب المحادثات + + + Error connecting to forwarding server %@. Please try later. + خطأ في الإتصال بخادم التحويل @%. حاول مجددا بعد حين. + @@ -3925,4 +5903,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 4aa1f2213f..a59179ddfa 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (може да се копира) @@ -114,10 +102,12 @@ %@ server + %@ сървър No comment provided by engineer. %@ servers + %@ сървъри No comment provided by engineer. @@ -132,6 +122,7 @@ %1$@, %2$@ + %1$@, %2$@ format for date separator in chat @@ -156,18 +147,22 @@ %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 @@ -177,6 +172,7 @@ %d messages not forwarded + %d непрепратени съобщения alert title @@ -194,6 +190,11 @@ %d сек. time interval + + %d seconds(s) + %d секунди + delete after time + %d skipped message(s) %d пропуснато(и) съобщение(я) @@ -264,11 +265,6 @@ %lld нови езици на интерфейса No comment provided by engineer. - - %lld second(s) - %lld секунда(и) - No comment provided by engineer. - %lld seconds %lld секунди @@ -319,11 +315,6 @@ %u пропуснати съобщения. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (ново) @@ -334,11 +325,6 @@ (това устройство v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Добави контакт**: за създаване на нов линк. @@ -376,6 +362,7 @@ **Scan / Paste link**: to connect via a link you received. + **Сканирай / Постави линк**: за свързване чрез получения линк. No comment provided by engineer. @@ -403,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). @@ -444,11 +426,6 @@ - история на редактиране. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -462,7 +439,8 @@ 1 day 1 ден - time interval + delete after time +time interval 1 hour @@ -477,19 +455,28 @@ 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. @@ -507,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> @@ -568,21 +550,35 @@ About operators + За операторите No comment provided by engineer. Accent + Акцент No comment provided by engineer. Accept Приеми accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Приеми като член + alert action + + + Accept as observer + Приеми като наблюдател + alert action Accept conditions + Приеми условията No comment provided by engineer. @@ -590,6 +586,11 @@ Приемане на заявка за връзка? No comment provided by engineer. + + Accept contact request + Приеми заявка за контакт + alert title + Accept contact request from %@? Приемане на заявка за контакт от %@? @@ -598,23 +599,37 @@ Accept incognito Приеми инкогнито - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Приеми член + alert title 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. @@ -624,8 +639,19 @@ Add friends + Добави приятели No comment provided by engineer. + + Add list + Добави списък + No comment provided by engineer. + + + Add message + Добави съобщение + placeholder for sending contact request + Add profile Добави профил @@ -643,6 +669,7 @@ Add team members + Добави членове на екипа No comment provided by engineer. @@ -650,6 +677,11 @@ Добави към друго устройство No comment provided by engineer. + + Add to list + Добави към списъка + No comment provided by engineer. + Add welcome message Добави съобщение при посрещане @@ -657,26 +689,32 @@ 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. @@ -691,10 +729,12 @@ Address or 1-time link? + Адрес или еднократен линк? No comment provided by engineer. Address settings + Настройки на адреса No comment provided by engineer. @@ -714,6 +754,12 @@ Advanced settings + Разширени настройки + No comment provided by engineer. + + + All + Всички No comment provided by engineer. @@ -726,6 +772,11 @@ Всички чатове и съобщения ще бъдат изтрити - това не може да бъде отменено! 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. Всички данни се изтриват при въвеждане. @@ -733,6 +784,7 @@ All data is kept private on your device. + Всички данни се съхраняват поверително на вашето устройство. No comment provided by engineer. @@ -742,6 +794,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Всички съобщения и файлове се изпращат с **криптиране от край до край**, с постквантова сигурност в директните съобщения. No comment provided by engineer. @@ -761,8 +814,19 @@ 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. Всички ваши контакти ще останат свързани. @@ -790,6 +854,7 @@ Allow calls? + Позволи обаждания? No comment provided by engineer. @@ -799,6 +864,12 @@ Allow downgrade + Позволи понижаване + No comment provided by engineer. + + + Allow files and media only if your contact allows them. + Разреши файлове и медия само ако вашият контакт ги разрешава. No comment provided by engineer. @@ -828,6 +899,7 @@ Allow sharing + Позволи споделяне No comment provided by engineer. @@ -835,6 +907,11 @@ Позволи необратимо изтриване на изпратените съобщения. (24 часа) No comment provided by engineer. + + Allow to report messsages to moderators. + Позволи докладването на съобщения на модераторите. + No comment provided by engineer. + Allow to send SimpleX links. Разрешаване на изпращане на SimpleX линкове. @@ -880,6 +957,11 @@ Позволи на вашите контакти да изпращат изчезващи съобщения. No comment provided by engineer. + + Allow your contacts to send files and media. + Позволи на вашите контактите да изпращат файлове и медия. + No comment provided by engineer. + Allow your contacts to send voice messages. Позволи на вашите контакти да изпращат гласови съобщения. @@ -893,15 +975,16 @@ Already connecting! В процес на свързване! - No comment provided by engineer. + new chat sheet title Already joining the group! Вече се присъединихте към групата! - No comment provided by engineer. + new chat sheet title Always use private routing. + Винаги използвай поверително рутиране. No comment provided by engineer. @@ -914,6 +997,11 @@ Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено. No comment provided by engineer. + + Another reason + Друга причина + report reason + Answer call Отговор на повикване @@ -939,6 +1027,11 @@ Приложението криптира нови локални файлове (с изключение на видеоклипове). No comment provided by engineer. + + App group: + Група приложения: + No comment provided by engineer. + App icon Икона на приложението @@ -956,6 +1049,7 @@ App session + Сесия на приложението No comment provided by engineer. @@ -980,6 +1074,22 @@ 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. @@ -989,10 +1099,27 @@ 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. @@ -1060,10 +1187,6 @@ Автоматично приемане на изображения No comment provided by engineer. - - Auto-accept settings - alert title - Back Назад @@ -1071,6 +1194,7 @@ Background + Фон No comment provided by engineer. @@ -1090,6 +1214,7 @@ Better calls + По-добри обаждания No comment provided by engineer. @@ -1097,8 +1222,14 @@ По-добри групи No comment provided by engineer. + + Better groups performance + По-добра производителност на групите + No comment provided by engineer. + Better message dates. + По-добри дати на съобщението. No comment provided by engineer. @@ -1108,22 +1239,42 @@ 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. + + Bio + Био + No comment provided by engineer. + + + Bio too large + Биографията е твърде дълга + alert title + Black + Черна No comment provided by engineer. @@ -1163,10 +1314,17 @@ Blur for better privacy. + Размазване за по-добра поверителност. No comment provided by engineer. Blur media + Размазване на медия + No comment provided by engineer. + + + Bot + Бот No comment provided by engineer. @@ -1189,6 +1347,11 @@ И вие, и вашият контакт можете да изпращате изчезващи съобщения. No comment provided by engineer. + + Both you and your contact can send files and media. + И вие, и вашият контакт можете да изпращате файлове и медия. + No comment provided by engineer. + Both you and your contact can send voice messages. И вие, и вашият контакт можете да изпращате гласови съобщения. @@ -1201,10 +1364,22 @@ Business address + Бизнес адрес No comment provided by engineer. Business chats + Бизнес чатове + No comment provided by engineer. + + + Business connection + Бизнес връзка + No comment provided by engineer. + + + Businesses + Бизнеси No comment provided by engineer. @@ -1212,6 +1387,15 @@ Чрез чат профил (по подразбиране) или [чрез връзка](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! Разговорът вече приключи! @@ -1224,6 +1408,7 @@ Calls prohibited! + Обажданията са забранени! No comment provided by engineer. @@ -1233,12 +1418,19 @@ Can't call contact + Обаждането на контакта не е позволено No comment provided by engineer. Can't call member + Обаждането на члена не е позволено No comment provided by engineer. + + Can't change profile + Промяната на профила е невъзможна + alert title + Can't invite contact! Не може да покани контакта! @@ -1251,13 +1443,15 @@ Can't message member + Изпращането на съобщения на груповия член не е налично No comment provided by engineer. Cancel Отказ alert action - alert button +alert button +new chat action Cancel migration @@ -1271,6 +1465,7 @@ Cannot forward message + Съобщение не може да бъде препратено No comment provided by engineer. @@ -1280,6 +1475,7 @@ Capacity exceeded - recipient did not receive previously sent messages. + Капацитетът е надвишен - получателят не е получил предишно изпратените съобщения. snd error text @@ -1292,8 +1488,14 @@ Промени No comment provided by engineer. + + Change automatic message deletion? + Промяна на автоматичното изтриване на съобщения? + alert title + Change chat profiles + Промени чат профилите authentication reason @@ -1340,22 +1542,26 @@ Change self-destruct passcode Промени кода за достъп за самоунищожение authentication reason - set passcode view +set passcode view Chat + Чат No comment provided by engineer. Chat already exists + Чатът вече съществува No comment provided by engineer. Chat already exists! - No comment provided by engineer. + Чатът вече съществува! + new chat sheet title Chat colors + Цветове на чата No comment provided by engineer. @@ -1365,7 +1571,7 @@ Chat database - База данни за чата + База данни No comment provided by engineer. @@ -1375,11 +1581,12 @@ Chat database exported + Базата данни е експортирана No comment provided by engineer. Chat database imported - Базата данни на чат е импортирана + Базата данни на е импортирана No comment provided by engineer. @@ -1399,6 +1606,7 @@ Chat list + Списък с чатове No comment provided by engineer. @@ -1413,6 +1621,7 @@ Chat preferences were changed. + Настройките на чата бяха променени. alert message @@ -1422,14 +1631,32 @@ 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. + + + Chat with admins + Чат с администраторите + chat toolbar + + + Chat with member + Чат с член + No comment provided by engineer. + + + Chat with members before they join. + Разговаряйте с членовете, преди да се присъединят. No comment provided by engineer. @@ -1437,12 +1664,19 @@ Чатове No comment provided by engineer. + + Chats with members + Чатове с членовете + 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. @@ -1472,14 +1706,17 @@ Chunks deleted + Изтрити парчета No comment provided by engineer. Chunks downloaded + Изтеглени парчета No comment provided by engineer. Chunks uploaded + Качени парчета No comment provided by engineer. @@ -1497,6 +1734,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? Изчистване на лични бележки? @@ -1509,12 +1756,19 @@ 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 Сравни файл @@ -1527,42 +1781,42 @@ 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 enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + Условия за ползване + alert button 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. @@ -1570,6 +1824,11 @@ Конфигурирай ICE сървъри No comment provided by engineer. + + Configure server operators + Конфигуриране на сървърни оператори + No comment provided by engineer. + Confirm Потвърди @@ -1582,6 +1841,7 @@ Confirm contact deletion? + Потвърди изтриването на контакта? No comment provided by engineer. @@ -1591,6 +1851,7 @@ Confirm files from unknown servers. + Потвърди файлове от неизвестни сървъри. No comment provided by engineer. @@ -1618,6 +1879,11 @@ Потвърди качването No comment provided by engineer. + + Confirmed + Потвърдено + token status text + Connect Свързване @@ -1628,9 +1894,9 @@ Автоматично свъзрване No comment provided by engineer. - - Connect incognito - Свързване инкогнито + + Connect faster! 🚀 + Свържете се по-бързо! 🚀 No comment provided by engineer. @@ -1640,11 +1906,7 @@ Connect to your friends faster. - No comment provided by engineer. - - - Connect to yourself? - Свърване със себе си? + Свържете се с приятелите си по-бързо. No comment provided by engineer. @@ -1652,37 +1914,38 @@ This is your own SimpleX address! Свърване със себе си? Това е вашият личен SimpleX адрес! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Свърване със себе си? Това е вашят еднократен линк за връзка! - No comment provided by engineer. + new chat sheet title Connect via contact address Свързване чрез адрес за контакт - No comment provided by engineer. + new chat sheet title Connect via link Свърване чрез линк - No comment provided by engineer. + new chat sheet title Connect via one-time link Свързване чрез еднократен линк за връзка - No comment provided by engineer. + new chat sheet title Connect with %@ Свързване с %@ - No comment provided by engineer. + new chat action Connected + Свързан No comment provided by engineer. @@ -1692,6 +1955,7 @@ This is your own one-time link! Connected servers + Свързани сървъри No comment provided by engineer. @@ -1701,6 +1965,7 @@ This is your own one-time link! Connecting + Свързване No comment provided by engineer. @@ -1715,6 +1980,7 @@ This is your own one-time link! Connecting to contact, please wait or check later! + Тече свързване с контакт, моля изчакайте или проверете по-късно! No comment provided by engineer. @@ -1729,20 +1995,39 @@ This is your own one-time link! Connection and servers status. + Състояние на връзката и сървърите. + No comment provided by engineer. + + + Connection blocked + Връзката е блокирана No comment provided by engineer. Connection error Грешка при свързване - No comment provided by engineer. + alert title Connection error (AUTH) Грешка при свързване (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. @@ -1750,6 +2035,11 @@ This is your own one-time link! Заявката за връзка е изпратена! No comment provided by engineer. + + Connection requires encryption renegotiation. + Връзката изисква предоговаряне на криптирането. + No comment provided by engineer. + Connection security No comment provided by engineer. @@ -1762,7 +2052,7 @@ This is your own one-time link! Connection timeout Времето на изчакване за установяване на връзката изтече - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1810,6 +2100,10 @@ This is your own one-time link! Настройки за контакт No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -1824,6 +2118,10 @@ This is your own one-time link! Контактите могат да маркират съобщения за изтриване; ще можете да ги разглеждате. No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue Продължи @@ -1858,31 +2156,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 file - Създай файл + Създаване на файл server test step Create group - Създай група + Създаване на група No comment provided by engineer. @@ -1895,6 +2194,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/). 💻 @@ -1910,9 +2213,8 @@ This is your own one-time link! Създай опашка server test step - - Create secret group - Създай тайна група + + Create your address No comment provided by engineer. @@ -2105,8 +2407,7 @@ This is your own one-time link! Delete Изтрий alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2146,6 +2447,10 @@ This is your own one-time link! Delete chat No comment provided by engineer. + + Delete chat messages from your device. + No comment provided by engineer. + Delete chat profile Изтрий чат профила @@ -2156,6 +2461,10 @@ This is your own one-time link! Изтриване на чат профила? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2234,6 +2543,10 @@ This is your own one-time link! Изтрий линк? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Изтрий съобщението на члена? @@ -2247,7 +2560,7 @@ This is your own one-time link! Delete messages Изтрий съобщенията - No comment provided by engineer. + alert button Delete messages after @@ -2283,6 +2596,10 @@ This is your own one-time link! Изтрий опашка server test step + + Delete report + No comment provided by engineer. + Delete up to 20 messages at once. No comment provided by engineer. @@ -2333,11 +2650,19 @@ This is your own one-time link! Потвърждениe за доставка! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Описание No comment provided by engineer. + + Description too large + alert title + Desktop address Адрес на настолно устройство @@ -2431,6 +2756,14 @@ This is your own one-time link! Деактивирай SimpleX заключване authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Деактивиране за всички @@ -2517,6 +2850,10 @@ This is your own one-time link! Do not use credentials with proxy. No comment provided by engineer. + + Documents: + No comment provided by engineer. + Don't create address Не създавай адрес @@ -2527,9 +2864,17 @@ This is your own one-time link! Не активирай No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Не показвай отново + alert action + + + Done No comment provided by engineer. @@ -2541,7 +2886,7 @@ This is your own one-time link! Download Изтегли alert button - chat item action +chat item action Download errors @@ -2603,6 +2948,10 @@ This is your own one-time link! Редактирай групов профил No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable Активирай @@ -2613,8 +2962,8 @@ This is your own one-time link! Активиране (запазване на промените) No comment provided by engineer. - - Enable Flux + + Enable Flux in Network & servers settings for better metadata privacy. No comment provided by engineer. @@ -2630,13 +2979,17 @@ This is your own one-time link! Enable automatic message deletion? Активиране на автоматично изтриване на съобщения? - No comment provided by engineer. + alert title Enable camera access Разреши достъпа до камерата No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all Активиране за всички @@ -2756,6 +3109,10 @@ This is your own one-time link! Неуспешно повторно договаряне на криптирането. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Въведете kодa за достъп @@ -2830,6 +3187,10 @@ This is your own one-time link! Грешка при приемане на заявка за контакт No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Грешка при добавяне на член(ове) @@ -2839,11 +3200,19 @@ This is your own one-time link! Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Грешка при промяна на адреса No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile No comment provided by engineer. @@ -2856,15 +3225,23 @@ This is your own one-time link! Error changing setting Грешка при промяна на настройката - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2881,6 +3258,10 @@ This is your own one-time link! Грешка при създаване на групов линк No comment provided by engineer. + + Error creating list + alert title + Error creating member contact Грешка при създаване на контакт с член @@ -2896,20 +3277,28 @@ This is your own one-time link! Грешка при създаване на профил! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Грешка при декриптирането на файла No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database - Грешка при изтриване на чат базата данни - No comment provided by engineer. + Грешка при изтриване на базата данни + alert title Error deleting chat! Грешка при изтриването на чата! - No comment provided by engineer. + alert title Error deleting connection @@ -2919,12 +3308,12 @@ This is your own one-time link! Error deleting database Грешка при изтриване на базата данни - No comment provided by engineer. + alert title Error deleting old database Грешка при изтриване на старата база данни - No comment provided by engineer. + alert title Error deleting token @@ -2958,8 +3347,8 @@ This is your own one-time link! Error exporting chat database - Грешка при експортиране на чат базата данни - No comment provided by engineer. + Грешка при експортиране на базата данни + alert title Error exporting theme: %@ @@ -2967,8 +3356,8 @@ This is your own one-time link! Error importing chat database - Грешка при импортиране на чат базата данни - No comment provided by engineer. + Грешка при импортиране на базата данни + alert title Error joining group @@ -2988,6 +3377,10 @@ This is your own one-time link! Грешка при отваряне на чата No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file Грешка при получаване на файл @@ -3001,10 +3394,22 @@ This is your own one-time link! Error reconnecting servers No comment provided by engineer. + + Error registering for notifications + alert title + + + Error rejecting contact request + alert title + Error removing member Грешка при отстраняване на член - No comment provided by engineer. + alert title + + + Error reordering lists + alert title Error resetting statistics @@ -3015,6 +3420,10 @@ This is your own one-time link! Грешка при запазване на ICE сървърите No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile Грешка при запазване на профила на групата @@ -3064,6 +3473,10 @@ This is your own one-time link! Грешка при изпращане на съобщение No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Грешка при настройването на потвърждениeто за доставка!! @@ -3081,7 +3494,7 @@ This is your own one-time link! Error switching profile - No comment provided by engineer. + alert title Error switching profile! @@ -3093,6 +3506,10 @@ This is your own one-time link! Грешка при синхронизиране на връзката No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link Грешка при актуализиране на груповия линк @@ -3135,7 +3552,13 @@ This is your own one-time link! Error: %@ Грешка: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3170,6 +3593,10 @@ This is your own one-time link! Разшири chat item action + + Expired + token status text + Export database Експортирай база данни @@ -3209,25 +3636,42 @@ This is your own one-time link! Бързо и без чакане, докато подателят е онлайн! 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 - No comment provided by engineer. + 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 @@ -3278,6 +3722,10 @@ This is your own one-time link! Файлове и медия chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Файловете и медията са забранени в тази група. @@ -3318,6 +3766,23 @@ This is your own one-time link! Намирайте чатове по-бързо No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Поправи @@ -3348,6 +3813,10 @@ This is your own one-time link! Поправката не се поддържа от члена на групата No comment provided by engineer. + + For all moderators + No comment provided by engineer. + For chat profile %@: servers error @@ -3361,6 +3830,10 @@ This is your own one-time link! 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. @@ -3410,8 +3883,8 @@ This is your own one-time link! No comment provided by engineer. - Forwarding server %@ failed to connect to destination server %@. Please try later. - No comment provided by engineer. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3471,6 +3944,10 @@ Error: %2$@ GIF файлове и стикери No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3492,7 +3969,7 @@ Error: %2$@ Group already exists! Групата вече съществува! - No comment provided by engineer. + new chat sheet title Group display name @@ -3559,6 +4036,10 @@ Error: %2$@ Груповият профил се съхранява на устройствата на членовете, а не на сървърите. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Съобщение при посрещане в групата @@ -3574,11 +4055,19 @@ Error: %2$@ Групата ще бъде изтрита за вас - това не може да бъде отменено! 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 Скрит @@ -3637,6 +4126,10 @@ Error: %2$@ How it helps privacy No comment provided by engineer. + + How it works + alert button + How to Информация @@ -3718,7 +4211,7 @@ Error: %2$@ Import chat database? - Импортиране на чат база данни? + Импортиране на база данни? No comment provided by engineer. @@ -3775,6 +4268,14 @@ More improvements are coming soon! Звуци по време на разговор No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Инкогнито @@ -3866,6 +4367,26 @@ More improvements are coming soon! 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 код @@ -3884,7 +4405,7 @@ More improvements are coming soon! Invalid link Невалиден линк - No comment provided by engineer. + alert title Invalid migration confirmation @@ -3995,37 +4516,32 @@ More improvements are coming soon! Присъединяване swipe action + + Join as %@ + присъединяване като %@ + No comment provided by engineer. + Join group Влез в групата - No comment provided by engineer. + new chat sheet title Join group conversations Присъединяване към групи No comment provided by engineer. - - Join group? - Влез в групата? - No comment provided by engineer. - Join incognito Влез инкогнито No comment provided by engineer. - - Join with current profile - Присъединяване с текущия профил - No comment provided by engineer. - Join your group? This is your link for group %@! Влез в твоята група? Това е вашят линк за група %@! - No comment provided by engineer. + new chat action Joining group @@ -4051,6 +4567,10 @@ This is your link for group %@! Запази неизползваната покана за връзка? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections Запазете връзките си @@ -4104,6 +4624,10 @@ This is your link for group %@! Напусни групата? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat Нека да поговорим в SimpleX Chat @@ -4134,6 +4658,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! Съобщение на живо! @@ -4144,6 +4680,10 @@ This is your link for group %@! Съобщения на живо No comment provided by engineer. + + Loading profile… + in progress text + Local name Локално име @@ -4217,10 +4757,26 @@ This is your link for group %@! Член No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + No comment provided by engineer. + Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + Member reports + chat feature + Member role will be changed to "%@". All chat members will be notified. No comment provided by engineer. @@ -4244,6 +4800,10 @@ This is your link for group %@! Членът ще бъде премахнат от групата - това не може да бъде отменено! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Членовете на групата могат да добавят реакции към съобщенията. @@ -4254,6 +4814,10 @@ This is your link for group %@! Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) No comment provided by engineer. + + Members can report messsages to moderators. + No comment provided by engineer. + Members can send SimpleX links. Членовете на групата могат да изпращат SimpleX линкове. @@ -4279,6 +4843,10 @@ This is your link for group %@! Членовете на групата могат да изпращат гласови съобщения. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4306,6 +4874,10 @@ This is your link for group %@! Message forwarded item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. item status description @@ -4374,11 +4946,19 @@ This is your link for group %@! Съобщения и файлове No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! Съобщенията от %@ ще бъдат показани! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + Messages received No comment provided by engineer. @@ -4476,6 +5056,10 @@ This is your link for group %@! Модерирано в: %@ copied message info + + More + swipe action + More improvements are coming soon! Очаквайте скоро още подобрения! @@ -4503,7 +5087,11 @@ This is your link for group %@! Mute Без звук - swipe action + notification label action + + + Mute all + notification label action Muted when inactive! @@ -4550,7 +5138,11 @@ This is your link for group %@! Network status Състояние на мрежата - No comment provided by engineer. + alert title + + + New + token status text New Passcode @@ -4598,6 +5190,10 @@ This is your link for group %@! New events notification + + New group role: Moderator + No comment provided by engineer. + New in %@ Ново в %@ @@ -4612,6 +5208,10 @@ This is your link for group %@! Нова членска роля No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Ново съобщение @@ -4636,6 +5236,22 @@ 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 chats with members + No comment provided by engineer. + No contacts selected Няма избрани контакти @@ -4683,6 +5299,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -4705,6 +5325,10 @@ This is your link for group %@! Няма разрешение за запис на гласово съобщение No comment provided by engineer. + + No private routing session + alert title + No push server Локално @@ -4731,6 +5355,14 @@ This is your link for group %@! No servers to send files. servers error + + No token! + alert title + + + No unread chats + No comment provided by engineer. + No user identifiers. Първата платформа без никакви потребителски идентификатори – поверителна по дизайн. @@ -4741,6 +5373,10 @@ This is your link for group %@! Несъвместим! No comment provided by engineer. + + Notes + No comment provided by engineer. + Nothing selected No comment provided by engineer. @@ -4759,10 +5395,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. @@ -4785,7 +5429,9 @@ This is your link for group %@! Ok Ок - alert button + alert action +alert button +new chat action Old database @@ -4844,6 +5490,14 @@ Requires compatible VPN. Само собствениците на групата могат да активират гласови съобщения. 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. Само вие можете да добавяте реакции на съобщенията. @@ -4864,6 +5518,10 @@ Requires compatible VPN. Само вие можете да изпращате изчезващи съобщения. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Само вие можете да изпращате гласови съобщения. @@ -4889,6 +5547,10 @@ Requires compatible VPN. Само вашият контакт може да изпраща изчезващи съобщения. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Само вашият контакт може да изпраща гласови съобщения. @@ -4897,7 +5559,7 @@ Requires compatible VPN. Open Отвори - No comment provided by engineer. + alert action Open Settings @@ -4911,27 +5573,63 @@ Requires compatible VPN. Open chat Отвори чат - No comment provided by engineer. + new chat action Open chat console Отвори конзолата authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group Отвори група - No comment provided by engineer. + new chat action + + + Open link? + alert title Open migration to another device Отвори миграцията към друго устройство authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… Приложението се отваря… @@ -4973,6 +5671,10 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Други @@ -5027,11 +5729,6 @@ Requires compatible VPN. Парола за показване No comment provided by engineer. - - Past member %@ - Бивш член %@ - past/unknown group member - Paste desktop address Постави адрес на настолно устройство @@ -5097,7 +5794,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. Моля, проверете мрежовата си връзка с %@ и опитайте отново. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5156,6 +5853,22 @@ Error: %@ Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите. No comment provided by engineer. + + Please try to disable and re-enable notfications. + token info + + + Please wait for group moderators to review your request to join the group. + snd group event chat item + + + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + token info + Polish interface Полски интерфейс @@ -5165,11 +5878,6 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен - server test error - Preserve the last message draft, with attachments. Запазете последната чернова на съобщението с прикачени файлове. @@ -5202,16 +5910,28 @@ Error: %@ 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. @@ -5231,7 +5951,11 @@ Error: %@ Private routing error - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5282,6 +6006,10 @@ Error: %@ Забрани реакциите на съобщенията. No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. Забранете изпращането на SimpleX линкове. @@ -5326,6 +6054,10 @@ Enable in *Network & servers* settings. Защитете чат профилите с парола! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout Време за изчакване на протокола @@ -5426,11 +6158,6 @@ Enable in *Network & servers* settings. Получено в: %@ copied message info - - Received file event - Събитие за получен файл - notification - Received message Получено съобщение @@ -5523,11 +6250,24 @@ Enable in *Network & servers* settings. Намалена консумация на батерията 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5537,7 +6277,11 @@ Enable in *Network & servers* settings. Reject contact request Отхвърли заявката за контакт - No comment provided by engineer. + alert title + + + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5562,6 +6306,10 @@ Enable in *Network & servers* settings. Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Острани член @@ -5577,6 +6325,10 @@ Enable in *Network & servers* settings. Премахване на паролата от keychain? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate Предоговоряне @@ -5592,11 +6344,6 @@ Enable in *Network & servers* settings. Предоговори криптирането? No comment provided by engineer. - - Repeat connection request? - Изпрати отново заявката за свързване? - No comment provided by engineer. - Repeat download Повтори изтеглянето @@ -5607,11 +6354,6 @@ Enable in *Network & servers* settings. Повтори импортирането No comment provided by engineer. - - Repeat join request? - Изпрати отново заявката за присъединяване? - No comment provided by engineer. - Repeat upload Повтори качването @@ -5622,6 +6364,50 @@ Enable in *Network & servers* settings. Отговори 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 sent to moderators + alert title + + + 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 Задължително @@ -5695,7 +6481,7 @@ Enable in *Network & servers* settings. Retry Опитай отново - No comment provided by engineer. + alert action Reveal @@ -5706,10 +6492,18 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later + + Review group members No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Отзови @@ -5756,13 +6550,21 @@ Enable in *Network & servers* settings. Save Запази alert button - chat item action +chat item action Save (and notify contacts) Запази (и уведоми контактите) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Запази и уведоми контакта @@ -5787,6 +6589,14 @@ Enable in *Network & servers* settings. Запази профила на групата No comment provided by engineer. + + Save group profile? + alert title + + + Save list + No comment provided by engineer. + Save passphrase and open chat Запази паролата и отвори чата @@ -5968,6 +6778,10 @@ Enable in *Network & servers* settings. Изпратете съобщение на живо - то ще се актуализира за получателя(ите), докато го пишете No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to Изпращайте потвърждениe за доставка на @@ -6014,6 +6828,10 @@ Enable in *Network & servers* settings. Изпращай известия No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Изпращайте въпроси и идеи @@ -6024,6 +6842,14 @@ Enable in *Network & servers* settings. Изпращане на потвърждениe за доставка No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. Изпрати от галерия или персонализирани клавиатури. @@ -6034,6 +6860,10 @@ Enable in *Network & servers* settings. Изпращане до последните 100 съобщения на нови членове. No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. Подателят отмени прехвърлянето на файла. @@ -6098,11 +6928,6 @@ Enable in *Network & servers* settings. Sent directly No comment provided by engineer. - - Sent file event - Събитие за изпратен файл - notification - Sent message Изпратено съобщение @@ -6161,13 +6986,13 @@ Enable in *Network & servers* settings. Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Сървърът изисква оторизация за създаване на опашки, проверете паролата server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Сървърът изисква оторизация за качване, проверете паролата server test error @@ -6211,6 +7036,10 @@ Enable in *Network & servers* settings. Задай 1 ден No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… Задай име на контакт… @@ -6230,6 +7059,14 @@ Enable in *Network & servers* settings. Задайте го вместо системната идентификация. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Задай kод за достъп @@ -6245,6 +7082,10 @@ Enable in *Network & servers* settings. Задай парола за експортиране No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! Задай съобщението, показано на новите членове! @@ -6273,7 +7114,7 @@ Enable in *Network & servers* settings. Share Сподели alert action - chat item action +chat item action Share 1-time link @@ -6311,6 +7152,14 @@ Enable in *Network & servers* settings. Сподели линк No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile No comment provided by engineer. @@ -6329,6 +7178,22 @@ Enable in *Network & servers* settings. Сподели с контактите No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + No comment provided by engineer. + Show QR code Покажи QR код @@ -6422,6 +7287,15 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX address settings + Автоматично приемане на настройки + alert title + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX адрес за контакт @@ -6461,6 +7335,10 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Опростен режим инкогнито @@ -6516,6 +7394,11 @@ Enable in *Network & servers* settings. Някой notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. Квадрат, кръг или нещо между тях. @@ -6599,6 +7482,10 @@ Enable in *Network & servers* settings. Спиране на чата No comment provided by engineer. + + Storage + No comment provided by engineer. + Strong blur media @@ -6647,11 +7534,19 @@ Enable in *Network & servers* settings. TCP connection No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -6676,10 +7571,26 @@ Enable in *Network & servers* settings. Направи снимка No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join group + No comment provided by engineer. + Tap button Докосни бутона @@ -6717,13 +7628,17 @@ Enable in *Network & servers* settings. Temporary file error - No comment provided by engineer. + file error alert title Test failed at step %@. Тестът е неуспешен на стъпка %@. server test failure + + Test notifications + No comment provided by engineer. + Test server Тествай сървър @@ -6761,6 +7676,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + The app can notify you when you receive messages or contact requests - please open settings to enable. Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате. @@ -6818,6 +7737,10 @@ It can happen because of some bug or when the connection is compromised.Хешът на предишното съобщение е различен. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. Съобщението ще бъде изтрито за всички членове. @@ -6841,19 +7764,10 @@ 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. - Профилът се споделя само с вашите контакти. - No comment provided by engineer. - The same conditions will apply to operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! No comment provided by engineer. @@ -6866,7 +7780,7 @@ It can happen because of some bug or when the connection is compromised. The sender will NOT be notified Подателят НЯМА да бъде уведомен - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -6914,6 +7828,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. Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени. @@ -6949,25 +7867,31 @@ It can happen because of some bug or when the connection is compromised.Тази група вече не съществува. No comment provided by engineer. - - This is your own SimpleX address! - Това е вашият личен SimpleX адрес! - No comment provided by engineer. - - - This is your own one-time 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. 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title No comment provided by engineer. @@ -7043,11 +7967,19 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. За поддръжка на незабавни push известия, базата данни за чат трябва да бъде мигрирана. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. @@ -7066,6 +7998,10 @@ You will be prompted to complete authentication before this feature is enabled.< Избор на инкогнито при свързване. No comment provided by engineer. + + Token status: %@. + token status + Toolbar opacity No comment provided by engineer. @@ -7083,15 +8019,9 @@ You will be prompted to complete authentication before this feature is enabled.< 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. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7226,13 +8156,17 @@ To connect, please ask your contact to create another connection link and check Unmute Уведомявай - swipe action + 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 съобщения. @@ -7257,16 +8191,44 @@ To connect, please ask your contact to create another connection link and check Update settings? No comment provided by engineer. + + Updated conditions + No comment provided by engineer. + Updating settings will re-connect the client to all servers. Актуализирането на настройките ще свърже отново клиента към всички сървъри. No comment provided by engineer. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat Актуализирай и отвори чата No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors No comment provided by engineer. @@ -7312,6 +8274,14 @@ To connect, please ask your contact to create another connection link and check Използвай сървърите на 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 Използвай чата @@ -7320,7 +8290,7 @@ To connect, please ask your contact to create another connection link and check Use current profile Използвай текущия профил - No comment provided by engineer. + new chat action Use for files @@ -7345,10 +8315,14 @@ To connect, please ask your contact to create another connection link and check Използвай интерфейса за повикване на iOS No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile Използвай нов инкогнито профил - No comment provided by engineer. + new chat action Use only local notifications? @@ -7381,6 +8355,10 @@ To connect, please ask your contact to create another connection link and check Use the app with one hand. No comment provided by engineer. + + Use web port + No comment provided by engineer. + User selection No comment provided by engineer. @@ -7565,6 +8543,10 @@ To connect, please ask your contact to create another connection link and check Съобщението при посрещане е твърде дълго No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new Какво е новото @@ -7681,12 +8663,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. Вече се свързвате с %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Вече се свързвате чрез този еднократен линк за връзка! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7696,35 +8678,33 @@ To connect, please ask your contact to create another connection link and check 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. + new chat sheet message You are already joining the group via this link. Вие вече се присъединявате към групата чрез този линк. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Вече се присъединихте към групата! Изпрати отново заявката за присъединяване? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Поканени сте в групата No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. No comment provided by engineer. @@ -7738,10 +8718,6 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. - - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. No comment provided by engineer. @@ -7828,10 +8804,14 @@ Repeat join request? Можете да видите отново линкът за покана в подробностите за връзката. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Не може да изпращате съобщения! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -7843,17 +8823,12 @@ Repeat join request? Хората могат да се свържат с вас само чрез ликовете, които споделяте. No comment provided by engineer. - - You have already requested connection via this address! - Вече сте заявили връзка през този адрес! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Вече сте направили заявката за връзка! Изпрати отново заявката за свързване? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -7907,6 +8882,14 @@ Repeat connection request? Изпратихте покана за групата No comment provided by engineer. + + You should receive notifications. + token info + + + You will be able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно! @@ -7932,11 +8915,6 @@ Repeat connection request? Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим. No comment provided by engineer. - - You will connect to all group members. - Ще се свържете с всички членове на групата. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Все още ще получавате обаждания и известия от заглушени профили, когато са активни. @@ -7971,14 +8949,13 @@ Repeat connection request? Вашите ICE сървъри No comment provided by engineer. - - Your SMP servers - Вашите SMP сървъри - No comment provided by engineer. - Your SimpleX address - Вашият SimpleX адрес + Вашият адрес в SimpleX + No comment provided by engineer. + + + Your business contact No comment provided by engineer. @@ -7988,12 +8965,12 @@ 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. @@ -8005,8 +8982,16 @@ Repeat connection request? Вашите чат профили No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. + No comment provided by engineer. + + + Your contact No comment provided by engineer. @@ -8030,7 +9015,7 @@ Repeat connection request? Your current chat database will be DELETED and REPLACED with the imported one. - Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. + Вашата текуща база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. No comment provided by engineer. @@ -8038,6 +9023,10 @@ Repeat connection request? Вашият текущ профил No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences Вашите настройки @@ -8058,6 +9047,11 @@ Repeat connection request? Вашият профил **%@** ще бъде споделен. No comment provided by engineer. + + Your profile is stored on your device and only shared with your contacts. + Профилът се споделя само с вашите контакти. + No comment provided by engineer. + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил. @@ -8067,11 +9061,6 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message - - Your profile, contacts and delivered messages are stored on your device. - Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство. - No comment provided by engineer. - Your random profile Вашият автоматично генериран профил @@ -8121,6 +9110,10 @@ Repeat connection request? по-горе, след това избери: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call обаждането прието @@ -8130,6 +9123,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin админ @@ -8150,6 +9147,10 @@ Repeat connection request? съгласуване на криптиране… chat item text + + all + member criteria value + all members всички членове @@ -8165,6 +9166,10 @@ Repeat connection request? и %lld други събития No comment provided by engineer. + + archived report + No comment provided by engineer. + attempts No comment provided by engineer. @@ -8202,7 +9207,8 @@ Repeat connection request? blocked by admin блокиран от админ - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8228,6 +9234,10 @@ Repeat connection request? повикване… call status + + can't send messages + No comment provided by engineer. + cancelled %@ отменен %@ @@ -8278,11 +9288,6 @@ Repeat connection request? свързан No comment provided by engineer. - - connected directly - свързан директно - rcv group event chat item - connecting свързване @@ -8333,6 +9338,14 @@ Repeat connection request? името на контакта %1$@ е променено на %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption контактът има e2e криптиране @@ -8343,6 +9356,14 @@ Repeat connection request? контактът няма e2e криптиране No comment provided by engineer. + + contact not ready + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator създател @@ -8370,7 +9391,8 @@ Repeat connection request? default (%@) по подразбиране (%@) - pref value + delete after time +pref value default (no) @@ -8394,7 +9416,7 @@ Repeat connection request? deleted group - групата изтрита + групата е изтрита rcv group event chat item @@ -8496,29 +9518,28 @@ Repeat connection request? грешка No comment provided by engineer. - - event happened - събитие се случи - No comment provided by engineer. - expired No comment provided by engineer. - - for better metadata privacy. - No comment provided by engineer. - forwarded препратено No comment provided by engineer. + + group + shown on group welcome message + group deleted групата е изтрита No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated профилът на групата е актуализиран @@ -8612,11 +9633,6 @@ Repeat connection request? курсив No comment provided by engineer. - - join as %@ - присъединяване като %@ - No comment provided by engineer. - left напусна @@ -8642,6 +9658,10 @@ Repeat connection request? свързан rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8671,19 +9691,19 @@ Repeat connection request? модерирано от %@ marked deleted chat item preview text + + moderator + member role + months месеци time unit - - mute - No comment provided by engineer. - never никога - No comment provided by engineer. + delete after time new message @@ -8700,11 +9720,19 @@ Repeat connection request? липсва e2e криптиране No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text няма текст copied message info in history + + not synchronized + No comment provided by engineer. + observer наблюдател @@ -8714,8 +9742,9 @@ Repeat connection request? off изключено enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -8755,6 +9784,18 @@ Repeat connection request? peer-to-peer No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + + + pending review + No comment provided by engineer. + quantum resistant e2e encryption квантово устойчиво e2e криптиране @@ -8770,6 +9811,10 @@ Repeat connection request? получено потвърждение… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call отхвърлено повикване @@ -8790,6 +9835,10 @@ Repeat connection request? премахнат адрес за контакт profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture премахната профилна снимка @@ -8800,10 +9849,34 @@ Repeat connection request? ви острани rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved запазено @@ -8838,11 +9911,6 @@ Repeat connection request? кодът за сигурност е променен chat item text - - send direct message - изпрати лично съобщение - No comment provided by engineer. - server queue info: %1$@ @@ -8898,10 +9966,6 @@ last received msg: %2$@ неизвестен статус No comment provided by engineer. - - unmute - No comment provided by engineer. - unprotected No comment provided by engineer. @@ -8989,10 +10053,9 @@ last received msg: %2$@ вие No comment provided by engineer. - - you are invited to group - вие сте поканени в групата - No comment provided by engineer. + + you accepted this member + snd group event chat item you are observer @@ -9063,7 +10126,7 @@ last received msg: %2$@
- +
@@ -9100,7 +10163,7 @@ last received msg: %2$@
- +
@@ -9122,13 +10185,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9141,15 +10208,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9168,7 +10231,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json index 5356e25a2e..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 7002f790df..fbda1abd29 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -2597,8 +2597,8 @@ Polish interface No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -3073,12 +3073,12 @@ Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. server test error @@ -3422,8 +3422,8 @@ 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. + + Your profile is stored on your device and only shared with your contacts. No comment provided by engineer. @@ -4351,7 +4351,7 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -4548,8 +4548,8 @@ SimpleX servers cannot see your profile. yes pref value - - you are invited to group + + You are invited to group 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 668888c20e..ace3079550 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (lze kopírovat) @@ -94,6 +82,7 @@ %@ downloaded + %@ staženo No comment provided by engineer. @@ -113,14 +102,17 @@ %@ server + %@ server No comment provided by engineer. %@ servers + %@ servery No comment provided by engineer. %@ uploaded + %@ nahrán No comment provided by engineer. @@ -130,10 +122,12 @@ %1$@, %2$@ + %1$@, %2$@ format for date separator in chat %@, %@ and %lld members + %@, %@ a %lld členů No comment provided by engineer. @@ -153,18 +147,22 @@ %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 @@ -174,6 +172,7 @@ %d messages not forwarded + %d zprávy nebyly přeposlány alert title @@ -191,6 +190,11 @@ %d sek time interval + + %d seconds(s) + %d sekund + delete after time + %d skipped message(s) %d přeskočené zprávy @@ -232,18 +236,22 @@ %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 messages moderated by %@ + %lld zprávy moderované %@ No comment provided by engineer. @@ -256,11 +264,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 @@ -311,30 +314,24 @@ %u zpráv přeskočeno. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) + (nový) No comment provided by engineer. (this device v%@) - No comment provided by engineer. - - - ) - ) + (toto zařízení v%@) No comment provided by engineer. **Create 1-time link**: to create and share a new invitation link. + **Vytvořit jednorázový odkaz**: pro vytvoření a sdílení nové pozvánky. No comment provided by engineer. **Create group**: to create a new group. + **Vytvořit skupinu**: pro vytvoření nové skupiny. No comment provided by engineer. @@ -363,6 +360,7 @@ **Scan / Paste link**: to connect via a link you received. + **Skenovat / Vložit odkaz**: pro připojení pomocí odkazu který jste obdrželi. No comment provided by engineer. @@ -372,6 +370,7 @@ **Warning**: the archive will be removed. + **Varování**: archiv bude odstraněn. No comment provided by engineer. @@ -389,11 +388,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,6 +410,9 @@ - optionally notify deleted contacts. - profile names with spaces. - and more! + - volitelně informuje smazané kontakty. +- profilová jména s mezerami. +- a více! No comment provided by engineer. @@ -427,13 +424,9 @@ - historie úprav. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec + 0 sek time to disappear @@ -444,7 +437,8 @@ 1 day 1 den - time interval + delete after time +time interval 1 hour @@ -459,12 +453,19 @@ 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 + 1 rok + delete after time 1-time link @@ -472,6 +473,7 @@ 1-time link can be used *with one contact only* - share in person or via any messenger. + Jednorázový odkaz lze použít *pouze s jedním kontaktem* - sdílejte osobně nebo prostřednictvím libovolného komunikátoru. No comment provided by engineer. @@ -489,11 +491,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> @@ -550,6 +547,7 @@ About operators + O operátorech No comment provided by engineer. @@ -560,11 +558,23 @@ Accept Přijmout accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Přijmout za člena + alert action + + + Accept as observer + Přijmout jako pozorovatele + alert action Accept conditions + Přijmout podmínky No comment provided by engineer. @@ -572,6 +582,11 @@ Přijmout kontakt? No comment provided by engineer. + + Accept contact request + Přijmout žádost o kontakt + alert title + Accept contact request from %@? Přijmout žádost o kontakt od %@? @@ -580,11 +595,17 @@ Accept incognito Přijmout inkognito - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Přijmout člena + alert title Accepted conditions + Přijaté podmínky No comment provided by engineer. @@ -595,8 +616,14 @@ Acknowledgement errors No comment provided by engineer. + + Active + Aktivní + token status text + Active connections + Aktivní spojení No comment provided by engineer. @@ -606,8 +633,17 @@ Add friends + Přidat přátele No comment provided by engineer. + + Add list + No comment provided by engineer. + + + Add message + placeholder for sending contact request + Add profile Přidat profil @@ -625,6 +661,7 @@ Add team members + Přidat členy týmu No comment provided by engineer. @@ -632,6 +669,11 @@ Přidat do jiného zařízení No comment provided by engineer. + + Add to list + Přidat do seznamu + No comment provided by engineer. + Add welcome message Přidat uvítací zprávu @@ -639,6 +681,7 @@ Add your team members to the conversations. + Přidat členy týmu do konverzace. No comment provided by engineer. @@ -647,14 +690,17 @@ Added message servers + Přidané servery zpráv No comment provided by engineer. Additional accent + Další zbarvení No comment provided by engineer. Additional accent 2 + Další zbarvení 2 No comment provided by engineer. @@ -673,14 +719,17 @@ Address or 1-time link? + Adresa nebo jednorázový odkaz? No comment provided by engineer. Address settings + Nastavení adresy No comment provided by engineer. Admins can block a member for all. + Správci mohou blokovat člena pro všechny. No comment provided by engineer. @@ -695,6 +744,12 @@ Advanced settings + Pokročilá nastavení + No comment provided by engineer. + + + All + Vše No comment provided by engineer. @@ -707,6 +762,11 @@ 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. + Všechny chaty budou odstraněny ze seznamu %@ a seznam bude odstraněn. + alert message + All data is erased when it is entered. Všechna data se při zadání vymažou. @@ -714,6 +774,7 @@ All data is kept private on your device. + Všechna data jsou uchována ve vašem zařízení. No comment provided by engineer. @@ -736,12 +797,23 @@ All new messages from %@ will be hidden! + Všechny nové zprávy od %@ budou skryté! No comment provided by engineer. All profiles + Všechny profily profile dropdown + + All reports will be archived for you. + No comment provided by engineer. + + + All servers + Všechny servery + No comment provided by engineer. + All your contacts will remain connected. Všechny vaše kontakty zůstanou připojeny. @@ -768,6 +840,7 @@ Allow calls? + Povolit volání? No comment provided by engineer. @@ -779,6 +852,10 @@ Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin) @@ -806,6 +883,7 @@ Allow sharing + Povolit sdílení No comment provided by engineer. @@ -813,8 +891,14 @@ Povolit nevratné smazání odeslaných zpráv. (24 hodin) No comment provided by engineer. + + Allow to report messsages to moderators. + Povolit nahlášení zpráv moderátorům. + No comment provided by engineer. + Allow to send SimpleX links. + Povolit odesílání SimpleX odkazů. No comment provided by engineer. @@ -857,6 +941,11 @@ Umožněte svým kontaktům odesílat mizící zprávy. No comment provided by engineer. + + Allow your contacts to send files and media. + Povolit vašim kontaktům odesílání souborů a médii. + No comment provided by engineer. + Allow your contacts to send voice messages. Povolte svým kontaktům odesílání hlasových zpráv. @@ -869,14 +958,17 @@ Already connecting! - No comment provided by engineer. + Již připojováno! + new chat sheet title Already joining the group! - No comment provided by engineer. + Již se ke skupině připojujete! + new chat sheet title Always use private routing. + Vždy používat soukromé směrování. No comment provided by engineer. @@ -889,6 +981,11 @@ Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle. No comment provided by engineer. + + Another reason + Jiný důvod + report reason + Answer call Přijmout hovor @@ -906,6 +1003,7 @@ App data migration + Přenos dat aplikace No comment provided by engineer. @@ -913,6 +1011,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 @@ -949,22 +1051,51 @@ Apply + Použít No comment provided by engineer. Apply to No comment provided by engineer. + + Archive + Archiv + No comment provided by engineer. + + + Archive %lld reports? + No comment provided by engineer. + + + Archive all reports? + Archivovat všechny hlášení? + No comment provided by engineer. + Archive and upload + Archivovat a nahrát 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 + Archivované kontakty No comment provided by engineer. @@ -1031,10 +1162,6 @@ Automaticky přijímat obrázky No comment provided by engineer. - - Auto-accept settings - alert title - Back Zpět @@ -1042,6 +1169,7 @@ Background + Pozadí No comment provided by engineer. @@ -1060,14 +1188,22 @@ Better calls + Lepší volání No comment provided by engineer. Better groups + Lepší skupiny + No comment provided by engineer. + + + Better groups performance + Lepší výkon skupin No comment provided by engineer. Better message dates. + Lepší datumy zpráv. No comment provided by engineer. @@ -1077,20 +1213,36 @@ Better networking + Lepší sítě No comment provided by engineer. Better notifications + Lepší upozornění + No comment provided by engineer. + + + Better privacy and security + Lepší soukromí a zabezpečení No comment provided by engineer. Better security ✅ + Lepší zabezpečení ✅ No comment provided by engineer. Better user experience No comment provided by engineer. + + Bio + No comment provided by engineer. + + + Bio too large + alert title + Black No comment provided by engineer. @@ -1109,6 +1261,7 @@ Block member + Blokovat člena No comment provided by engineer. @@ -1117,10 +1270,12 @@ Block member? + Blokovat člena? No comment provided by engineer. Blocked by admin + Blokován správcem No comment provided by engineer. @@ -1129,6 +1284,11 @@ Blur media + Rozmazat média + No comment provided by engineer. + + + Bot No comment provided by engineer. @@ -1151,6 +1311,11 @@ Vy i váš kontakt můžete posílat mizící zprávy. No comment provided by engineer. + + Both you and your contact can send files and media. + Vy i vaše kontakty můžete posílat soubory a média. + No comment provided by engineer. + Both you and your contact can send voice messages. Hlasové zprávy můžete posílat vy i váš kontakt. @@ -1163,17 +1328,32 @@ Business address + Obchodní adresa No comment provided by engineer. Business chats No comment provided by engineer. + + Business connection + 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! @@ -1186,20 +1366,29 @@ Calls prohibited! + Volání zakázáno! No comment provided by engineer. Camera not available + Kamera není k dispozici No comment provided by engineer. Can't call contact + Kontaktu nelze volat No comment provided by engineer. Can't call member + Členu nelze volat No comment provided by engineer. + + Can't change profile + Nelze změnit profil + alert title + Can't invite contact! Nelze pozvat kontakt! @@ -1212,16 +1401,19 @@ Can't message member + Členu nelze poslat zprávu No comment provided by engineer. Cancel Zrušit alert action - alert button +alert button +new chat action Cancel migration + Zrušit přesun No comment provided by engineer. @@ -1231,6 +1423,7 @@ Cannot forward message + Zprávu nelze přeposlat No comment provided by engineer. @@ -1240,10 +1433,12 @@ Capacity exceeded - recipient did not receive previously sent messages. + Kapacita překročena - příjemce neobdrží dříve poslané zprávy. snd error text Cellular + Mobilní No comment provided by engineer. @@ -1251,8 +1446,14 @@ Změnit No comment provided by engineer. + + Change automatic message deletion? + Změnit automatické mazání zpráv? + alert title + Change chat profiles + Změnit chat profily authentication reason @@ -1299,7 +1500,7 @@ Change self-destruct passcode Změnit sebedestrukční heslo authentication reason - set passcode view +set passcode view Chat @@ -1311,10 +1512,11 @@ Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors + Barvy chatu No comment provided by engineer. @@ -1334,6 +1536,7 @@ Chat database exported + Chat databáze exportována No comment provided by engineer. @@ -1361,6 +1564,7 @@ Chat migrated! + Chat přesunut! No comment provided by engineer. @@ -1389,11 +1593,27 @@ Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + + + Chat with members before they join. + No comment provided by engineer. + Chats Chaty No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1453,6 +1673,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. @@ -1470,6 +1698,10 @@ Color mode No comment provided by engineer. + + Community guidelines violation + report reason + Compare file Porovnat soubor @@ -1498,15 +1730,7 @@ Conditions of use - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1525,6 +1749,10 @@ Konfigurace serverů ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Potvrdit @@ -1568,8 +1796,13 @@ Confirm upload + Potvrdit nahrání No comment provided by engineer. + + Confirmed + token status text + Connect Připojit @@ -1577,11 +1810,11 @@ Connect automatically + Připojit automaticky No comment provided by engineer. - - Connect incognito - Spojit se inkognito + + Connect faster! 🚀 No comment provided by engineer. @@ -1592,37 +1825,35 @@ Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - No comment provided by engineer. + Připojit se k sobě? +Toto je váš vlastní jednorázový odkaz! + new chat sheet title Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link Připojte se prostřednictvím odkazu - No comment provided by engineer. + new chat sheet title Connect via one-time link Připojit se jednorázovým odkazem - No comment provided by engineer. + new chat sheet title Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1671,16 +1902,29 @@ This is your own one-time link! Connection and servers status. No comment provided by engineer. + + Connection blocked + No comment provided by engineer. + Connection error Chyba připojení - No comment provided by engineer. + alert title Connection error (AUTH) 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. @@ -1690,6 +1934,10 @@ This is your own one-time link! 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. @@ -1701,7 +1949,7 @@ This is your own one-time link! Connection timeout Časový limit připojení - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1749,6 +1997,10 @@ This is your own one-time link! Předvolby kontaktů No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -1763,6 +2015,10 @@ 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 @@ -1831,6 +2087,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/). 💻 @@ -1838,6 +2098,7 @@ This is your own one-time link! Create profile + Vytvořte si profil No comment provided by engineer. @@ -1845,9 +2106,8 @@ This is your own one-time link! Vytvořit frontu server test step - - Create secret group - Vytvořit tajnou skupinu + + Create your address No comment provided by engineer. @@ -2036,8 +2296,7 @@ This is your own one-time link! Delete Smazat alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2075,6 +2334,10 @@ This is your own one-time link! Delete chat No comment provided by engineer. + + Delete chat messages from your device. + No comment provided by engineer. + Delete chat profile Smazat chat profil @@ -2085,6 +2348,10 @@ This is your own one-time link! Smazat chat profil? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2162,6 +2429,10 @@ This is your own one-time link! Smazat odkaz? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Smazat zprávu člena? @@ -2175,7 +2446,7 @@ This is your own one-time link! Delete messages Smazat zprávy - No comment provided by engineer. + alert button Delete messages after @@ -2211,6 +2482,10 @@ This is your own one-time link! Odstranit frontu server test step + + Delete report + No comment provided by engineer. + Delete up to 20 messages at once. No comment provided by engineer. @@ -2261,11 +2536,19 @@ This is your own one-time link! Potvrzení o doručení! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Popis No comment provided by engineer. + + Description too large + alert title + Desktop address No comment provided by engineer. @@ -2356,6 +2639,14 @@ This is your own one-time link! Vypnutí zámku SimpleX authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Vypnout pro všechny @@ -2439,6 +2730,10 @@ This is your own one-time link! Do not use credentials with proxy. No comment provided by engineer. + + Documents: + No comment provided by engineer. + Don't create address Nevytvářet adresu @@ -2449,9 +2744,17 @@ This is your own one-time link! Nepovolovat No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Znovu neukazuj + alert action + + + Done No comment provided by engineer. @@ -2462,7 +2765,7 @@ This is your own one-time link! Download alert button - chat item action +chat item action Download errors @@ -2521,6 +2824,10 @@ This is your own one-time link! Upravit profil skupiny No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable Zapnout @@ -2531,8 +2838,8 @@ This is your own one-time link! Povolit (zachovat přepsání) No comment provided by engineer. - - Enable Flux + + Enable Flux in Network & servers settings for better metadata privacy. No comment provided by engineer. @@ -2548,12 +2855,16 @@ This is your own one-time link! Enable automatic message deletion? Povolit automatické mazání zpráv? - No comment provided by engineer. + alert title Enable camera access No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all Povolit pro všechny @@ -2668,6 +2979,10 @@ This is your own one-time link! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Zadat heslo @@ -2738,6 +3053,10 @@ This is your own one-time link! Chyba při přijímání žádosti o kontakt No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Chyba přidávání člena(ů) @@ -2747,11 +3066,19 @@ This is your own one-time link! Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Chuba změny adresy No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile No comment provided by engineer. @@ -2764,15 +3091,23 @@ This is your own one-time link! Error changing setting Chyba změny nastavení - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2789,6 +3124,10 @@ This is your own one-time link! 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 @@ -2803,20 +3142,28 @@ This is your own one-time link! 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 No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Chyba při mazání databáze chatu - No comment provided by engineer. + alert title Error deleting chat! Chyba při mazání chatu! - No comment provided by engineer. + alert title Error deleting connection @@ -2826,12 +3173,12 @@ This is your own one-time link! Error deleting database Chyba při mazání databáze - No comment provided by engineer. + alert title Error deleting old database Chyba při mazání staré databáze - No comment provided by engineer. + alert title Error deleting token @@ -2865,7 +3212,7 @@ This is your own one-time link! Error exporting chat database Chyba při exportu databáze chatu - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -2874,7 +3221,7 @@ This is your own one-time link! Error importing chat database Chyba při importu databáze chatu - No comment provided by engineer. + alert title Error joining group @@ -2893,6 +3240,10 @@ This is your own one-time link! Error opening chat No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file Chyba při příjmu souboru @@ -2906,10 +3257,22 @@ This is your own one-time link! Error reconnecting servers No comment provided by engineer. + + Error registering for notifications + alert title + + + Error rejecting contact request + alert title + Error removing member Chyba při odebrání člena - No comment provided by engineer. + alert title + + + Error reordering lists + alert title Error resetting statistics @@ -2920,6 +3283,10 @@ This is your own one-time link! 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 @@ -2967,6 +3334,10 @@ This is your own one-time link! Chyba při odesílání zprávy No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Chyba nastavování potvrzení o doručení! @@ -2984,7 +3355,7 @@ This is your own one-time link! Error switching profile - No comment provided by engineer. + alert title Error switching profile! @@ -2996,6 +3367,10 @@ This is your own one-time link! 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 @@ -3036,7 +3411,13 @@ This is your own one-time link! Error: %@ Chyba: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3070,6 +3451,10 @@ This is your own one-time link! Expand chat item action + + Expired + token status text + Export database Export databáze @@ -3108,24 +3493,41 @@ This is your own one-time link! 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 - No comment provided by engineer. + 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 @@ -3176,6 +3578,10 @@ This is your own one-time link! Soubory a média chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Soubory a média jsou zakázány v této skupině. @@ -3213,6 +3619,23 @@ This is your own one-time link! Najděte chaty rychleji No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Je možné, že otisk certifikátu v adrese serveru je nesprávný + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Opravit @@ -3243,6 +3666,10 @@ This is your own one-time link! Opravit nepodporované členem skupiny No comment provided by engineer. + + For all moderators + No comment provided by engineer. + For chat profile %@: servers error @@ -3256,6 +3683,10 @@ This is your own one-time link! 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. @@ -3301,8 +3732,8 @@ This is your own one-time link! No comment provided by engineer. - Forwarding server %@ failed to connect to destination server %@. Please try later. - No comment provided by engineer. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3360,6 +3791,10 @@ Error: %2$@ GIFy a nálepky No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3379,7 +3814,7 @@ Error: %2$@ Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3446,6 +3881,10 @@ Error: %2$@ Profil skupiny je uložen v zařízeních členů, nikoli na serverech. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Uvítací zpráva skupin @@ -3461,11 +3900,19 @@ Error: %2$@ 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é @@ -3523,6 +3970,10 @@ Error: %2$@ How it helps privacy No comment provided by engineer. + + How it works + alert button + How to Jak @@ -3655,6 +4106,14 @@ More improvements are coming soon! In-call sounds No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Inkognito @@ -3744,6 +4203,26 @@ More improvements are coming soon! 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. @@ -3759,7 +4238,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -3867,32 +4346,29 @@ More improvements are coming soon! Připojte se na swipe action + + Join as %@ + připojit se jako %@ + No comment provided by engineer. + Join group Připojit ke skupině - No comment provided by engineer. + new chat sheet title Join group conversations No comment provided by engineer. - - Join group? - No comment provided by engineer. - Join incognito Připojit se inkognito No comment provided by engineer. - - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -3915,6 +4391,10 @@ This is your link for group %@! Keep unused invitation? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections Zachovat vaše připojení @@ -3968,6 +4448,10 @@ This is your link for group %@! Opustit skupinu? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat Promluvme si v SimpleX Chatu @@ -3995,6 +4479,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! @@ -4005,6 +4501,10 @@ This is your link for group %@! Živé zprávy No comment provided by engineer. + + Loading profile… + in progress text + Local name Místní název @@ -4078,10 +4578,26 @@ This is your link for group %@! Člen No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + No comment provided by engineer. + Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + Member reports + chat feature + Member role will be changed to "%@". All chat members will be notified. No comment provided by engineer. @@ -4105,6 +4621,10 @@ This is your link for group %@! Člen bude odstraněn ze skupiny - toto nelze vzít zpět! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Členové skupin mohou přidávat reakce na zprávy. @@ -4115,6 +4635,10 @@ This is your link for group %@! Č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. @@ -4139,6 +4663,10 @@ This is your link for group %@! Č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. @@ -4166,6 +4694,10 @@ This is your link for group %@! Message forwarded item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. item status description @@ -4232,10 +4764,18 @@ This is your link for group %@! Zprávy No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + Messages received No comment provided by engineer. @@ -4324,6 +4864,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! @@ -4350,7 +4894,11 @@ This is your link for group %@! Mute Ztlumit - swipe action + notification label action + + + Mute all + notification label action Muted when inactive! @@ -4395,7 +4943,11 @@ This is your link for group %@! Network status Stav sítě - No comment provided by engineer. + alert title + + + New + token status text New Passcode @@ -4442,6 +4994,10 @@ This is your link for group %@! New events notification + + New group role: Moderator + No comment provided by engineer. + New in %@ Nový V %@ @@ -4456,6 +5012,10 @@ This is your link for group %@! Nová role člena No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Nová zpráva @@ -4480,6 +5040,22 @@ 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 chats with members + No comment provided by engineer. + No contacts selected Nebyl vybrán žádný kontakt @@ -4527,6 +5103,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -4548,6 +5128,10 @@ This is your link for group %@! Nemáte oprávnění nahrávat hlasové zprávy No comment provided by engineer. + + No private routing session + alert title + No push server Místní @@ -4574,6 +5158,14 @@ This is your link for group %@! 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ů @@ -4583,6 +5175,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Notes + No comment provided by engineer. + Nothing selected No comment provided by engineer. @@ -4601,10 +5197,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. @@ -4626,7 +5230,9 @@ This is your link for group %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -4685,6 +5291,14 @@ Vyžaduje povolení sítě VPN. 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. @@ -4705,6 +5319,10 @@ Vyžaduje povolení sítě VPN. Mizící zprávy můžete odesílat pouze vy. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Hlasové zprávy můžete posílat pouze vy. @@ -4730,6 +5348,10 @@ Vyžaduje povolení sítě VPN. Zmizelé zprávy může odesílat pouze váš kontakt. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Hlasové zprávy může odesílat pouze váš kontakt. @@ -4738,7 +5360,7 @@ Vyžaduje povolení sítě VPN. Open Otevřít - No comment provided by engineer. + alert action Open Settings @@ -4752,25 +5374,61 @@ Vyžaduje povolení sítě VPN. Open chat Otevřete chat - No comment provided by engineer. + new chat action Open chat console Otevřete konzolu chatu authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group - No comment provided by engineer. + new chat action + + + Open link? + alert title Open migration to another device authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -4807,6 +5465,10 @@ Vyžaduje povolení sítě VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other No comment provided by engineer. @@ -4860,10 +5522,6 @@ Vyžaduje povolení sítě VPN. Heslo k zobrazení No comment provided by engineer. - - Past member %@ - past/unknown group member - Paste desktop address No comment provided by engineer. @@ -4925,7 +5583,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. Zkontrolujte síťové připojení pomocí %@ a zkuste to znovu. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -4981,6 +5639,22 @@ 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 group moderators to review your request to join the group. + snd group event chat item + + + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + token info + Polish interface Polské rozhraní @@ -4990,11 +5664,6 @@ Error: %@ 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ý - server test error - Preserve the last message draft, with attachments. Zachování posledního návrhu zprávy s přílohami. @@ -5027,16 +5696,28 @@ Error: %@ 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. @@ -5055,7 +5736,11 @@ Error: %@ Private routing error - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5105,6 +5790,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. @@ -5148,6 +5837,10 @@ Enable in *Network & servers* settings. Chraňte své chat profily heslem! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout Časový limit protokolu @@ -5245,11 +5938,6 @@ Enable in *Network & servers* settings. Přijato v: %@ copied message info - - Received file event - Událost přijatého souboru - notification - Received message Přijatá zpráva @@ -5340,11 +6028,24 @@ Enable in *Network & servers* settings. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5354,7 +6055,11 @@ Enable in *Network & servers* settings. Reject contact request Odmítnout žádost o kontakt - No comment provided by engineer. + alert title + + + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5379,6 +6084,10 @@ Enable in *Network & servers* settings. Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Odstranit člena @@ -5394,6 +6103,10 @@ Enable in *Network & servers* settings. Odstranit přístupovou frázi z klíčenek? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate Znovu vyjednat @@ -5409,10 +6122,6 @@ Enable in *Network & servers* settings. Znovu vyjednat šifrování? No comment provided by engineer. - - Repeat connection request? - No comment provided by engineer. - Repeat download No comment provided by engineer. @@ -5421,10 +6130,6 @@ Enable in *Network & servers* settings. Repeat import No comment provided by engineer. - - Repeat join request? - No comment provided by engineer. - Repeat upload No comment provided by engineer. @@ -5434,6 +6139,50 @@ Enable in *Network & servers* settings. 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 sent to moderators + alert title + + + 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é @@ -5506,7 +6255,7 @@ Enable in *Network & servers* settings. Retry - No comment provided by engineer. + alert action Reveal @@ -5517,10 +6266,18 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later + + Review group members No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Odvolat @@ -5566,13 +6323,21 @@ Enable in *Network & servers* settings. Save Uložit alert button - chat item action +chat item action Save (and notify contacts) Uložit (a informovat kontakty) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Uložit a upozornit kontakt @@ -5597,6 +6362,14 @@ Enable in *Network & servers* settings. Uložení profilu skupiny No comment provided by engineer. + + Save group profile? + alert title + + + Save list + No comment provided by engineer. + Save passphrase and open chat Uložte heslo a otevřete chat @@ -5772,6 +6545,10 @@ Enable in *Network & servers* settings. Poslat živou zprávu - zpráva se bude aktualizovat pro příjemce během psaní No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to Potvrzení o doručení zasílat na @@ -5818,6 +6595,10 @@ Enable in *Network & servers* settings. Odeslat oznámení No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Zasílání otázek a nápadů @@ -5828,6 +6609,14 @@ Enable in *Network & servers* settings. Odeslat potvrzení No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. Odeslat je z galerie nebo vlastní klávesnice. @@ -5837,6 +6626,10 @@ Enable in *Network & servers* settings. Send up to 100 last messages to new members. No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. Odesílatel zrušil přenos souboru. @@ -5901,11 +6694,6 @@ Enable in *Network & servers* settings. Sent directly No comment provided by engineer. - - Sent file event - Odeslaná událost souboru - notification - Sent message Poslaná zpráva @@ -5964,13 +6752,13 @@ Enable in *Network & servers* settings. Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo server test error @@ -6013,6 +6801,10 @@ Enable in *Network & servers* settings. Nastavit 1 den No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… Nastavení jména kontaktu… @@ -6032,6 +6824,14 @@ Enable in *Network & servers* settings. Nastavte jej namísto ověřování systému. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Nastavit heslo @@ -6046,6 +6846,10 @@ Enable in *Network & servers* settings. Nastavení přístupové fráze pro export No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! Nastavte zprávu zobrazenou novým členům! @@ -6073,7 +6877,7 @@ Enable in *Network & servers* settings. Share Sdílet alert action - chat item action +chat item action Share 1-time link @@ -6111,6 +6915,14 @@ Enable in *Network & servers* settings. Sdílet odkaz No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile No comment provided by engineer. @@ -6128,6 +6940,22 @@ Enable in *Network & servers* settings. Sdílet s kontakty No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -6220,6 +7048,14 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX address settings + alert title + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX kontaktní adresa @@ -6257,6 +7093,10 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Zjednodušený inkognito režim @@ -6312,6 +7152,11 @@ Enable in *Network & servers* settings. Někdo notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. No comment provided by engineer. @@ -6391,6 +7236,10 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Storage + No comment provided by engineer. + Strong blur media @@ -6439,11 +7288,19 @@ Enable in *Network & servers* settings. TCP connection No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -6468,10 +7325,26 @@ Enable in *Network & servers* settings. Vyfotit No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join group + No comment provided by engineer. + Tap button Klepněte na tlačítko @@ -6506,13 +7379,17 @@ Enable in *Network & servers* settings. Temporary file error - No comment provided by engineer. + 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 @@ -6550,6 +7427,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + 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í. @@ -6606,6 +7487,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Hash předchozí zprávy se liší. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. Zpráva bude smazána pro všechny členy. @@ -6629,19 +7514,10 @@ 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. - No comment provided by engineer. - The same conditions will apply to operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! No comment provided by engineer. @@ -6654,7 +7530,7 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The sender will NOT be notified Odesílatel NEBUDE informován - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -6701,6 +7577,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. @@ -6732,23 +7612,31 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Tato skupina již neexistuje. No comment provided by engineer. - - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time 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. 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title No comment provided by engineer. @@ -6823,11 +7711,19 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. @@ -6846,6 +7742,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. 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. @@ -6863,15 +7763,9 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. 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: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -6998,13 +7892,17 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Unmute Zrušit ztlumení - swipe action + notification label action Unread Nepřečtený swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. No comment provided by engineer. @@ -7028,16 +7926,44 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Update settings? No comment provided by engineer. + + Updated conditions + 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. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat Zvýšit a otevřít chat No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors No comment provided by engineer. @@ -7081,6 +8007,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Používat servery SimpleX Chat? No comment provided by engineer. + + 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 @@ -7089,7 +8023,7 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use current profile Použít aktuální profil - No comment provided by engineer. + new chat action Use for files @@ -7113,10 +8047,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Použít rozhraní volání iOS No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile Použít nový inkognito profil - No comment provided by engineer. + new chat action Use only local notifications? @@ -7147,6 +8085,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use the app with one hand. No comment provided by engineer. + + Use web port + No comment provided by engineer. + User selection No comment provided by engineer. @@ -7320,6 +8262,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Welcome message is too long No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new Co je nového @@ -7428,11 +8374,11 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7440,31 +8386,30 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu 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. + new chat sheet message You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Jste pozváni do skupiny No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. No comment provided by engineer. @@ -7478,10 +8423,6 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. - - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. No comment provided by engineer. @@ -7565,10 +8506,14 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Nemůžete posílat zprávy! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -7580,14 +8525,10 @@ Repeat join request? 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. - You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -7641,6 +8582,14 @@ Repeat connection request? Odeslali jste pozvánku do skupiny No comment provided by engineer. + + You should receive notifications. + token info + + + You will be able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později! @@ -7665,10 +8614,6 @@ Repeat connection request? Při spuštění nebo obnovení aplikace po 30 sekundách na pozadí budete požádáni o ověření. No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní. @@ -7703,16 +8648,15 @@ Repeat connection request? 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 business contact + No comment provided by engineer. + Your calls Vaše hovory @@ -7737,8 +8681,16 @@ Repeat connection request? Vaše chat profily No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. + No comment provided by engineer. + + + Your contact No comment provided by engineer. @@ -7770,6 +8722,10 @@ Repeat connection request? Váš současný profil No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences Vaše preference @@ -7789,6 +8745,11 @@ Repeat connection request? Váš profil **%@** bude sdílen. No comment provided by engineer. + + 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 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. @@ -7798,11 +8759,6 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message - - 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í. - No comment provided by engineer. - Your random profile Váš náhodný profil @@ -7852,6 +8808,10 @@ Repeat connection request? výše, pak vyberte: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call přijatý hovor @@ -7861,6 +8821,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin správce @@ -7880,6 +8844,10 @@ Repeat connection request? povoluji šifrování… chat item text + + all + member criteria value + all members feature role @@ -7893,6 +8861,10 @@ Repeat connection request? and %lld other events No comment provided by engineer. + + archived report + No comment provided by engineer. + attempts No comment provided by engineer. @@ -7926,7 +8898,8 @@ Repeat connection request? blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -7952,6 +8925,10 @@ Repeat connection request? volání… call status + + can't send messages + No comment provided by engineer. + cancelled %@ zrušeno %@ @@ -8002,11 +8979,6 @@ Repeat connection request? připojeno No comment provided by engineer. - - connected directly - připojeno přímo - rcv group event chat item - connecting připojování @@ -8056,6 +9028,14 @@ Repeat connection request? contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption kontakt má šifrování e2e @@ -8066,6 +9046,14 @@ Repeat connection request? kontakt nemá šifrování e2e No comment provided by engineer. + + contact not ready + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator tvůrce @@ -8093,7 +9081,8 @@ Repeat connection request? default (%@) výchozí (%@) - pref value + delete after time +pref value default (no) @@ -8218,27 +9207,27 @@ Repeat connection request? chyba No comment provided by engineer. - - event happened - No comment provided by engineer. - expired No comment provided by engineer. - - for better metadata privacy. - No comment provided by engineer. - forwarded No comment provided by engineer. + + group + shown on group welcome message + group deleted skupina smazána No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated profil skupiny aktualizován @@ -8332,11 +9321,6 @@ Repeat connection request? kurzíva No comment provided by engineer. - - join as %@ - připojit se jako %@ - No comment provided by engineer. - left opustil @@ -8361,6 +9345,10 @@ Repeat connection request? připojeno rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8390,19 +9378,19 @@ Repeat connection request? moderovaný %@ marked deleted chat item preview text + + moderator + member role + months měsíců time unit - - mute - No comment provided by engineer. - never nikdy - No comment provided by engineer. + delete after time new message @@ -8419,11 +9407,19 @@ Repeat connection request? bez šifrování e2e No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text žádný text copied message info in history + + not synchronized + No comment provided by engineer. + observer pozorovatel @@ -8433,8 +9429,9 @@ Repeat connection request? off vypnuto enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -8473,6 +9470,18 @@ Repeat connection request? peer-to-peer No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -8487,6 +9496,10 @@ Repeat connection request? obdržel potvrzení… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call odmítnutý hovor @@ -8506,6 +9519,10 @@ Repeat connection request? removed contact address profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profile update event chat item @@ -8515,10 +9532,34 @@ Repeat connection request? odstranil vás rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved No comment provided by engineer. @@ -8551,11 +9592,6 @@ Repeat connection request? bezpečnostní kód změněn chat item text - - send direct message - odeslat přímou zprávu - No comment provided by engineer. - server queue info: %1$@ @@ -8606,10 +9642,6 @@ last received msg: %2$@ unknown status No comment provided by engineer. - - unmute - No comment provided by engineer. - unprotected No comment provided by engineer. @@ -8694,10 +9726,9 @@ last received msg: %2$@ you No comment provided by engineer. - - you are invited to group - jste pozváni do skupiny - No comment provided by engineer. + + you accepted this member + snd group event chat item you are observer @@ -8766,7 +9797,7 @@ last received msg: %2$@
- +
@@ -8802,7 +9833,7 @@ last received msg: %2$@
- +
@@ -8824,13 +9855,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -8843,15 +9878,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -8870,7 +9901,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json index aaa2ed1ee0..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 e993740f1c..34bbab2a6a 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (kann kopiert werden) @@ -202,6 +190,11 @@ %d s time interval + + %d seconds(s) + %d Sekunde(n) + delete after time + %d skipped message(s) %d übersprungene Nachricht(en) @@ -272,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 @@ -327,11 +315,6 @@ %u übersprungene Nachrichten. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (Neu) @@ -342,11 +325,6 @@ (Dieses Gerät hat v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen. @@ -412,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). @@ -453,11 +426,6 @@ - Nachrichtenverlauf bearbeiten No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sek @@ -470,8 +438,9 @@ 1 day - täglich - time interval + Älter als ein Tag + delete after time +time interval 1 hour @@ -485,13 +454,20 @@ 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 @@ -518,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> @@ -579,7 +550,7 @@ About operators - Über Betreiber + Über die Betreiber No comment provided by engineer. @@ -591,8 +562,19 @@ Accept Annehmen accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Als Mitglied übernehmen + alert action + + + Accept as observer + Als Beobachter übernehmen + alert action Accept conditions @@ -604,6 +586,11 @@ Kontaktanfrage annehmen? No comment provided by engineer. + + Accept contact request + Kontaktanfrage annehmen + alert title + Accept contact request from %@? Die Kontaktanfrage von %@ annehmen? @@ -612,8 +599,13 @@ Accept incognito Inkognito akzeptieren - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Mitglied annehmen + alert title Accepted conditions @@ -630,6 +622,11 @@ Fehler bei der Bestätigung No comment provided by engineer. + + Active + Aktiv + token status text + Active connections Aktive Verbindungen @@ -645,6 +642,16 @@ Freunde aufnehmen No comment provided by engineer. + + Add list + Liste hinzufügen + No comment provided by engineer. + + + Add message + Nachricht hinzufügen + placeholder for sending contact request + Add profile Profil hinzufügen @@ -652,12 +659,12 @@ Add server - Füge Server hinzu + Server hinzufügen No comment provided by engineer. Add servers by scanning QR codes. - Fügen Sie Server durch Scannen der QR Codes hinzu. + Server durch Scannen von QR Codes hinzufügen. No comment provided by engineer. @@ -670,6 +677,11 @@ 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 @@ -745,6 +757,11 @@ 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. @@ -755,6 +772,11 @@ 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. @@ -795,6 +817,16 @@ 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. @@ -835,6 +867,11 @@ Herabstufung erlauben No comment provided by engineer. + + Allow files and media only if your contact allows them. + Erlauben Sie Dateien und Medien nur dann, wenn es Ihr Kontakt ebenfalls erlaubt. + 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) @@ -870,6 +907,11 @@ 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. @@ -892,7 +934,7 @@ Allow voice messages? - Sprachnachricht erlauben? + Sprachnachrichten erlauben? No comment provided by engineer. @@ -915,6 +957,11 @@ Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten. No comment provided by engineer. + + Allow your contacts to send files and media. + Erlauben Sie Ihren Kontakten Dateien und Medien zu senden. + No comment provided by engineer. + Allow your contacts to send voice messages. Erlauben Sie Ihren Kontakten Sprachnachrichten zu senden. @@ -928,12 +975,12 @@ Already connecting! Bereits verbunden! - No comment provided by engineer. + new chat sheet title Already joining the group! Sie sind bereits Mitglied der Gruppe! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -950,6 +997,11 @@ 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 @@ -975,6 +1027,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 @@ -1020,6 +1077,21 @@ 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 @@ -1030,6 +1102,21 @@ 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 @@ -1100,11 +1187,6 @@ Bilder automatisch akzeptieren No comment provided by engineer. - - Auto-accept settings - Einstellungen automatisch akzeptieren - alert title - Back Zurück @@ -1140,6 +1222,11 @@ 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 @@ -1160,6 +1247,11 @@ 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 ✅ @@ -1170,6 +1262,16 @@ Verbesserte Nutzer-Erfahrung No comment provided by engineer. + + Bio + Biografie + No comment provided by engineer. + + + Bio too large + Biografie zu lang + alert title + Black Schwarz @@ -1217,7 +1319,12 @@ Blur media - Medium unscharf machen + Medien verpixeln + No comment provided by engineer. + + + Bot + Bot No comment provided by engineer. @@ -1240,6 +1347,11 @@ Ihr Kontakt und Sie können beide verschwindende Nachrichten senden. No comment provided by engineer. + + Both you and your contact can send files and media. + Sowohl Sie, als auch Ihr Kontakt können Dateien und Medien senden. + No comment provided by engineer. + Both you and your contact can send voice messages. Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden. @@ -1260,11 +1372,30 @@ Geschäftliche Chats No comment provided by engineer. + + Business connection + Geschäftliche Verbindung + 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! @@ -1295,6 +1426,11 @@ Mitglied kann nicht angerufen werden No comment provided by engineer. + + Can't change profile + Änderung des Profils nicht möglich + alert title + Can't invite contact! Kontakt kann nicht eingeladen werden! @@ -1314,7 +1450,8 @@ Cancel Abbrechen alert action - alert button +alert button +new chat action Cancel migration @@ -1351,6 +1488,11 @@ Ändern No comment provided by engineer. + + Change automatic message deletion? + Automatisches Löschen von Nachrichten ändern? + alert title + Change chat profiles Chat-Profile wechseln @@ -1400,7 +1542,7 @@ Change self-destruct passcode Selbstzerstörungs-Zugangscode ändern authentication reason - set passcode view +set passcode view Chat @@ -1415,7 +1557,7 @@ Chat already exists! Chat besteht bereits! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1502,11 +1644,31 @@ Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. + + Chat with admins + Chat mit Administratoren + chat toolbar + + + Chat with member + Chat mit einem Mitglied + No comment provided by engineer. + + + Chat with members before they join. + Mit Mitgliedern chatten bevor sie beitreten. + No comment provided by engineer. + Chats Chats No comment provided by engineer. + + Chats with members + Chats mit Mitgliedern + No comment provided by engineer. + Check messages every 20 min. Alle 20min Nachrichten überprüfen. @@ -1572,6 +1734,16 @@ 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? @@ -1592,6 +1764,11 @@ Farbvariante No comment provided by engineer. + + Community guidelines violation + Verstoß gegen die Gemeinschaftsrichtlinien + report reason + Compare file Datei vergleichen @@ -1625,17 +1802,7 @@ Conditions of use Nutzungsbedingungen - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1657,6 +1824,11 @@ ICE-Server konfigurieren No comment provided by engineer. + + Configure server operators + Server-Betreiber konfigurieren + No comment provided by engineer. + Confirm Bestätigen @@ -1707,6 +1879,11 @@ Hochladen bestätigen No comment provided by engineer. + + Confirmed + Bestätigt + token status text + Connect Verbinden @@ -1717,9 +1894,9 @@ Automatisch verbinden No comment provided by engineer. - - Connect incognito - Inkognito verbinden + + Connect faster! 🚀 + Schneller miteinander verbinden! 🚀 No comment provided by engineer. @@ -1732,44 +1909,39 @@ Schneller mit Ihren Freunden verbinden. No comment provided by engineer. - - Connect to yourself? - Mit Ihnen selbst verbinden? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Sich mit Ihnen selbst verbinden? Das ist Ihre eigene SimpleX-Adresse! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Mit Ihnen selbst verbinden? Das ist Ihr eigener Einmal-Link! - No comment provided by engineer. + new chat sheet title Connect via contact address Über die Kontakt-Adresse verbinden - No comment provided by engineer. + new chat sheet title Connect via link Über einen Link verbinden - No comment provided by engineer. + new chat sheet title Connect via one-time link Über einen Einmal-Link verbinden - No comment provided by engineer. + new chat sheet title Connect with %@ Mit %@ verbinden - No comment provided by engineer. + new chat action Connected @@ -1826,16 +1998,33 @@ Das ist Ihr eigener Einmal-Link! Verbindungs- und Server-Status. No comment provided by engineer. + + Connection blocked + Verbindung blockiert + No comment provided by engineer. + Connection error Verbindungsfehler - No comment provided by engineer. + alert title Connection error (AUTH) Verbindungsfehler (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + Die Verbindung wurde vom Serverbetreiber blockiert: +%@ + No comment provided by engineer. + + + Connection not ready. + Verbindung noch nicht bereit. + No comment provided by engineer. + Connection notifications Verbindungsbenachrichtigungen @@ -1846,6 +2035,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -1859,7 +2053,7 @@ Das ist Ihr eigener Einmal-Link! Connection timeout Verbindungszeitüberschreitung - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1911,6 +2105,11 @@ Das ist Ihr eigener Einmal-Link! Kontakt-Präferenzen No comment provided by engineer. + + Contact requests from groups + KONTAKTANFRAGEN VON GRUPPEN + 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! @@ -1926,6 +2125,11 @@ 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 @@ -2001,6 +2205,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/). 💻 @@ -2016,9 +2225,9 @@ Das ist Ihr eigener Einmal-Link! Erzeuge Warteschlange server test step - - Create secret group - Geheime Gruppe erstellen + + Create your address + Ihre Adresse erstellen No comment provided by engineer. @@ -2068,7 +2277,7 @@ Das ist Ihr eigener Einmal-Link! Current profile - Aktueller Profil + Aktuelles Profil No comment provided by engineer. @@ -2218,8 +2427,7 @@ Das ist Ihr eigener Einmal-Link! Delete Löschen alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2261,6 +2469,11 @@ Das ist Ihr eigener Einmal-Link! Chat löschen No comment provided by engineer. + + 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 @@ -2271,6 +2484,11 @@ Das ist Ihr eigener Einmal-Link! Chat-Profil löschen? No comment provided by engineer. + + Delete chat with member? + Chat mit dem Mitglied löschen? + alert title + Delete chat? Chat löschen? @@ -2351,6 +2569,11 @@ Das ist Ihr eigener Einmal-Link! Link löschen? No comment provided by engineer. + + Delete list? + Liste löschen? + alert title + Delete member message? Nachricht des Mitglieds löschen? @@ -2364,11 +2587,11 @@ Das ist Ihr eigener Einmal-Link! 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. @@ -2401,6 +2624,11 @@ Das ist Ihr eigener Einmal-Link! 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. @@ -2456,11 +2684,21 @@ Das ist Ihr eigener Einmal-Link! Empfangsbestätigungen! No comment provided by engineer. + + Deprecated options + Veraltete Optionen + No comment provided by engineer. + Description Beschreibung No comment provided by engineer. + + Description too large + Beschreibung zu lang + alert title + Desktop address Desktop-Adresse @@ -2561,6 +2799,16 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -2651,6 +2899,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -2661,9 +2914,19 @@ Das ist Ihr eigener Einmal-Link! 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 + alert action + + + Done + Fertig No comment provided by engineer. @@ -2675,7 +2938,7 @@ Das ist Ihr eigener Einmal-Link! Download Herunterladen alert button - chat item action +chat item action Download errors @@ -2742,6 +3005,11 @@ Das ist Ihr eigener Einmal-Link! Gruppenprofil bearbeiten No comment provided by engineer. + + Empty message! + Leere Nachricht! + No comment provided by engineer. + Enable Aktivieren @@ -2752,9 +3020,9 @@ Das ist Ihr eigener Einmal-Link! Aktivieren (vorgenommene Einstellungen bleiben erhalten) No comment provided by engineer. - - Enable Flux - Flux aktivieren + + 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. @@ -2770,13 +3038,18 @@ Das ist Ihr eigener Einmal-Link! Enable automatic message deletion? Automatisches Löschen von Nachrichten aktivieren? - No comment provided by engineer. + alert title Enable camera access Kamera-Zugriff aktivieren No comment provided by engineer. + + Enable disappearing messages by default. + Verschwindende Nachrichten sind per Voreinstellung aktiviert. + No comment provided by engineer. + Enable for all Für Alle aktivieren @@ -2897,6 +3170,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -2972,6 +3250,11 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Annehmen der Kontaktanfrage No comment provided by engineer. + + Error accepting member + Fehler beim Annehmen des Mitglieds + alert title + Error adding member(s) Fehler beim Hinzufügen von Mitgliedern @@ -2982,11 +3265,21 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Hinzufügen des Servers alert title + + Error adding short link + Fehler beim Hinzufügen eines verkürzten Links + No comment provided by engineer. + Error changing address Fehler beim Wechseln der Empfängeradresse No comment provided by engineer. + + Error changing chat profile + Fehler beim Wechseln des Chat-Profils + alert title + Error changing connection profile Fehler beim Wechseln des Verbindungs-Profils @@ -3000,17 +3293,27 @@ Das ist Ihr eigener Einmal-Link! Error changing setting Fehler beim Ändern der Einstellung - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + Fehler beim Herstellen der Verbindung zum Server, der für den Empfang von Nachrichten dieser Verbindung genutzt wird: %@ + subscription status explanation Error creating address @@ -3027,6 +3330,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -3042,20 +3350,30 @@ Das ist Ihr eigener Einmal-Link! 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 No comment provided by engineer. + + Error deleting chat + Fehler beim Löschen des Chats mit dem Mitglied + alert title + Error deleting chat database Fehler beim Löschen der Chat-Datenbank - No comment provided by engineer. + alert title Error deleting chat! Fehler beim Löschen des Chats! - No comment provided by engineer. + alert title Error deleting connection @@ -3065,12 +3383,12 @@ Das ist Ihr eigener Einmal-Link! Error deleting database Fehler beim Löschen der Datenbank - No comment provided by engineer. + alert title Error deleting old database Fehler beim Löschen der alten Datenbank - No comment provided by engineer. + alert title Error deleting token @@ -3105,7 +3423,7 @@ Das ist Ihr eigener Einmal-Link! Error exporting chat database Fehler beim Exportieren der Chat-Datenbank - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3115,7 +3433,7 @@ Das ist Ihr eigener Einmal-Link! Error importing chat database Fehler beim Importieren der Chat-Datenbank - No comment provided by engineer. + alert title Error joining group @@ -3134,12 +3452,17 @@ Das ist Ihr eigener Einmal-Link! Error opening chat - Fehler beim Öffnen des Chats + Fehler beim Öffnen des Chat + No comment provided by engineer. + + + Error opening group + Fehler beim Vorbereiten der Gruppe No comment provided by engineer. Error receiving file - Fehler beim Empfangen der Datei + Fehler beim Herunterladen der Datei alert title @@ -3152,10 +3475,25 @@ Das ist Ihr eigener Einmal-Link! 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 rejecting contact request + Fehler bei der Ablehnung der Kontaktanfrage + alert title + Error removing member Fehler beim Entfernen des Mitglieds - No comment provided by engineer. + alert title + + + Error reordering lists + Fehler beim Umsortieren der Listen + alert title Error resetting statistics @@ -3167,6 +3505,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -3217,6 +3560,11 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Senden der Nachricht No comment provided by engineer. + + Error setting auto-accept + Fehler bei der Einstellung des automatischen Akzeptierens + No comment provided by engineer. + Error setting delivery receipts! Fehler beim Setzen von Empfangsbestätigungen! @@ -3235,7 +3583,7 @@ Das ist Ihr eigener Einmal-Link! Error switching profile Fehler beim Wechseln des Profils - No comment provided by engineer. + alert title Error switching profile! @@ -3247,6 +3595,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -3290,7 +3643,14 @@ Das ist Ihr eigener Einmal-Link! Error: %@ Fehler: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + Fehler: %@. + server test error Error: URL is invalid @@ -3327,6 +3687,11 @@ Das ist Ihr eigener Einmal-Link! Erweitern chat item action + + Expired + Abgelaufen + token status text + Export database Datenbank exportieren @@ -3367,20 +3732,35 @@ Das ist Ihr eigener Einmal-Link! 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 - No comment provided by engineer. + file error alert title File errors: @@ -3389,6 +3769,13 @@ Das ist Ihr eigener Einmal-Link! %@ alert message + + File is blocked by server operator: +%@. + Die Datei wurde vom Serverbetreiber 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. @@ -3416,12 +3803,12 @@ Das ist Ihr eigener Einmal-Link! 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. @@ -3444,6 +3831,11 @@ Das ist Ihr eigener Einmal-Link! Dateien und Medien chat feature + + Files and media are prohibited in this chat. + In diesem Chat sind Dateien und Medien nicht erlaubt. + No comment provided by engineer. + Files and media are prohibited. In dieser Gruppe sind Dateien und Medien nicht erlaubt. @@ -3484,6 +3876,26 @@ Das ist Ihr eigener Einmal-Link! Chats schneller finden No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + Fingerabdruck in der Zielserveradresse stimmt nicht mit dem Zertifikat überein: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + Fingerabdruck in der Weiterleitungsserveradresse stimmt nicht mit dem Zertifikat überein: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein. + server test error + + + Fingerprint in server address does not match certificate: %@. + Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein: %@. + No comment provided by engineer. + Fix Reparieren @@ -3514,6 +3926,11 @@ Das ist Ihr eigener Einmal-Link! 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 %@: @@ -3529,6 +3946,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -3585,9 +4007,9 @@ Das ist Ihr eigener Einmal-Link! 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 %1$@ failed to connect to destination server %2$@. Please try later. + Weiterleitungsserver %1$@ konnte sich nicht mit dem Zielserver %2$@ verbinden. Bitte versuchen Sie es später erneut. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3653,6 +4075,11 @@ Fehler: %2$@ 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! @@ -3676,7 +4103,7 @@ Fehler: %2$@ Group already exists! Die Gruppe besteht bereits! - No comment provided by engineer. + new chat sheet title Group display name @@ -3743,6 +4170,11 @@ Fehler: %2$@ Das Gruppenprofil wird nur auf den Mitglieds-Systemen gespeichert und nicht auf den Servern. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + Das Gruppenprofil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an die Gruppenmitglieder gesendet. + alert message + Group welcome message Gruppen-Begrüßungsmeldung @@ -3758,11 +4190,21 @@ Fehler: %2$@ 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 @@ -3823,6 +4265,11 @@ Fehler: %2$@ Wie es die Privatsphäre schützt No comment provided by engineer. + + How it works + Wie es funktioniert + alert button + How to Anleitung @@ -3880,12 +4327,12 @@ Fehler: %2$@ 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. @@ -3965,6 +4412,16 @@ Weitere Verbesserungen sind bald verfügbar! Klingeltöne No comment provided by engineer. + + Inappropriate content + Unangemessener Inhalt + report reason + + + Inappropriate profile + Unangemessenes Profil + report reason + Incognito Inkognito @@ -4057,6 +4514,31 @@ Weitere Verbesserungen sind bald verfügbar! 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 @@ -4075,7 +4557,7 @@ Weitere Verbesserungen sind bald verfügbar! Invalid link Ungültiger Link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4188,37 +4670,32 @@ Weitere Verbesserungen sind bald verfügbar! Beitreten swipe action + + Join as %@ + Als %@ beitreten + No comment provided by engineer. + Join group Treten Sie der Gruppe bei - No comment provided by engineer. + new chat sheet title Join group conversations Gruppenunterhaltungen beitreten No comment provided by engineer. - - Join group? - Der Gruppe beitreten? - No comment provided by engineer. - Join incognito Inkognito beitreten No comment provided by engineer. - - Join with current profile - Mit dem aktuellen Profil beitreten - No comment provided by engineer. - Join your group? This is your link for group %@! Ihrer Gruppe beitreten? Das ist Ihr Link für die Gruppe %@! - No comment provided by engineer. + new chat action Joining group @@ -4245,6 +4722,11 @@ Das ist Ihr Link für die Gruppe %@! Nicht genutzte Einladung behalten? alert title + + Keep your chats clean + Ihre Chats übersichtlich halten + No comment provided by engineer. + Keep your connections Ihre Verbindungen beibehalten @@ -4300,6 +4782,11 @@ Das ist Ihr Link für die Gruppe %@! Die Gruppe verlassen? No comment provided by engineer. + + Less traffic on mobile networks. + Weniger Datenverkehr in mobilen Netzen. + No comment provided by engineer. + Let's talk in SimpleX Chat Lassen Sie uns in SimpleX Chat kommunizieren @@ -4330,6 +4817,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! @@ -4340,6 +4842,11 @@ Das ist Ihr Link für die Gruppe %@! Live Nachrichten No comment provided by engineer. + + Loading profile… + Profil wird geladen… + in progress text + Local name Lokaler Name @@ -4415,11 +4922,31 @@ Das ist Ihr Link für die Gruppe %@! Mitglied No comment provided by engineer. + + Member %@ + Mitglied %@ + past/unknown group member + + + Member admission + Aufnahme von Mitgliedern + No comment provided by engineer. + Member inactive Mitglied inaktiv item status text + + Member is deleted - can't accept request + Mitglied ist gelöscht - Anfrage kann nicht angenommen werden + No comment provided by engineer. + + + 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. @@ -4445,6 +4972,11 @@ Das ist Ihr Link für die Gruppe %@! Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. + + Member will join the group, accept member? + Mitglied wird der Gruppe beitreten. Annehmen? + alert message + Members can add message reactions. Gruppenmitglieder können eine Reaktion auf Nachrichten geben. @@ -4455,9 +4987,14 @@ Das ist Ihr Link für die Gruppe %@! 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 senden. + Gruppenmitglieder können SimpleX-Links versenden. No comment provided by engineer. @@ -4467,12 +5004,12 @@ Das ist Ihr Link für die Gruppe %@! Members can send disappearing messages. - Gruppenmitglieder können verschwindende Nachrichten senden. + Gruppenmitglieder können verschwindende Nachrichten versenden. No comment provided by engineer. Members can send files and media. - Gruppenmitglieder können Dateien und Medien senden. + Gruppenmitglieder können Dateien und Medien versenden. No comment provided by engineer. @@ -4480,6 +5017,11 @@ Das ist Ihr Link für die Gruppe %@! Gruppenmitglieder können Sprachnachrichten versenden. No comment provided by engineer. + + Mention members 👋 + Erwähnung von Mitgliedern 👋 + No comment provided by engineer. + Menus Menüs @@ -4510,6 +5052,11 @@ Das ist Ihr Link für die Gruppe %@! Nachricht weitergeleitet item status text + + Message instantly once you tap Connect. + Sobald Sie auf Verbinden tippen, erhalten Sie sofort eine Nachricht. + No comment provided by engineer. + Message may be delivered later if member becomes active. Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird. @@ -4517,7 +5064,7 @@ Das ist Ihr Link für die Gruppe %@! Message queue info - Nachrichten-Warteschlangen-Information + Information Nachrichtenwarteschlange No comment provided by engineer. @@ -4585,11 +5132,21 @@ Das ist Ihr Link für die Gruppe %@! Nachrichten No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + Nachrichten sind durch **Ende-zu-Ende-Verschlüsselung** geschützt. + No comment provided by engineer. + Messages from %@ will be shown! 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 @@ -4607,12 +5164,12 @@ Das ist Ihr Link für die Gruppe %@! 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. @@ -4690,6 +5247,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! @@ -4718,7 +5280,12 @@ Das ist Ihr Link für die Gruppe %@! Mute Stummschalten - swipe action + notification label action + + + Mute all + Alle stummschalten + notification label action Muted when inactive! @@ -4768,7 +5335,12 @@ Das ist Ihr Link für die Gruppe %@! Network status Netzwerkstatus - No comment provided by engineer. + alert title + + + New + Neu + token status text New Passcode @@ -4820,6 +5392,11 @@ Das ist Ihr Link für die Gruppe %@! Neue Ereignisse notification + + New group role: Moderator + Neue Gruppen-Rolle: Moderator + No comment provided by engineer. + New in %@ Neu in %@ @@ -4835,6 +5412,11 @@ Das ist Ihr Link für die Gruppe %@! Neue Mitgliedsrolle No comment provided by engineer. + + New member wants to join the group. + Ein neues Mitglied will der Gruppe beitreten. + rcv group event chat item + New message Neue Nachricht @@ -4860,6 +5442,26 @@ 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 chats with members + Keine Chats mit Mitgliedern + No comment provided by engineer. + No contacts selected Keine Kontakte ausgewählt @@ -4910,6 +5512,11 @@ Das ist Ihr Link für die Gruppe %@! Keine Medien- und Dateiserver. servers error + + No message + Keine Nachricht + No comment provided by engineer. + No message servers. Keine Nachrichten-Server. @@ -4935,6 +5542,11 @@ Das ist Ihr Link für die Gruppe %@! Keine Berechtigung für das Aufnehmen von Sprachnachrichten No comment provided by engineer. + + No private routing session + Keine private Routing-Sitzung + alert title + No push server Lokal @@ -4942,7 +5554,7 @@ Das ist Ihr Link für die Gruppe %@! No received or sent files - Keine empfangenen oder gesendeten Dateien + Keine herunter- oder hochgeladenen Dateien No comment provided by engineer. @@ -4952,7 +5564,7 @@ Das ist Ihr Link für die Gruppe %@! No servers to receive files. - Keine Server für den Empfang von Dateien. + Keine Server für das Herunterladen von Dateien. servers error @@ -4965,6 +5577,16 @@ Das ist Ihr Link für die Gruppe %@! 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. @@ -4975,6 +5597,11 @@ 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 @@ -4995,11 +5622,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. @@ -5022,7 +5659,9 @@ Das ist Ihr Link für die Gruppe %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5083,6 +5722,16 @@ Dies erfordert die Aktivierung eines VPNs. 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. @@ -5103,6 +5752,11 @@ Dies erfordert die Aktivierung eines VPNs. Nur Sie können verschwindende Nachrichten senden. No comment provided by engineer. + + Only you can send files and media. + Nur Sie können Dateien und Medien senden. + No comment provided by engineer. + Only you can send voice messages. Nur Sie können Sprachnachrichten versenden. @@ -5128,6 +5782,11 @@ Dies erfordert die Aktivierung eines VPNs. Nur Ihr Kontakt kann verschwindende Nachrichten senden. No comment provided by engineer. + + Only your contact can send files and media. + Nur Ihr Kontakt kann Dateien und Medien senden. + No comment provided by engineer. + Only your contact can send voice messages. Nur Ihr Kontakt kann Sprachnachrichten versenden. @@ -5136,7 +5795,7 @@ Dies erfordert die Aktivierung eines VPNs. Open Öffnen - No comment provided by engineer. + alert action Open Settings @@ -5151,28 +5810,73 @@ Dies erfordert die Aktivierung eines VPNs. Open chat Chat öffnen - No comment provided by engineer. + new chat action Open chat console Chat-Konsole öffnen authentication reason + + Open clean link + Trackingfreien Link öffnen + alert action + Open conditions Nutzungsbedingungen öffnen No comment provided by engineer. + + Open full link + Vollständigen Link öffnen + alert action + Open group Gruppe öffnen - No comment provided by engineer. + new chat action + + + Open link? + Link öffnen? + alert title Open migration to another device Migration auf ein anderes Gerät öffnen authentication reason + + Open new chat + Neuen Chat öffnen + new chat action + + + Open new group + Neue Gruppe öffnen + new chat action + + + Open to accept + Zum Akzeptieren öffnen + No comment provided by engineer. + + + Open to connect + Zum Verbinden öffnen + No comment provided by engineer. + + + Open to join + Zum Beitreten öffnen + No comment provided by engineer. + + + Open to use bot + Bot für die Nutzung erlaubt + No comment provided by engineer. + Opening app… App wird geöffnet… @@ -5218,6 +5922,11 @@ Dies erfordert die Aktivierung eines VPNs. Oder zum privaten Teilen No comment provided by engineer. + + Organize chats into lists + Chats in Listen verwalten + No comment provided by engineer. + Other Andere @@ -5275,11 +5984,6 @@ Dies erfordert die Aktivierung eines VPNs. Passwort anzeigen No comment provided by engineer. - - Past member %@ - Ehemaliges Mitglied %@ - past/unknown group member - Paste desktop address Desktop-Adresse einfügen @@ -5350,7 +6054,7 @@ Bitte teilen Sie weitere mögliche Probleme den Entwicklern mit. Please check your network connection with %@ and try again. Bitte überprüfen Sie Ihre Netzwerkverbindung mit %@ und versuchen Sie es erneut. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5409,6 +6113,26 @@ 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 group moderators to review your request to join the group. + Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können. + snd group event chat item + + + 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 @@ -5419,11 +6143,6 @@ Fehler: %@ 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 - server test error - Preserve the last message draft, with attachments. Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren. @@ -5459,16 +6178,31 @@ Fehler: %@ 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 @@ -5492,7 +6226,12 @@ Fehler: %@ Private routing error Fehler beim privaten Routing - No comment provided by engineer. + alert title + + + Private routing timeout + Zeitüberschreitung der privaten Routing-Sitzung + alert title Profile and server connections @@ -5544,6 +6283,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. @@ -5591,6 +6335,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Ihre Chat-Profile mit einem Passwort schützen! No comment provided by engineer. + + Protocol background timeout + Protokoll Hintergrund-Zeitüberschreitung + No comment provided by engineer. + Protocol timeout Protokollzeitüberschreitung @@ -5696,11 +6445,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Empfangen um: %@ copied message info - - Received file event - Datei-Ereignis empfangen - notification - Received message Empfangene Nachricht @@ -5728,7 +6472,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Receiving file will be stopped. - Der Empfang der Datei wird beendet. + Das Herunterladen der Datei wird beendet. No comment provided by engineer. @@ -5801,11 +6545,27 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5815,7 +6575,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Reject contact request Kontaktanfrage ablehnen - No comment provided by engineer. + alert title + + + Reject member? + Mitglied ablehnen? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5842,6 +6607,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Bild entfernen No comment provided by engineer. + + Remove link tracking + Link-Tracking entfernen + No comment provided by engineer. + Remove member Mitglied entfernen @@ -5857,6 +6627,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Passwort aus dem Schlüsselbund entfernen? No comment provided by engineer. + + Removes messages and blocks members. + Entfernt Nachrichten und blockiert Mitglieder. + No comment provided by engineer. + Renegotiate Neu aushandeln @@ -5872,11 +6647,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Verschlüsselung neu aushandeln? No comment provided by engineer. - - Repeat connection request? - Verbindungsanfrage wiederholen? - No comment provided by engineer. - Repeat download Herunterladen wiederholen @@ -5887,11 +6657,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Import wiederholen No comment provided by engineer. - - Repeat join request? - Verbindungsanfrage wiederholen? - No comment provided by engineer. - Repeat upload Hochladen wiederholen @@ -5902,6 +6667,61 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. 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 sent to moderators + Meldung wurde an die Moderatoren gesendet + alert title + + + 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 @@ -5980,7 +6800,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Retry Wiederholen - No comment provided by engineer. + alert action Reveal @@ -5992,11 +6812,21 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Nutzungsbedingungen einsehen No comment provided by engineer. - - Review later - Später einsehen + + Review group members + Gruppenmitglieder überprüfen No comment provided by engineer. + + Review members + Überprüfung der Mitglieder + admission stage + + + Review members before admitting ("knocking"). + Überprüfung der Mitglieder vor der Aufnahme ("Anklopfen"). + admission stage description + Revoke Widerrufen @@ -6034,7 +6864,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Safely receive files - Dateien sicher empfangen + Dateien sicher herunterladen No comment provided by engineer. @@ -6046,13 +6876,23 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Save Speichern alert button - chat item action +chat item action Save (and notify contacts) Speichern (und Kontakte benachrichtigen) alert button + + Save (and notify members) + Speichern (und Mitglieder benachrichtigen) + alert button + + + Save admission settings? + Speichern der Aufnahme-Einstellungen? + alert title + Save and notify contact Speichern und Kontakt benachrichtigen @@ -6078,6 +6918,16 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Gruppenprofil speichern No comment provided by engineer. + + Save group profile? + Gruppenprofil speichern? + alert title + + + Save list + Liste speichern + No comment provided by engineer. + Save passphrase and open chat Passwort speichern und Chat öffnen @@ -6185,7 +7035,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Search bar accepts invitation links. - In der Suchleiste werden nun auch Einladungslinks akzeptiert. + In der Suchleiste werden nun auch Einladungslinks angenommen. No comment provided by engineer. @@ -6210,7 +7060,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Security assessment - Sicherheits-Gutachten + Security-Gutachten No comment provided by engineer. @@ -6268,6 +7118,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Eine Live Nachricht senden - der/die Empfänger sieht/sehen Nachrichtenaktualisierungen, während Sie sie eingeben No comment provided by engineer. + + Send contact request? + Kontakt-Anfrage senden? + No comment provided by engineer. + Send delivery receipts to Empfangsbestätigungen senden an @@ -6318,6 +7173,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Benachrichtigungen senden No comment provided by engineer. + + Send private reports + Private Meldungen senden + No comment provided by engineer. + Send questions and ideas Senden Sie Fragen und Ideen @@ -6328,6 +7188,16 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Bestätigungen senden No comment provided by engineer. + + Send request + Anfrage senden + No comment provided by engineer. + + + Send request without message + Anfrage ohne Nachricht senden + No comment provided by engineer. + Send them from gallery or custom keyboards. Senden Sie diese aus dem Fotoalbum oder von individuellen Tastaturen. @@ -6338,6 +7208,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Bis zu 100 der letzten Nachrichten an neue Gruppenmitglieder senden. No comment provided by engineer. + + Send your private feedback to groups. + Senden Sie Ihr privates Feedback an Gruppen. + No comment provided by engineer. + Sender cancelled file transfer. Der Absender hat die Dateiübertragung abgebrochen. @@ -6403,11 +7278,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Direkt gesendet No comment provided by engineer. - - Sent file event - Datei-Ereignis wurde gesendet - notification - Sent message Gesendete Nachricht @@ -6478,14 +7348,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. 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 + + Server requires authorization to create queues, check password. + Der Server erfordert zum Erstellen von Warteschlangen eine Autorisierung. Bitte überprüfen Sie das Passwort. server test error - - Server requires authorization to upload, check password - Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung + + Server requires authorization to upload, check password. + Der Server erfordert zum Hochladen eine Autorisierung. Bitte überprüfen Sie das Passwort. server test error @@ -6533,6 +7403,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Einen Tag festlegen No comment provided by engineer. + + Set chat name… + Chat-Name festlegen… + No comment provided by engineer. + Set contact name… Kontaktname festlegen… @@ -6553,6 +7428,16 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Anstelle der System-Authentifizierung festlegen. No comment provided by engineer. + + Set member admission + Aufnahme von Mitgliedern 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 @@ -6568,6 +7453,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Passwort für den Export festlegen No comment provided by engineer. + + Set profile bio and welcome message. + Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben. + No comment provided by engineer. + Set the message shown to new members! Definieren Sie eine Begrüßungsmeldung, die neuen Mitgliedern angezeigt wird! @@ -6597,7 +7487,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Share Teilen alert action - chat item action +chat item action Share 1-time link @@ -6639,6 +7529,16 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Link teilen No comment provided by engineer. + + Share old address + Alte Adresse teilen + alert button + + + Share old link + Vollständigen Link teilen + alert button + Share profile Profil teilen @@ -6659,6 +7559,26 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Mit Kontakten teilen No comment provided by engineer. + + Share your address + Ihre Adresse teilen + No comment provided by engineer. + + + Short SimpleX address + Verkürzte SimpleX-Adresse + No comment provided by engineer. + + + Short description + Kurze Beschreibung + No comment provided by engineer. + + + Short link + Verkürzter Link + No comment provided by engineer. + Show QR code QR-Code anzeigen @@ -6759,6 +7679,16 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. SimpleX-Adresse oder Einmal-Link? No comment provided by engineer. + + SimpleX address settings + Einstellungen automatisch akzeptieren + alert title + + + SimpleX channel link + SimpleX-Kanal-Link + simplex link type + SimpleX contact address SimpleX-Kontaktadressen-Link @@ -6799,6 +7729,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Die SimpleX-Protokolle wurden von Trail of Bits überprüft. No comment provided by engineer. + + SimpleX relay link + SimpleX Relais-Link + simplex link type + Simplified incognito mode Vereinfachter Inkognito-Modus @@ -6861,6 +7796,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Jemand notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Quadratisch, kreisförmig oder irgendetwas dazwischen. @@ -6918,17 +7859,17 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. 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. @@ -6946,6 +7887,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Chat wird beendet No comment provided by engineer. + + Storage + Ablage + No comment provided by engineer. + Strong Hart @@ -7001,11 +7947,21 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. TCP-Verbindung No comment provided by engineer. + + TCP connection bg timeout + TCP-Verbindung Hintergrund-Zeitüberschreitung + 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 @@ -7031,11 +7987,31 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Machen Sie ein Foto No comment provided by engineer. + + Tap Connect to chat + Verbinden tippen, um zu chatten + No comment provided by engineer. + + + Tap Connect to send request + Verbinden tippen, um die Anfrage zu senden + No comment provided by engineer. + + + Tap Connect to use bot + Verbinden tippen, um den Bot zu nutzen. + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. No comment provided by engineer. + + Tap Join group + Tippen, um der Gruppe beizutreten + No comment provided by engineer. + Tap button Schaltfläche antippen @@ -7074,13 +8050,18 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Temporary file error Temporärer Datei-Fehler - No comment provided by engineer. + 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 @@ -7118,6 +8099,11 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + Die Adresse wird gekürzt sein, und Ihr Profil wird über die Adresse geteilt. + alert message + 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. @@ -7178,6 +8164,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Der Hash der vorherigen Nachricht unterscheidet sich. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + Der Link wird gekürzt sein, und das Gruppen-Profil wird über den Link geteilt. + alert message + The message will be deleted for all members. Diese Nachricht wird für alle Gruppenmitglieder gelöscht. @@ -7203,21 +8194,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden. No comment provided by engineer. - - The profile is 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 same conditions will apply to operator(s): **%@**. - Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**. - No comment provided by engineer. - The second preset operator in the app! Der zweite voreingestellte Netzwerk-Betreiber in der App! @@ -7231,7 +8212,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro The sender will NOT be notified Der Absender wird NICHT benachrichtigt - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7275,17 +8256,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! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. + Dieser Vorgang kann nicht rückgängig gemacht werden - alle empfangenen und gesendeten Dateien sowie Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. 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! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. + Dieser Vorgang kann nicht rückgängig gemacht werden - die früher als ausgewählt gesendeten und empfangenen Nachrichten werden gelöscht. Dies kann ein paar Minuten dauern. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + Dieser Vorgang kann nicht rückgängig gemacht werden - die in diesem Chat früher als ausgewählt gesendeten und empfangenen Nachrichten werden gelöscht. + 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, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -7318,14 +8304,9 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Diese Gruppe existiert nicht mehr. No comment provided by engineer. - - This is your own SimpleX address! - Das ist Ihre eigene SimpleX-Adresse! - No comment provided by engineer. - - - This is your own one-time link! - Das ist Ihr eigener Einmal-Link! + + 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. @@ -7333,11 +8314,26 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro 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. + + This setting is for your current profile **%@**. + Diese Einstellung gilt für Ihr aktuelles Profil **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + Die Zeit bis zum Verschwinden wird nur für neue Kontakte eingestellt. + No comment provided by engineer. + Title Bezeichnung @@ -7420,11 +8416,21 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Für das Senden No comment provided by engineer. + + To send commands you must be connected. + Um Befehle senden zu können, müssen Sie verbunden sein. + alert message + To support instant push notifications the chat database has to be migrated. Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + Wenn Sie nach dem Verbindungsversuch ein anderes Profil verwenden möchten, löschen Sie den Chat und verwenden Sie den Link erneut. + alert message + To use the servers of **%@**, accept conditions of use. Um die Server von **%@** zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren. @@ -7445,6 +8451,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Inkognito beim Verbinden einschalten. No comment provided by engineer. + + Token status: %@. + Token-Status: %@. + token status + Toolbar opacity Deckkraft der Symbolleiste @@ -7465,15 +8476,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt 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: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + Versuche eine Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten dieser Verbindung genutzt wird. + subscription status explanation Turkish interface @@ -7610,13 +8616,18 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Unmute Stummschaltung aufheben - swipe action + notification label action Unread Ungelesen swipe action + + 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. @@ -7642,16 +8653,51 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Einstellungen aktualisieren? No comment provided by engineer. + + Updated conditions + Aktualisierte Nutzungsbedingungen + No comment provided by engineer. + Updating settings will re-connect the client to all servers. Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden. No comment provided by engineer. + + Upgrade + Aktualisieren + alert button + + + Upgrade address + Adresse aktualisieren + No comment provided by engineer. + + + Upgrade address? + Adresse aktualisieren? + alert message + Upgrade and open chat Aktualisieren und den Chat öffnen No comment provided by engineer. + + Upgrade group link? + Gruppen-Link aktualisieren? + alert message + + + Upgrade link + Link hinzufügen + No comment provided by engineer. + + + Upgrade your address + Ihre Adresse aktualisieren + No comment provided by engineer. + Upload errors Fehler beim Hochladen @@ -7702,6 +8748,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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 @@ -7710,7 +8766,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use current profile Aktuelles Profil nutzen - No comment provided by engineer. + new chat action Use for files @@ -7737,10 +8793,15 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s iOS Anrufschnittstelle nutzen No comment provided by engineer. + + Use incognito profile + Inkognito-Profil nutzen + No comment provided by engineer. + Use new incognito profile Neues Inkognito-Profil nutzen - No comment provided by engineer. + new chat action Use only local notifications? @@ -7777,6 +8838,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Die App mit einer Hand bedienen. No comment provided by engineer. + + Use web port + Web-Port nutzen + No comment provided by engineer. + User selection Benutzer-Auswahl @@ -7844,12 +8910,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. @@ -7967,6 +9033,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Die Begrüßungsmeldung ist zu lang No comment provided by engineer. + + Welcome your contacts 👋 + Begrüßen Sie Ihre Kontakte 👋 + No comment provided by engineer. + What's new Was ist neu @@ -8064,7 +9135,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s You accepted connection - Sie haben die Verbindung akzeptiert + Sie haben die Verbindung angenommen No comment provided by engineer. @@ -8090,12 +9161,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s You are already connecting to %@. Sie sind bereits mit %@ verbunden. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Sie sind bereits über diesen Einmal-Link verbunden! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8105,35 +9176,35 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s You are already joining the group %@. Sie sind bereits Mitglied der Gruppe %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Sie sind über diesen Link bereits Mitglied der Gruppe! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Sie sind über diesen Link bereits Mitglied der Gruppe. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Sie sind bereits Mitglied dieser Gruppe! Verbindungsanfrage wiederholen? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten dieser Verbindung genutzt wird. + subscription status explanation You are invited to group Sie sind zu der Gruppe eingeladen No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + Sie sind nicht mit dem Server verbunden, der für den Empfang von Nachrichten dieser Verbindung genutzt wird (kein Abonnement). + subscription status explanation + 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. @@ -8149,11 +9220,6 @@ Verbindungsanfrage wiederholen? Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden. No comment provided by engineer. - - You can configure operators in Network & servers settings. - Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren. - No comment provided by engineer. - You can configure servers via settings. Sie können die Server über die Einstellungen konfigurieren. @@ -8244,10 +9310,15 @@ Verbindungsanfrage wiederholen? Den Einladungslink können Sie in den Details der Verbindung nochmals sehen. alert message + + You can view your reports in Chat with admins. + Sie können Ihre Meldungen im Chat mit den Administratoren sehen. + alert message + You can't send messages! Sie können keine Nachrichten versenden! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8259,17 +9330,12 @@ Verbindungsanfrage wiederholen? 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! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Sie haben bereits ein Verbindungsanfrage beantragt! Verbindungsanfrage wiederholen? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8326,6 +9392,16 @@ 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 able to send messages **only after your request is accepted**. + Sie können erst dann Nachrichten versenden, **sobald Ihre Anfrage angenommen wurde**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach! @@ -8338,7 +9414,7 @@ Verbindungsanfrage wiederholen? You will be connected when your connection request is accepted, please wait or check later! - Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach! + Sie werden verbunden, sobald Ihre Verbindungsanfrage angenommen wird. Bitte warten oder schauen Sie später nochmal nach! No comment provided by engineer. @@ -8351,11 +9427,6 @@ Verbindungsanfrage wiederholen? Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen. No comment provided by engineer. - - You will connect to all group members. - Sie werden mit allen Gruppenmitgliedern verbunden. - 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. @@ -8363,7 +9434,7 @@ Verbindungsanfrage wiederholen? You will stop receiving messages from this chat. Chat history will be preserved. - Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten. + Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf bleibt erhalten. No comment provided by engineer. @@ -8391,16 +9462,16 @@ Verbindungsanfrage wiederholen? 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 business contact + Ihr geschäftlicher Kontakt + No comment provided by engineer. + Your calls Anrufe @@ -8426,11 +9497,21 @@ Verbindungsanfrage wiederholen? Ihre Chat-Profile No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Ihr Chat wurde nach %@ verschoben, aber es ist ein unerwarteter Fehler aufgetreten, als Sie zum Profil weitergeleitet wurden. + alert message + + + Your connection was moved to %@ but an error happened when switching 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 + Ihr Kontakt + 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 (%@). @@ -8461,6 +9542,11 @@ Verbindungsanfrage wiederholen? Mein aktuelles Chat-Profil No comment provided by engineer. + + Your group + Ihre Gruppe + No comment provided by engineer. + Your preferences Ihre Präferenzen @@ -8468,7 +9554,7 @@ Verbindungsanfrage wiederholen? Your privacy - Ihre Privatsphäre + Privatsphäre No comment provided by engineer. @@ -8481,6 +9567,11 @@ Verbindungsanfrage wiederholen? Ihr Profil **%@** wird geteilt. No comment provided by engineer. + + Your profile is stored on your device and only shared with your contacts. + Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten 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. @@ -8491,11 +9582,6 @@ Verbindungsanfrage wiederholen? Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet. alert message - - Your profile, contacts and delivered messages are stored on your device. - Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert. - No comment provided by engineer. - Your random profile Ihr Zufallsprofil @@ -8546,6 +9632,11 @@ Verbindungsanfrage wiederholen? Danach die gewünschte Aktion auswählen: No comment provided by engineer. + + accepted %@ + %@ angenommen + rcv group event chat item + accepted call Anruf angenommen @@ -8553,9 +9644,14 @@ Verbindungsanfrage wiederholen? accepted invitation - Einladung akzeptiert + Einladung angenommen chat list item title + + accepted you + hat Sie angenommen + rcv group event chat item + admin Admin @@ -8576,6 +9672,11 @@ Verbindungsanfrage wiederholen? Verschlüsselung zustimmen… chat item text + + all + alle + member criteria value + all members Alle Mitglieder @@ -8591,6 +9692,11 @@ Verbindungsanfrage wiederholen? und %lld weitere Ereignisse No comment provided by engineer. + + archived report + Archivierte Meldung + No comment provided by engineer. + attempts Versuche @@ -8623,13 +9729,14 @@ Verbindungsanfrage wiederholen? blocked %@ - %@ wurde blockiert + hat %@ blockiert rcv group event chat item blocked by admin wurde vom Administrator blockiert - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8656,6 +9763,11 @@ Verbindungsanfrage wiederholen? Anrufen… call status + + can't send messages + Es können keine Nachrichten gesendet werden + No comment provided by engineer. + cancelled %@ abgebrochen %@ @@ -8706,11 +9818,6 @@ Verbindungsanfrage wiederholen? Verbunden No comment provided by engineer. - - connected directly - Direkt miteinander verbunden - rcv group event chat item - connecting verbinde @@ -8761,6 +9868,16 @@ Verbindungsanfrage wiederholen? Der Kontaktname wurde von %1$@ auf %2$@ geändert profile update event chat item + + contact deleted + Kontakt gelöscht + No comment provided by engineer. + + + contact disabled + Kontakt deaktiviert + No comment provided by engineer. + contact has e2e encryption Kontakt nutzt E2E-Verschlüsselung @@ -8771,6 +9888,16 @@ Verbindungsanfrage wiederholen? Kontakt nutzt keine E2E-Verschlüsselung No comment provided by engineer. + + contact not ready + Kontakt nicht bereit + No comment provided by engineer. + + + contact should accept… + Kontakt sollte annehmen… + No comment provided by engineer. + creator Ersteller @@ -8798,8 +9925,9 @@ Verbindungsanfrage wiederholen? default (%@) - Voreinstellung (%@) - pref value + Default (%@) + delete after time +pref value default (no) @@ -8926,31 +10054,31 @@ Verbindungsanfrage wiederholen? Fehler No comment provided by engineer. - - event happened - event happened - No comment provided by engineer. - expired Abgelaufen No comment provided by engineer. - - for better metadata privacy. - für einen besseren Metadatenschutz. - No comment provided by engineer. - forwarded weitergeleitet No comment provided by engineer. + + group + Gruppe + shown on group welcome message + group deleted Gruppe gelöscht No comment provided by engineer. + + group is deleted + Gruppe wurde gelöscht + No comment provided by engineer. + group profile updated Gruppenprofil aktualisiert @@ -9046,11 +10174,6 @@ Verbindungsanfrage wiederholen? kursiv No comment provided by engineer. - - join as %@ - beitreten als %@ - No comment provided by engineer. - left hat die Gruppe verlassen @@ -9076,6 +10199,11 @@ Verbindungsanfrage wiederholen? ist der Gruppe beigetreten rcv group event chat item + + member has old version + Das Mitglied hat eine alte App-Version + No comment provided by engineer. + message Nachricht @@ -9106,20 +10234,20 @@ Verbindungsanfrage wiederholen? Von %@ moderiert marked deleted chat item preview text + + moderator + Moderator + member role + months Monate time unit - - mute - Stummschalten - No comment provided by engineer. - never nie - No comment provided by engineer. + delete after time new message @@ -9136,11 +10264,21 @@ Verbindungsanfrage wiederholen? Keine E2E-Verschlüsselung No comment provided by engineer. + + no subscription + Kein Abonnement + No comment provided by engineer. + no text Kein Text copied message info in history + + not synchronized + Nicht synchronisiert + No comment provided by engineer. + observer Beobachter @@ -9150,8 +10288,9 @@ Verbindungsanfrage wiederholen? off Aus enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9193,6 +10332,21 @@ Verbindungsanfrage wiederholen? Peer-to-Peer No comment provided by engineer. + + pending + ausstehend + No comment provided by engineer. + + + pending approval + ausstehende Genehmigung + No comment provided by engineer. + + + pending review + Ausstehende Überprüfung + No comment provided by engineer. + quantum resistant e2e encryption Quantum-resistente E2E-Verschlüsselung @@ -9208,6 +10362,11 @@ Verbindungsanfrage wiederholen? Bestätigung erhalten… No comment provided by engineer. + + rejected + abgelehnt + No comment provided by engineer. + rejected call Abgelehnter Anruf @@ -9228,6 +10387,11 @@ Verbindungsanfrage wiederholen? Die Kontaktadresse wurde entfernt profile update event chat item + + removed from group + Aus der Gruppe entfernt + No comment provided by engineer. + removed profile picture Das Profil-Bild wurde entfernt @@ -9238,11 +10402,41 @@ Verbindungsanfrage wiederholen? hat Sie aus der Gruppe entfernt rcv group event chat item + + request is sent + Anfrage wurde gesendet + No comment provided by engineer. + + + request to join rejected + Beitrittsanfrage abgelehnt + No comment provided by engineer. + + + requested connection + Angefragte Verbindung + rcv group event chat item + + + requested connection from group %@ + Angefragte Verbindung von Gruppe %@ + rcv direct event chat item + requested to connect Zur Verbindung aufgefordert chat list item title + + review + Überprüfung + No comment provided by engineer. + + + reviewed by admins + Von Administratoren überprüft + No comment provided by engineer. + saved abgespeichert @@ -9278,11 +10472,6 @@ Verbindungsanfrage wiederholen? Sicherheitscode wurde geändert chat item text - - send direct message - Direktnachricht senden - No comment provided by engineer. - server queue info: %1$@ @@ -9324,7 +10513,7 @@ Zuletzt empfangene Nachricht: %2$@ unblocked %@ - %@ wurde freigegeben + hat %@ freigegeben rcv group event chat item @@ -9342,11 +10531,6 @@ Zuletzt empfangene Nachricht: %2$@ unbekannter Gruppenmitglieds-Status No comment provided by engineer. - - unmute - Stummschaltung aufheben - No comment provided by engineer. - unprotected Ungeschützt @@ -9437,10 +10621,10 @@ Zuletzt empfangene Nachricht: %2$@ Profil No comment provided by engineer. - - you are invited to group - Sie sind zu der Gruppe eingeladen - No comment provided by engineer. + + you accepted this member + Sie haben dieses Mitglied angenommen + snd group event chat item you are observer @@ -9511,7 +10695,7 @@ Zuletzt empfangene Nachricht: %2$@
- +
@@ -9541,14 +10725,14 @@ Zuletzt empfangene Nachricht: %2$@ 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
- +
@@ -9563,14 +10747,14 @@ Zuletzt empfangene Nachricht: %2$@ Copyright © 2022 SimpleX Chat. All rights reserved. - Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2025 SimpleX Chat. All rights reserved. Copyright (human-readable)
- +
@@ -9578,6 +10762,11 @@ Zuletzt empfangene Nachricht: %2$@ %d neue Ereignisse notification body + + From %d chat(s) + Von %d Chat(s) + notification body + From: %@ Von: %@ @@ -9593,16 +10782,11 @@ Zuletzt empfangene Nachricht: %2$@ Neue Nachrichten notification - - New messages in %d chats - Neue Nachrichten in %d Chats - notification body -
- +
@@ -9617,14 +10801,14 @@ Zuletzt empfangene Nachricht: %2$@ Copyright © 2024 SimpleX Chat. All rights reserved. - Copyright © 2024 SimpleX Chat. Alle Rechte vorbehalten. + Copyright © 2025 SimpleX Chat. Alle Rechte vorbehalten. Copyright (human-readable)
- +
@@ -9659,17 +10843,17 @@ Zuletzt empfangene Nachricht: %2$@ Currently maximum supported file size is %@. - Die maximale erlaubte Dateigröße beträgt aktuell %@. + Die maximal erlaubte Dateigröße beträgt aktuell %@. No comment provided by engineer. Database downgrade required - Datenbank-Herabstufung erforderlich + Datenbank-Herunterstufung ist erforderlich No comment provided by engineer. Database encrypted! - Datenbank verschlüsselt! + Datenbank ist verschlüsselt! No comment provided by engineer. @@ -9684,7 +10868,7 @@ Zuletzt empfangene Nachricht: %2$@ Database passphrase is required to open chat. - Ein Datenbank-Passwort ist erforderlich, um den Chat zu öffnen. + Um den Chat zu öffnen, ist ein Datenbank-Passwort ist erforderlich. No comment provided by engineer. @@ -9709,7 +10893,7 @@ Zuletzt empfangene Nachricht: %2$@ File error - Dateifehler + Datei-Fehler No comment provided by engineer. @@ -9744,12 +10928,12 @@ Zuletzt empfangene Nachricht: %2$@ Open the app to downgrade the database. - Öffne die App, um die Datenbank herabzustufen. + Öffnen Sie die App, um die Datenbank herunterzustufen. No comment provided by engineer. Open the app to upgrade the database. - Öffne die App, um die Datenbank zu aktualisieren. + Öffnen Sie die App, um die Datenbank zu aktualisieren. No comment provided by engineer. @@ -9759,12 +10943,12 @@ Zuletzt empfangene Nachricht: %2$@ Please create a profile in the SimpleX app - Bitte erstelle ein Profil in der SimpleX-App + Bitte erstellen Sie in der SimpleX-App ein Profil No comment provided by engineer. Selected chat preferences prohibit this message. - Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. + Die gewählten Chat-Einstellungen erlauben diese Nachricht nicht. No comment provided by engineer. @@ -9809,7 +10993,7 @@ Zuletzt empfangene Nachricht: %2$@ You can allow sharing in Privacy & Security / SimpleX Lock settings. - Du kannst das Teilen in den Einstellungen zu Datenschutz & Sicherheit - SimpleX-Sperre erlauben. + 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/contents.json b/apps/ios/SimpleX Localizations/de.xcloc/contents.json index 18b517d802..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 b601d1fa74..7a560bb41b 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -2350,8 +2350,8 @@ Available in v5.1 Polish interface No comment provided by engineer.
- - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -2726,12 +2726,12 @@ Available in v5.1 Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. server test error @@ -3043,8 +3043,8 @@ 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. + + Your profile is stored on your device and only shared with your contacts. No comment provided by engineer. @@ -3940,7 +3940,7 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -4117,8 +4117,8 @@ SimpleX servers cannot see your profile. yes pref value - - you are invited to group + + You are invited to group No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index cebd6c90d1..2f5a0acbb1 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (can be copied) @@ -202,6 +190,11 @@ %d sec time interval + + %d seconds(s) + %d seconds(s) + delete after time + %d skipped message(s) %d skipped message(s) @@ -272,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 @@ -327,11 +315,6 @@ %u messages skipped. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (new) @@ -342,11 +325,6 @@ (this device v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Create 1-time link**: to create and share a new invitation link. @@ -412,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). @@ -453,11 +426,6 @@ - editing history. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -471,7 +439,8 @@ 1 day 1 day - time interval + delete after time +time interval 1 hour @@ -486,12 +455,19 @@ 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 @@ -518,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> @@ -591,8 +562,19 @@ Accept Accept accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Accept as member + alert action + + + Accept as observer + Accept as observer + alert action Accept conditions @@ -604,6 +586,11 @@ Accept connection request? No comment provided by engineer. + + Accept contact request + Accept contact request + alert title + Accept contact request from %@? Accept contact request from %@? @@ -612,8 +599,13 @@ Accept incognito Accept incognito - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Accept member + alert title Accepted conditions @@ -630,6 +622,11 @@ Acknowledgement errors No comment provided by engineer. + + Active + Active + token status text + Active connections Active connections @@ -645,6 +642,16 @@ Add friends No comment provided by engineer. + + Add list + Add list + No comment provided by engineer. + + + Add message + Add message + placeholder for sending contact request + Add profile Add profile @@ -670,6 +677,11 @@ 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 @@ -745,6 +757,11 @@ Advanced settings No comment provided by engineer. + + All + All + No comment provided by engineer. + All app data is deleted. All app data is deleted. @@ -755,6 +772,11 @@ 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. @@ -795,6 +817,16 @@ 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. @@ -835,6 +867,11 @@ Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Allow irreversible message deletion only if your contact allows it to you. (24 hours) @@ -870,6 +907,11 @@ 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. @@ -915,6 +957,11 @@ Allow your contacts to send disappearing messages. No comment provided by engineer. + + Allow your contacts to send files and media. + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Allow your contacts to send voice messages. @@ -928,12 +975,12 @@ Already connecting! Already connecting! - No comment provided by engineer. + new chat sheet title Already joining the group! Already joining the group! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -950,6 +997,11 @@ 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 @@ -975,6 +1027,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 @@ -1020,6 +1077,21 @@ 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 @@ -1030,6 +1102,21 @@ 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 @@ -1100,11 +1187,6 @@ Auto-accept images No comment provided by engineer. - - Auto-accept settings - Auto-accept settings - alert title - Back Back @@ -1140,6 +1222,11 @@ Better groups No comment provided by engineer. + + Better groups performance + Better groups performance + No comment provided by engineer. + Better message dates. Better message dates. @@ -1160,6 +1247,11 @@ Better notifications No comment provided by engineer. + + Better privacy and security + Better privacy and security + No comment provided by engineer. + Better security ✅ Better security ✅ @@ -1170,6 +1262,16 @@ Better user experience No comment provided by engineer. + + Bio + Bio + No comment provided by engineer. + + + Bio too large + Bio too large + alert title + Black Black @@ -1220,6 +1322,11 @@ Blur media No comment provided by engineer. + + Bot + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Both you and your contact can add message reactions. @@ -1240,6 +1347,11 @@ Both you and your contact can send disappearing messages. No comment provided by engineer. + + Both you and your contact can send files and media. + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Both you and your contact can send voice messages. @@ -1260,11 +1372,30 @@ Business chats No comment provided by engineer. + + Business connection + Business connection + 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! @@ -1295,6 +1426,11 @@ Can't call member No comment provided by engineer. + + Can't change profile + Can't change profile + alert title + Can't invite contact! Can't invite contact! @@ -1314,7 +1450,8 @@ Cancel Cancel alert action - alert button +alert button +new chat action Cancel migration @@ -1351,6 +1488,11 @@ Change No comment provided by engineer. + + Change automatic message deletion? + Change automatic message deletion? + alert title + Change chat profiles Change chat profiles @@ -1400,7 +1542,7 @@ Change self-destruct passcode Change self-destruct passcode authentication reason - set passcode view +set passcode view Chat @@ -1415,7 +1557,7 @@ Chat already exists! Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1502,11 +1644,31 @@ Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + Chat with admins + chat toolbar + + + Chat with member + Chat with member + No comment provided by engineer. + + + Chat with members before they join. + Chat with members before they join. + No comment provided by engineer. + Chats Chats No comment provided by engineer. + + Chats with members + Chats with members + No comment provided by engineer. + Check messages every 20 min. Check messages every 20 min. @@ -1572,6 +1734,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? @@ -1592,6 +1764,11 @@ Color mode No comment provided by engineer. + + Community guidelines violation + Community guidelines violation + report reason + Compare file Compare file @@ -1625,17 +1802,7 @@ Conditions of use Conditions of use - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - Conditions will be accepted for enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1657,6 +1824,11 @@ Configure ICE servers No comment provided by engineer. + + Configure server operators + Configure server operators + No comment provided by engineer. + Confirm Confirm @@ -1707,6 +1879,11 @@ Confirm upload No comment provided by engineer. + + Confirmed + Confirmed + token status text + Connect Connect @@ -1717,9 +1894,9 @@ Connect automatically No comment provided by engineer. - - Connect incognito - Connect incognito + + Connect faster! 🚀 + Connect faster! 🚀 No comment provided by engineer. @@ -1732,44 +1909,39 @@ Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link Connect via link - No comment provided by engineer. + new chat sheet title Connect via one-time link Connect via one-time link - No comment provided by engineer. + new chat sheet title Connect with %@ Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1826,16 +1998,33 @@ This is your own one-time link! Connection and servers status. No comment provided by engineer. + + Connection blocked + Connection blocked + No comment provided by engineer. + Connection error Connection error - No comment provided by engineer. + alert title Connection error (AUTH) 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 @@ -1846,6 +2035,11 @@ This is your own one-time link! 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 @@ -1859,7 +2053,7 @@ This is your own one-time link! Connection timeout Connection timeout - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1911,6 +2105,11 @@ This is your own one-time link! Contact preferences No comment provided by engineer. + + Contact requests from groups + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Contact will be deleted - this cannot be undone! @@ -1926,6 +2125,11 @@ 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 @@ -2001,6 +2205,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/). 💻 @@ -2016,9 +2225,9 @@ This is your own one-time link! Create queue server test step - - Create secret group - Create secret group + + Create your address + Create your address No comment provided by engineer. @@ -2218,8 +2427,7 @@ This is your own one-time link! Delete Delete alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2261,6 +2469,11 @@ This is your own one-time link! Delete chat No comment provided by engineer. + + Delete chat messages from your device. + Delete chat messages from your device. + No comment provided by engineer. + Delete chat profile Delete chat profile @@ -2271,6 +2484,11 @@ This is your own one-time link! Delete chat profile? No comment provided by engineer. + + Delete chat with member? + Delete chat with member? + alert title + Delete chat? Delete chat? @@ -2351,6 +2569,11 @@ This is your own one-time link! Delete link? No comment provided by engineer. + + Delete list? + Delete list? + alert title + Delete member message? Delete member message? @@ -2364,7 +2587,7 @@ This is your own one-time link! Delete messages Delete messages - No comment provided by engineer. + alert button Delete messages after @@ -2401,6 +2624,11 @@ This is your own one-time link! 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. @@ -2456,11 +2684,21 @@ This is your own one-time link! Delivery receipts! No comment provided by engineer. + + Deprecated options + Deprecated options + No comment provided by engineer. + Description Description No comment provided by engineer. + + Description too large + Description too large + alert title + Desktop address Desktop address @@ -2561,6 +2799,16 @@ This is your own one-time link! 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 @@ -2651,6 +2899,11 @@ This is your own one-time link! 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 @@ -2661,9 +2914,19 @@ This is your own one-time link! 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 + alert action + + + Done + Done No comment provided by engineer. @@ -2675,7 +2938,7 @@ This is your own one-time link! Download Download alert button - chat item action +chat item action Download errors @@ -2742,6 +3005,11 @@ This is your own one-time link! Edit group profile No comment provided by engineer. + + Empty message! + Empty message! + No comment provided by engineer. + Enable Enable @@ -2752,9 +3020,9 @@ This is your own one-time link! Enable (keep overrides) No comment provided by engineer. - - Enable Flux - Enable Flux + + 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. @@ -2770,13 +3038,18 @@ This is your own one-time link! Enable automatic message deletion? Enable automatic message deletion? - No comment provided by engineer. + alert title Enable camera access Enable camera access No comment provided by engineer. + + Enable disappearing messages by default. + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all Enable for all @@ -2897,6 +3170,11 @@ This is your own one-time link! 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 @@ -2972,6 +3250,11 @@ This is your own one-time link! Error accepting contact request No comment provided by engineer. + + Error accepting member + Error accepting member + alert title + Error adding member(s) Error adding member(s) @@ -2982,11 +3265,21 @@ This is your own one-time link! Error adding server alert title + + Error adding short link + Error adding short link + No comment provided by engineer. + Error changing address Error changing address No comment provided by engineer. + + Error changing chat profile + Error changing chat profile + alert title + Error changing connection profile Error changing connection profile @@ -3000,17 +3293,27 @@ This is your own one-time link! Error changing setting Error changing setting - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -3027,6 +3330,11 @@ This is your own one-time link! 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 @@ -3042,20 +3350,30 @@ This is your own one-time link! 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 No comment provided by engineer. + + Error deleting chat + Error deleting chat + alert title + Error deleting chat database Error deleting chat database - No comment provided by engineer. + alert title Error deleting chat! Error deleting chat! - No comment provided by engineer. + alert title Error deleting connection @@ -3065,12 +3383,12 @@ This is your own one-time link! Error deleting database Error deleting database - No comment provided by engineer. + alert title Error deleting old database Error deleting old database - No comment provided by engineer. + alert title Error deleting token @@ -3105,7 +3423,7 @@ This is your own one-time link! Error exporting chat database Error exporting chat database - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3115,7 +3433,7 @@ This is your own one-time link! Error importing chat database Error importing chat database - No comment provided by engineer. + alert title Error joining group @@ -3137,6 +3455,11 @@ This is your own one-time link! Error opening chat No comment provided by engineer. + + Error opening group + Error opening group + No comment provided by engineer. + Error receiving file Error receiving file @@ -3152,10 +3475,25 @@ This is your own one-time link! Error reconnecting servers No comment provided by engineer. + + Error registering for notifications + Error registering for notifications + alert title + + + Error rejecting contact request + Error rejecting contact request + alert title + Error removing member Error removing member - No comment provided by engineer. + alert title + + + Error reordering lists + Error reordering lists + alert title Error resetting statistics @@ -3167,6 +3505,11 @@ This is your own one-time link! 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 @@ -3217,6 +3560,11 @@ This is your own one-time link! Error sending message No comment provided by engineer. + + Error setting auto-accept + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Error setting delivery receipts! @@ -3235,7 +3583,7 @@ This is your own one-time link! Error switching profile Error switching profile - No comment provided by engineer. + alert title Error switching profile! @@ -3247,6 +3595,11 @@ This is your own one-time link! 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 @@ -3290,7 +3643,14 @@ This is your own one-time link! Error: %@ Error: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + Error: %@. + server test error Error: URL is invalid @@ -3327,6 +3687,11 @@ This is your own one-time link! Expand chat item action + + Expired + Expired + token status text + Export database Export database @@ -3367,20 +3732,35 @@ This is your own one-time link! 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 - No comment provided by engineer. + file error alert title File errors: @@ -3389,6 +3769,13 @@ This is your own one-time link! %@ 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. @@ -3444,6 +3831,11 @@ This is your own one-time link! Files and media chat feature + + Files and media are prohibited in this chat. + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Files and media are prohibited. @@ -3484,6 +3876,26 @@ This is your own one-time link! Find chats faster No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Fingerprint in server address does not match certificate. + server test error + + + Fingerprint in server address does not match certificate: %@. + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Fix @@ -3514,6 +3926,11 @@ This is your own one-time link! 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 %@: @@ -3529,6 +3946,11 @@ This is your own one-time link! 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 @@ -3585,9 +4007,9 @@ This is your own one-time link! 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 %1$@ failed to connect to destination server %2$@. Please try later. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3653,6 +4075,11 @@ Error: %2$@ 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! @@ -3676,7 +4103,7 @@ Error: %2$@ Group already exists! Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3743,6 +4170,11 @@ Error: %2$@ Group profile is stored on members' devices, not on the servers. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Group welcome message @@ -3758,11 +4190,21 @@ Error: %2$@ 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 @@ -3823,6 +4265,11 @@ Error: %2$@ How it helps privacy No comment provided by engineer. + + How it works + How it works + alert button + How to How to @@ -3965,6 +4412,16 @@ More improvements are coming soon! In-call sounds No comment provided by engineer. + + Inappropriate content + Inappropriate content + report reason + + + Inappropriate profile + Inappropriate profile + report reason + Incognito Incognito @@ -4057,6 +4514,31 @@ More improvements are coming soon! 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 @@ -4075,7 +4557,7 @@ More improvements are coming soon! Invalid link Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4188,37 +4670,32 @@ More improvements are coming soon! Join swipe action + + Join as %@ + Join as %@ + No comment provided by engineer. + Join group Join group - No comment provided by engineer. + new chat sheet title Join group conversations Join group conversations No comment provided by engineer. - - Join group? - Join group? - No comment provided by engineer. - Join incognito Join incognito No comment provided by engineer. - - Join with current profile - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -4245,6 +4722,11 @@ This is your link for group %@! Keep unused invitation? alert title + + Keep your chats clean + Keep your chats clean + No comment provided by engineer. + Keep your connections Keep your connections @@ -4300,6 +4782,11 @@ This is your link for group %@! Leave group? No comment provided by engineer. + + Less traffic on mobile networks. + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat Let's talk in SimpleX Chat @@ -4330,6 +4817,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! @@ -4340,6 +4842,11 @@ This is your link for group %@! Live messages No comment provided by engineer. + + Loading profile… + Loading profile… + in progress text + Local name Local name @@ -4415,11 +4922,31 @@ This is your link for group %@! Member No comment provided by engineer. + + Member %@ + Member %@ + past/unknown group member + + + Member admission + Member admission + No comment provided by engineer. + Member inactive Member inactive item status text + + Member is deleted - can't accept request + Member is deleted - can't accept request + No comment provided by engineer. + + + 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. @@ -4445,6 +4972,11 @@ This is your link for group %@! Member will be removed from group - this cannot be undone! No comment provided by engineer. + + Member will join the group, accept member? + Member will join the group, accept member? + alert message + Members can add message reactions. Members can add message reactions. @@ -4455,6 +4987,11 @@ This is your link for group %@! 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. @@ -4480,6 +5017,11 @@ This is your link for group %@! Members can send voice messages. No comment provided by engineer. + + Mention members 👋 + Mention members 👋 + No comment provided by engineer. + Menus Menus @@ -4510,6 +5052,11 @@ This is your link for group %@! Message forwarded item status text + + Message instantly once you tap Connect. + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. Message may be delivered later if member becomes active. @@ -4585,11 +5132,21 @@ This is your link for group %@! Messages & files No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! 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 @@ -4690,6 +5247,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! @@ -4718,7 +5280,12 @@ This is your link for group %@! Mute Mute - swipe action + notification label action + + + Mute all + Mute all + notification label action Muted when inactive! @@ -4768,7 +5335,12 @@ This is your link for group %@! Network status Network status - No comment provided by engineer. + alert title + + + New + New + token status text New Passcode @@ -4820,6 +5392,11 @@ This is your link for group %@! New events notification + + New group role: Moderator + New group role: Moderator + No comment provided by engineer. + New in %@ New in %@ @@ -4835,6 +5412,11 @@ This is your link for group %@! New member role No comment provided by engineer. + + New member wants to join the group. + New member wants to join the group. + rcv group event chat item + New message New message @@ -4860,6 +5442,26 @@ 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 chats with members + No chats with members + No comment provided by engineer. + No contacts selected No contacts selected @@ -4910,6 +5512,11 @@ This is your link for group %@! No media & file servers. servers error + + No message + No message + No comment provided by engineer. + No message servers. No message servers. @@ -4935,6 +5542,11 @@ This is your link for group %@! No permission to record voice message No comment provided by engineer. + + No private routing session + No private routing session + alert title + No push server No push server @@ -4965,6 +5577,16 @@ This is your link for group %@! 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. @@ -4975,6 +5597,11 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Notes + Notes + No comment provided by engineer. + Nothing selected Nothing selected @@ -4995,11 +5622,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. @@ -5022,7 +5659,9 @@ This is your link for group %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5083,6 +5722,16 @@ Requires compatible VPN. 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. @@ -5103,6 +5752,11 @@ Requires compatible VPN. Only you can send disappearing messages. No comment provided by engineer. + + Only you can send files and media. + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Only you can send voice messages. @@ -5128,6 +5782,11 @@ Requires compatible VPN. Only your contact can send disappearing messages. No comment provided by engineer. + + Only your contact can send files and media. + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Only your contact can send voice messages. @@ -5136,7 +5795,7 @@ Requires compatible VPN. Open Open - No comment provided by engineer. + alert action Open Settings @@ -5151,28 +5810,73 @@ Requires compatible VPN. Open chat Open chat - No comment provided by engineer. + new chat action Open chat console Open chat console authentication reason + + Open clean link + Open clean link + alert action + Open conditions Open conditions No comment provided by engineer. + + Open full link + Open full link + alert action + Open group Open group - No comment provided by engineer. + new chat action + + + Open link? + Open link? + alert title Open migration to another device Open migration to another device authentication reason + + Open new chat + Open new chat + new chat action + + + Open new group + Open new group + new chat action + + + Open to accept + Open to accept + No comment provided by engineer. + + + Open to connect + Open to connect + No comment provided by engineer. + + + Open to join + Open to join + No comment provided by engineer. + + + Open to use bot + Open to use bot + No comment provided by engineer. + Opening app… Opening app… @@ -5218,6 +5922,11 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + Organize chats into lists + No comment provided by engineer. + Other Other @@ -5275,11 +5984,6 @@ Requires compatible VPN. Password to show No comment provided by engineer. - - Past member %@ - Past member %@ - past/unknown group member - Paste desktop address Paste desktop address @@ -5350,7 +6054,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. Please check your network connection with %@ and try again. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5409,6 +6113,26 @@ 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 group moderators to review your request to join the group. + Please wait for group moderators to review your request to join the group. + snd group event chat item + + + 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 @@ -5419,11 +6143,6 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Possibly, certificate fingerprint in server address is incorrect - server test error - Preserve the last message draft, with attachments. Preserve the last message draft, with attachments. @@ -5459,16 +6178,31 @@ Error: %@ 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 @@ -5492,7 +6226,12 @@ Error: %@ Private routing error Private routing error - No comment provided by engineer. + alert title + + + Private routing timeout + Private routing timeout + alert title Profile and server connections @@ -5544,6 +6283,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. @@ -5591,6 +6335,11 @@ Enable in *Network & servers* settings. Protect your chat profiles with a password! No comment provided by engineer. + + Protocol background timeout + Protocol background timeout + No comment provided by engineer. + Protocol timeout Protocol timeout @@ -5696,11 +6445,6 @@ Enable in *Network & servers* settings. Received at: %@ copied message info - - Received file event - Received file event - notification - Received message Received message @@ -5801,11 +6545,27 @@ Enable in *Network & servers* settings. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5815,7 +6575,12 @@ Enable in *Network & servers* settings. Reject contact request Reject contact request - No comment provided by engineer. + alert title + + + Reject member? + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5842,6 +6607,11 @@ Enable in *Network & servers* settings. Remove image No comment provided by engineer. + + Remove link tracking + Remove link tracking + No comment provided by engineer. + Remove member Remove member @@ -5857,6 +6627,11 @@ Enable in *Network & servers* settings. Remove passphrase from keychain? No comment provided by engineer. + + Removes messages and blocks members. + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate Renegotiate @@ -5872,11 +6647,6 @@ Enable in *Network & servers* settings. Renegotiate encryption? No comment provided by engineer. - - Repeat connection request? - Repeat connection request? - No comment provided by engineer. - Repeat download Repeat download @@ -5887,11 +6657,6 @@ Enable in *Network & servers* settings. Repeat import No comment provided by engineer. - - Repeat join request? - Repeat join request? - No comment provided by engineer. - Repeat upload Repeat upload @@ -5902,6 +6667,61 @@ Enable in *Network & servers* settings. 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 sent to moderators + Report sent to moderators + alert title + + + 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 @@ -5980,7 +6800,7 @@ Enable in *Network & servers* settings. Retry Retry - No comment provided by engineer. + alert action Reveal @@ -5992,11 +6812,21 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later - Review later + + Review group members + Review group members No comment provided by engineer. + + Review members + Review members + admission stage + + + Review members before admitting ("knocking"). + Review members before admitting ("knocking"). + admission stage description + Revoke Revoke @@ -6046,13 +6876,23 @@ Enable in *Network & servers* settings. Save Save alert button - chat item action +chat item action Save (and notify contacts) Save (and notify contacts) alert button + + Save (and notify members) + Save (and notify members) + alert button + + + Save admission settings? + Save admission settings? + alert title + Save and notify contact Save and notify contact @@ -6078,6 +6918,16 @@ Enable in *Network & servers* settings. Save group profile No comment provided by engineer. + + Save group profile? + Save group profile? + alert title + + + Save list + Save list + No comment provided by engineer. + Save passphrase and open chat Save passphrase and open chat @@ -6268,6 +7118,11 @@ Enable in *Network & servers* settings. Send a live message - it will update for the recipient(s) as you type it No comment provided by engineer. + + Send contact request? + Send contact request? + No comment provided by engineer. + Send delivery receipts to Send delivery receipts to @@ -6318,6 +7173,11 @@ Enable in *Network & servers* settings. Send notifications No comment provided by engineer. + + Send private reports + Send private reports + No comment provided by engineer. + Send questions and ideas Send questions and ideas @@ -6328,6 +7188,16 @@ Enable in *Network & servers* settings. Send receipts No comment provided by engineer. + + Send request + Send request + No comment provided by engineer. + + + Send request without message + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. Send them from gallery or custom keyboards. @@ -6338,6 +7208,11 @@ Enable in *Network & servers* settings. Send up to 100 last messages to new members. No comment provided by engineer. + + Send your private feedback to groups. + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. Sender cancelled file transfer. @@ -6403,11 +7278,6 @@ Enable in *Network & servers* settings. Sent directly No comment provided by engineer. - - Sent file event - Sent file event - notification - Sent message Sent message @@ -6478,14 +7348,14 @@ Enable in *Network & servers* settings. Server protocol changed. alert title - - Server requires authorization to create queues, check password - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. + Server requires authorization to upload, check password. server test error @@ -6533,6 +7403,11 @@ Enable in *Network & servers* settings. 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… @@ -6553,6 +7428,16 @@ Enable in *Network & servers* settings. Set it instead of system authentication. No comment provided by engineer. + + Set member admission + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + Set message expiration in chats. + No comment provided by engineer. + Set passcode Set passcode @@ -6568,6 +7453,11 @@ Enable in *Network & servers* settings. Set passphrase to export No comment provided by engineer. + + Set profile bio and welcome message. + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! Set the message shown to new members! @@ -6597,7 +7487,7 @@ Enable in *Network & servers* settings. Share Share alert action - chat item action +chat item action Share 1-time link @@ -6639,6 +7529,16 @@ Enable in *Network & servers* settings. Share link No comment provided by engineer. + + Share old address + Share old address + alert button + + + Share old link + Share old link + alert button + Share profile Share profile @@ -6659,6 +7559,26 @@ Enable in *Network & servers* settings. Share with contacts No comment provided by engineer. + + Share your address + Share your address + No comment provided by engineer. + + + Short SimpleX address + Short SimpleX address + No comment provided by engineer. + + + Short description + Short description + No comment provided by engineer. + + + Short link + Short link + No comment provided by engineer. + Show QR code Show QR code @@ -6759,6 +7679,16 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX address settings + SimpleX address settings + alert title + + + SimpleX channel link + SimpleX channel link + simplex link type + SimpleX contact address SimpleX contact address @@ -6799,6 +7729,11 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + SimpleX relay link + simplex link type + Simplified incognito mode Simplified incognito mode @@ -6861,6 +7796,12 @@ Enable in *Network & servers* settings. Somebody notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Square, circle, or anything in between. @@ -6946,6 +7887,11 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Storage + Storage + No comment provided by engineer. + Strong Strong @@ -7001,11 +7947,21 @@ Enable in *Network & servers* settings. TCP connection No comment provided by engineer. + + TCP connection bg timeout + TCP connection bg timeout + 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 @@ -7031,11 +7987,31 @@ Enable in *Network & servers* settings. Take picture No comment provided by engineer. + + Tap Connect to chat + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join group + Tap Join group + No comment provided by engineer. + Tap button Tap button @@ -7074,13 +8050,18 @@ Enable in *Network & servers* settings. Temporary file error Temporary file error - No comment provided by engineer. + 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 @@ -7118,6 +8099,11 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + The address will be short, and your profile will be shared via the address. + alert message + The app can notify you when you receive messages or contact requests - please open settings to enable. The app can notify you when you receive messages or contact requests - please open settings to enable. @@ -7178,6 +8164,11 @@ It can happen because of some bug or when the connection is compromised.The hash of the previous message is different. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. The message will be deleted for all members. @@ -7203,21 +8194,11 @@ It can happen because of some bug or when the connection is compromised.The old database was not removed during the migration, it can be deleted. No comment provided by engineer. - - The profile is only shared with your contacts. - The profile is only shared with your contacts. - No comment provided by engineer. - The same conditions will apply to operator **%@**. The same conditions will apply to operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - The same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! The second preset operator in the app! @@ -7231,7 +8212,7 @@ It can happen because of some bug or when the connection is compromised. The sender will NOT be notified The sender will NOT be notified - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7283,6 +8264,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. @@ -7318,14 +8304,9 @@ It can happen because of some bug or when the connection is compromised.This group no longer exists. No comment provided by engineer. - - This is your own SimpleX address! - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time link! - This is your own one-time link! + + 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. @@ -7333,11 +8314,26 @@ It can happen because of some bug or when the connection is compromised.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. + + This setting is for your current profile **%@**. + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title Title @@ -7420,11 +8416,21 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. To support instant push notifications the chat database has to be migrated. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. To use the servers of **%@**, accept conditions of use. @@ -7445,6 +8451,11 @@ You will be prompted to complete authentication before this feature is enabled.< Toggle incognito when connecting. No comment provided by engineer. + + Token status: %@. + Token status: %@. + token status + Toolbar opacity Toolbar opacity @@ -7465,15 +8476,10 @@ You will be prompted to complete authentication before this feature is enabled.< 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: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Trying to connect to the server used to receive messages from this contact. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7610,13 +8616,18 @@ To connect, please ask your contact to create another connection link and check Unmute Unmute - swipe action + notification label action Unread Unread swipe action + + Unsupported connection link + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. Up to 100 last messages are sent to new members. @@ -7642,16 +8653,51 @@ To connect, please ask your contact to create another connection link and check Update settings? No comment provided by engineer. + + Updated conditions + Updated conditions + No comment provided by engineer. + Updating settings will re-connect the client to all servers. Updating settings will re-connect the client to all servers. No comment provided by engineer. + + Upgrade + Upgrade + alert button + + + Upgrade address + Upgrade address + No comment provided by engineer. + + + Upgrade address? + Upgrade address? + alert message + Upgrade and open chat Upgrade and open chat No comment provided by engineer. + + Upgrade group link? + Upgrade group link? + alert message + + + Upgrade link + Upgrade link + No comment provided by engineer. + + + Upgrade your address + Upgrade your address + No comment provided by engineer. + Upload errors Upload errors @@ -7702,6 +8748,16 @@ To connect, please ask your contact to create another connection link and check 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 @@ -7710,7 +8766,7 @@ To connect, please ask your contact to create another connection link and check Use current profile Use current profile - No comment provided by engineer. + new chat action Use for files @@ -7737,10 +8793,15 @@ To connect, please ask your contact to create another connection link and check Use iOS call interface No comment provided by engineer. + + Use incognito profile + Use incognito profile + No comment provided by engineer. + Use new incognito profile Use new incognito profile - No comment provided by engineer. + new chat action Use only local notifications? @@ -7777,6 +8838,11 @@ To connect, please ask your contact to create another connection link and check Use the app with one hand. No comment provided by engineer. + + Use web port + Use web port + No comment provided by engineer. + User selection User selection @@ -7967,6 +9033,11 @@ To connect, please ask your contact to create another connection link and check Welcome message is too long No comment provided by engineer. + + Welcome your contacts 👋 + Welcome your contacts 👋 + No comment provided by engineer. + What's new What's new @@ -8090,12 +9161,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8105,35 +9176,35 @@ To connect, please ask your contact to create another connection link and check You are already joining the group %@. You are already joining the group %@. - No comment provided by engineer. - - - You are already joining the group via this link! - You are already joining the group via this link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - You are connected to the server used to receive messages from this contact. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group You are invited to group No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + 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. @@ -8149,11 +9220,6 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. - - You can configure operators in Network & servers settings. - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. You can configure servers via settings. @@ -8244,10 +9310,15 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + You can view your reports in Chat with admins. + alert message + You can't send messages! You can't send messages! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8259,17 +9330,12 @@ Repeat join request? 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! - No comment provided by engineer. - You have already requested connection! Repeat connection request? You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8326,6 +9392,16 @@ 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 able to send messages **only after your request is accepted**. + You will be able to send messages **only after your request is accepted**. + No comment provided by engineer. + 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! @@ -8351,11 +9427,6 @@ Repeat connection request? 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 connect to all group members. - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. You will still receive calls and notifications from muted profiles when they are active. @@ -8391,16 +9462,16 @@ Repeat connection request? 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 business contact + Your business contact + No comment provided by engineer. + Your calls Your calls @@ -8426,9 +9497,19 @@ Repeat connection request? Your chat profiles No comment provided by engineer. - - 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. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. + Your connection was moved to %@ but an error happened when switching profile. + No comment provided by engineer. + + + Your contact + Your contact No comment provided by engineer. @@ -8461,6 +9542,11 @@ Repeat connection request? Your current profile No comment provided by engineer. + + Your group + Your group + No comment provided by engineer. + Your preferences Your preferences @@ -8481,6 +9567,11 @@ Repeat connection request? Your profile **%@** will be shared. No comment provided by engineer. + + 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 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. @@ -8491,11 +9582,6 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message - - Your profile, contacts and delivered messages are stored on your device. - Your profile, contacts and delivered messages are stored on your device. - No comment provided by engineer. - Your random profile Your random profile @@ -8546,6 +9632,11 @@ Repeat connection request? above, then choose: No comment provided by engineer. + + accepted %@ + accepted %@ + rcv group event chat item + accepted call accepted call @@ -8556,6 +9647,11 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + accepted you + rcv group event chat item + admin admin @@ -8576,6 +9672,11 @@ Repeat connection request? agreeing encryption… chat item text + + all + all + member criteria value + all members all members @@ -8591,6 +9692,11 @@ Repeat connection request? and %lld other events No comment provided by engineer. + + archived report + archived report + No comment provided by engineer. + attempts attempts @@ -8629,7 +9735,8 @@ Repeat connection request? blocked by admin blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8656,6 +9763,11 @@ Repeat connection request? calling… call status + + can't send messages + can't send messages + No comment provided by engineer. + cancelled %@ cancelled %@ @@ -8706,11 +9818,6 @@ Repeat connection request? connected No comment provided by engineer. - - connected directly - connected directly - rcv group event chat item - connecting connecting @@ -8761,6 +9868,16 @@ Repeat connection request? contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + contact deleted + No comment provided by engineer. + + + contact disabled + contact disabled + No comment provided by engineer. + contact has e2e encryption contact has e2e encryption @@ -8771,6 +9888,16 @@ Repeat connection request? contact has no e2e encryption No comment provided by engineer. + + contact not ready + contact not ready + No comment provided by engineer. + + + contact should accept… + contact should accept… + No comment provided by engineer. + creator creator @@ -8799,7 +9926,8 @@ Repeat connection request? default (%@) default (%@) - pref value + delete after time +pref value default (no) @@ -8926,31 +10054,31 @@ Repeat connection request? error No comment provided by engineer. - - event happened - event happened - No comment provided by engineer. - expired expired No comment provided by engineer. - - for better metadata privacy. - for better metadata privacy. - No comment provided by engineer. - forwarded forwarded No comment provided by engineer. + + group + group + shown on group welcome message + group deleted group deleted No comment provided by engineer. + + group is deleted + group is deleted + No comment provided by engineer. + group profile updated group profile updated @@ -9046,11 +10174,6 @@ Repeat connection request? italic No comment provided by engineer. - - join as %@ - join as %@ - No comment provided by engineer. - left left @@ -9076,6 +10199,11 @@ Repeat connection request? connected rcv group event chat item + + member has old version + member has old version + No comment provided by engineer. + message message @@ -9106,20 +10234,20 @@ Repeat connection request? moderated by %@ marked deleted chat item preview text + + moderator + moderator + member role + months months time unit - - mute - mute - No comment provided by engineer. - never never - No comment provided by engineer. + delete after time new message @@ -9136,11 +10264,21 @@ Repeat connection request? no e2e encryption No comment provided by engineer. + + no subscription + no subscription + No comment provided by engineer. + no text no text copied message info in history + + not synchronized + not synchronized + No comment provided by engineer. + observer observer @@ -9150,8 +10288,9 @@ Repeat connection request? off off enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9193,6 +10332,21 @@ Repeat connection request? peer-to-peer No comment provided by engineer. + + pending + pending + No comment provided by engineer. + + + pending approval + pending approval + No comment provided by engineer. + + + pending review + pending review + No comment provided by engineer. + quantum resistant e2e encryption quantum resistant e2e encryption @@ -9208,6 +10362,11 @@ Repeat connection request? received confirmation… No comment provided by engineer. + + rejected + rejected + No comment provided by engineer. + rejected call rejected call @@ -9228,6 +10387,11 @@ Repeat connection request? removed contact address profile update event chat item + + removed from group + removed from group + No comment provided by engineer. + removed profile picture removed profile picture @@ -9238,11 +10402,41 @@ Repeat connection request? removed you rcv group event chat item + + request is sent + request is sent + No comment provided by engineer. + + + request to join rejected + request to join rejected + No comment provided by engineer. + + + requested connection + requested connection + rcv group event chat item + + + requested connection from group %@ + requested connection from group %@ + rcv direct event chat item + requested to connect requested to connect chat list item title + + review + review + No comment provided by engineer. + + + reviewed by admins + reviewed by admins + No comment provided by engineer. + saved saved @@ -9278,11 +10472,6 @@ Repeat connection request? security code changed chat item text - - send direct message - send direct message - No comment provided by engineer. - server queue info: %1$@ @@ -9342,11 +10531,6 @@ last received msg: %2$@ unknown status No comment provided by engineer. - - unmute - unmute - No comment provided by engineer. - unprotected unprotected @@ -9437,10 +10621,10 @@ last received msg: %2$@ you No comment provided by engineer. - - you are invited to group - you are invited to group - No comment provided by engineer. + + you accepted this member + you accepted this member + snd group event chat item you are observer @@ -9511,7 +10695,7 @@ last received msg: %2$@
- +
@@ -9548,7 +10732,7 @@ last received msg: %2$@
- +
@@ -9570,7 +10754,7 @@ last received msg: %2$@
- +
@@ -9578,6 +10762,11 @@ last received msg: %2$@ %d new events notification body + + From %d chat(s) + From %d chat(s) + notification body + From: %@ From: %@ @@ -9593,16 +10782,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - New messages in %d chats - notification body -
- +
@@ -9624,7 +10808,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/en.xcloc/contents.json b/apps/ios/SimpleX Localizations/en.xcloc/contents.json index 2f39a1f1ee..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 08522cc617..30c69af755 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (puede copiarse) @@ -202,6 +190,11 @@ %d segundo(s) time interval + + %d seconds(s) + %d segundos + delete after time + %d skipped message(s) %d mensaje(s) omitido(s) @@ -272,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 @@ -327,11 +315,6 @@ %u mensaje(s) omitido(s). No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nuevo) @@ -342,11 +325,6 @@ (este dispositivo v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Añadir contacto**: crea un enlace de invitación nuevo. @@ -374,7 +352,7 @@ **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. @@ -412,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). @@ -453,11 +426,6 @@ - historial de edición. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 seg @@ -471,7 +439,8 @@ 1 day un dia - time interval + delete after time +time interval 1 hour @@ -486,12 +455,19 @@ 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 @@ -518,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> @@ -554,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. @@ -591,8 +562,19 @@ Accept Aceptar accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Aceptar como miembro + alert action + + + Accept as observer + Aceptar como observador + alert action Accept conditions @@ -604,6 +586,11 @@ ¿Aceptar solicitud de conexión? No comment provided by engineer. + + Accept contact request + Aceptar petición de contacto + alert title + Accept contact request from %@? ¿Aceptar solicitud de contacto de %@? @@ -612,8 +599,13 @@ Accept incognito Aceptar incógnito - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Aceptar miembro + alert title Accepted conditions @@ -630,6 +622,11 @@ Errores de confirmación No comment provided by engineer. + + Active + Activo + token status text + Active connections Conexiones activas @@ -645,6 +642,16 @@ Añadir amigos No comment provided by engineer. + + Add list + Añadir lista + No comment provided by engineer. + + + Add message + Añadir mensaje + placeholder for sending contact request + Add profile Añadir perfil @@ -670,6 +677,11 @@ 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 @@ -677,7 +689,7 @@ Add your team members to the conversations. - Añade a los miembros de tu equipo a las conversaciones. + Añade a miembros de tu equipo a las conversaciones. No comment provided by engineer. @@ -722,7 +734,7 @@ Address settings - Configuración de dirección + Configurar dirección No comment provided by engineer. @@ -745,6 +757,11 @@ 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. @@ -752,9 +769,14 @@ 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. @@ -777,12 +799,12 @@ 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. @@ -795,6 +817,16 @@ 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. @@ -827,7 +859,7 @@ Allow disappearing messages only if your contact allows it to you. - Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí. + Se permiten los mensajes temporales pero sólo si tu contacto también los permite. No comment provided by engineer. @@ -835,9 +867,14 @@ Permitir versión anterior No comment provided by engineer. + + Allow files and media only if your contact allows them. + Se permiten archivos y multimedia pero sólo si tu contacto también los permite. + 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) + Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas) No comment provided by engineer. @@ -870,6 +907,11 @@ 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. Se permite enviar enlaces SimpleX. @@ -897,27 +939,32 @@ Allow your contacts adding message reactions. - Permitir que tus contactos añadan reacciones a los mensajes. + Permites que tus contactos añadan reacciones a los mensajes. No comment provided by engineer. Allow your contacts to call you. - Permites que tus contactos puedan llamarte. + Permites que tus contactos te llamen. No comment provided by engineer. Allow your contacts to irreversibly delete sent messages. (24 hours) - Permites a tus contactos eliminar irreversiblemente los mensajes enviados. (24 horas) + Permites que tus contactos eliminan irreversiblemente los mensajes enviados. (24 horas) No comment provided by engineer. Allow your contacts to send disappearing messages. - Permites a tus contactos enviar mensajes temporales. + Permites que tus contactos envien mensajes temporales. + No comment provided by engineer. + + + Allow your contacts to send files and media. + Permes que tus contactos envíen archivos y multimedia. No comment provided by engineer. Allow your contacts to send voice messages. - Permites a tus contactos enviar mensajes de voz. + Permites que tus contactos envien mensajes de voz. No comment provided by engineer. @@ -928,12 +975,12 @@ Already connecting! ¡Ya en proceso de conexión! - No comment provided by engineer. + new chat sheet title Already joining the group! ¡Ya en proceso de unirte al grupo! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -950,6 +997,11 @@ 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 @@ -975,9 +1027,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. @@ -992,7 +1049,7 @@ App session - Sesión de aplicación + por sesión No comment provided by engineer. @@ -1020,6 +1077,21 @@ 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 @@ -1030,6 +1102,21 @@ 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 @@ -1047,7 +1134,7 @@ Audio & video calls - Llamadas y videollamadas + Llamadas y Videollamadas No comment provided by engineer. @@ -1100,11 +1187,6 @@ Aceptar imágenes automáticamente No comment provided by engineer. - - Auto-accept settings - Auto aceptar configuración - alert title - Back Volver @@ -1140,6 +1222,11 @@ 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. @@ -1160,6 +1247,11 @@ Notificaciones mejoradas No comment provided by engineer. + + Better privacy and security + Privacidad y seguridad mejoradas + No comment provided by engineer. + Better security ✅ Seguridad mejorada ✅ @@ -1170,6 +1262,16 @@ Experiencia de usuario mejorada No comment provided by engineer. + + Bio + Biografía + No comment provided by engineer. + + + Bio too large + Biografía demasiado larga + alert title + Black Negro @@ -1220,6 +1322,11 @@ Difuminar multimedia No comment provided by engineer. + + Bot + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Tanto tú como tu contacto podéis añadir reacciones a los mensajes. @@ -1240,6 +1347,11 @@ Tanto tú como tu contacto podéis enviar mensajes temporales. No comment provided by engineer. + + Both you and your contact can send files and media. + Tanto tú como tu contacto podéis enviar archivos y multimedia. + No comment provided by engineer. + Both you and your contact can send voice messages. Tanto tú como tu contacto podéis enviar mensajes de voz. @@ -1260,11 +1372,30 @@ Chats empresariales No comment provided by engineer. + + Business connection + Conexión empresarial + 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: +- send only legal content in public groups. +- respect other users – no spam. + Al usar SimpleX Chat, aceptas: +- enviar únicamente contenido legal en los grupos públicos. +- respetar a los demás usuarios – spam prohibido. + No comment provided by engineer. + Call already ended! ¡La llamada ha terminado! @@ -1295,6 +1426,11 @@ No se puede llamar al miembro No comment provided by engineer. + + Can't change profile + No se puede cambiar el perfil + alert title + Can't invite contact! ¡No se puede invitar el contacto! @@ -1314,7 +1450,8 @@ Cancel Cancelar alert action - alert button +alert button +new chat action Cancel migration @@ -1351,6 +1488,11 @@ 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 @@ -1400,7 +1542,7 @@ Change self-destruct passcode Cambiar código autodestrucción authentication reason - set passcode view +set passcode view Chat @@ -1415,7 +1557,7 @@ Chat already exists! ¡El chat ya existe! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1494,12 +1636,27 @@ Chat will be deleted for all members - this cannot be undone! - El chat será eliminado para todos los miembros. ¡No podrá deshacerse! + 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 podrá deshacerse! + El chat será eliminado para tí. ¡No puede deshacerse! + No comment provided by engineer. + + + Chat with admins + Chatea con administradores + chat toolbar + + + Chat with member + Chat con miembro + No comment provided by engineer. + + + Chat with members before they join. + Chatea con el miembro antes de unirse. No comment provided by engineer. @@ -1507,6 +1664,11 @@ Chats No comment provided by engineer. + + Chats with members + Chat con miembros + No comment provided by engineer. + Check messages every 20 min. Comprobar mensajes cada 20 min. @@ -1534,7 +1696,7 @@ Choose file - Elije archivo + Elegir archivo No comment provided by engineer. @@ -1572,9 +1734,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. @@ -1592,6 +1764,11 @@ 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 @@ -1625,17 +1802,7 @@ Conditions of use Condiciones de uso - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - Las condiciones de los operadores habilitados serán aceptadas después de 30 días. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Las condiciones serán aceptadas para el/los operador(es): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1657,6 +1824,11 @@ Configure servidores ICE No comment provided by engineer. + + Configure server operators + Configurar operadores de servidores + No comment provided by engineer. + Confirm Confirmar @@ -1707,6 +1879,11 @@ Confirmar subida No comment provided by engineer. + + Confirmed + Confirmado + token status text + Connect Conectar @@ -1717,9 +1894,9 @@ Conectar automáticamente No comment provided by engineer. - - Connect incognito - Conectar incognito + + Connect faster! 🚀 + ¡Conéctate más rápido! 🚀 No comment provided by engineer. @@ -1729,12 +1906,7 @@ 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? + Conéctate más rápido con tus amigos. No comment provided by engineer. @@ -1742,34 +1914,34 @@ This is your own SimpleX address! ¿Conectarte a tí mismo? ¡Esta es tu propia dirección SimpleX! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! ¿Conectarte a tí mismo? ¡Este es tu propio enlace de un solo uso! - No comment provided by engineer. + new chat sheet title Connect via contact address Conectar mediante dirección de contacto - No comment provided by engineer. + new chat sheet title Connect via link Conectar mediante enlace - No comment provided by engineer. + new chat sheet title Connect via one-time link Conectar mediante enlace de un sólo uso - No comment provided by engineer. + new chat sheet title Connect with %@ Conectar con %@ - No comment provided by engineer. + new chat action Connected @@ -1826,16 +1998,33 @@ This is your own one-time link! 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. + alert title Connection error (AUTH) 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 @@ -1846,6 +2035,11 @@ 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 @@ -1859,7 +2053,7 @@ This is your own one-time link! Connection timeout Tiempo de conexión agotado - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1911,9 +2105,14 @@ This is your own one-time link! Preferencias de contacto No comment provided by engineer. + + Contact requests from groups + Solicitudes de contacto en grupo + No comment provided by engineer. + Contact will be deleted - this cannot be undone! - El contacto será eliminado. ¡No podrá deshacerse! + El contacto será eliminado. ¡No puede deshacerse! No comment provided by engineer. @@ -1926,6 +2125,11 @@ 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 @@ -1968,7 +2172,7 @@ This is your own one-time link! Create 1-time link - Crear enlace de un uso + Crear enlace de un solo uso No comment provided by engineer. @@ -2001,6 +2205,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/). 💻 @@ -2016,9 +2225,9 @@ This is your own one-time link! Crear cola server test step - - Create secret group - Crea grupo secreto + + Create your address + Crea tu dirección No comment provided by engineer. @@ -2218,8 +2427,7 @@ This is your own one-time link! Delete Eliminar alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2261,6 +2469,11 @@ This is your own one-time link! Eliminar chat No comment provided by engineer. + + Delete chat messages from your device. + Elimina los mensajes del dispositivo. + No comment provided by engineer. + Delete chat profile Eliminar perfil @@ -2271,6 +2484,11 @@ This is your own one-time link! ¿Eliminar perfil? No comment provided by engineer. + + Delete chat with member? + ¿Eliminar chat con miembro? + alert title + Delete chat? ¿Eliminar chat? @@ -2351,6 +2569,11 @@ This is your own one-time link! ¿Eliminar enlace? No comment provided by engineer. + + Delete list? + ¿Eliminar lista? + alert title + Delete member message? ¿Eliminar el mensaje de miembro? @@ -2363,8 +2586,8 @@ This is your own one-time link! Delete messages - Eliminar mensaje - No comment provided by engineer. + Activar + alert button Delete messages after @@ -2383,7 +2606,7 @@ This is your own one-time link! Delete or moderate up to 200 messages. - Borra o modera hasta 200 mensajes a la vez. + Elimina o modera hasta 200 mensajes a la vez. No comment provided by engineer. @@ -2401,6 +2624,11 @@ This is your own one-time link! 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. @@ -2456,11 +2684,21 @@ This is your own one-time link! ¡Confirmación de entrega! No comment provided by engineer. + + Deprecated options + Opciones obsoletas + No comment provided by engineer. + Description Descripción No comment provided by engineer. + + Description too large + Descripción demasiado larga + alert title + Desktop address Dirección ordenador @@ -2561,6 +2799,16 @@ This is your own one-time link! 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 @@ -2648,7 +2896,12 @@ This is your own one-time link! Do not use credentials with proxy. - No uses credenciales con proxy. + No se usan credenciales con proxy. + No comment provided by engineer. + + + Documents: + Documentos: No comment provided by engineer. @@ -2661,9 +2914,19 @@ This is your own one-time link! 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 volver a mostrar + alert action + + + Done + Hecho No comment provided by engineer. @@ -2675,7 +2938,7 @@ This is your own one-time link! Download Descargar alert button - chat item action +chat item action Download errors @@ -2742,6 +3005,11 @@ This is your own one-time link! Editar perfil de grupo No comment provided by engineer. + + Empty message! + ¡Mensaje vacío! + No comment provided by engineer. + Enable Activar @@ -2752,9 +3020,9 @@ This is your own one-time link! Activar (conservar anulaciones) No comment provided by engineer. - - Enable Flux - Habilita Flux + + 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. @@ -2770,13 +3038,18 @@ This is your own one-time link! Enable automatic message deletion? ¿Activar eliminación automática de mensajes? - No comment provided by engineer. + alert title Enable camera access Permitir acceso a la cámara No comment provided by engineer. + + Enable disappearing messages by default. + Activa por defecto los mensajes temporales. + No comment provided by engineer. + Enable for all Activar para todos @@ -2789,7 +3062,7 @@ This is your own one-time link! Enable instant notifications? - ¿Activar notificación instantánea? + ¿Activar notificaciones instantáneas? No comment provided by engineer. @@ -2839,7 +3112,7 @@ This is your own one-time link! Encrypt local files - Cifra archivos locales + Cifrar archivos locales No comment provided by engineer. @@ -2897,6 +3170,11 @@ This is your own one-time link! 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 @@ -2929,7 +3207,7 @@ This is your own one-time link! Enter server manually - Introduce el servidor manualmente + Añadir manualmente No comment provided by engineer. @@ -2939,12 +3217,12 @@ This is your own one-time link! Enter welcome message… - Introduce mensaje de bienvenida… + Deja un mensaje de bienvenida… placeholder Enter welcome message… (optional) - Introduce mensaje de bienvenida… (opcional) + Deja un mensaje de bienvenida… (opcional) placeholder @@ -2972,6 +3250,11 @@ This is your own one-time link! Error al aceptar solicitud del contacto No comment provided by engineer. + + Error accepting member + Error al aceptar el miembro + alert title + Error adding member(s) Error al añadir miembro(s) @@ -2982,11 +3265,21 @@ This is your own one-time link! Error al añadir servidor alert title + + Error adding short link + Error al añadir enlace corto + No comment provided by engineer. + Error changing address Error al cambiar servidor No comment provided by engineer. + + Error changing chat profile + Error al cambiar perfil de chat + alert title + Error changing connection profile Error al cambiar el perfil de conexión @@ -3000,17 +3293,27 @@ This is your own one-time link! Error changing setting Error cambiando configuración - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + Error al conectar con el servidor usado para recibir mensajes de esta conexión: %@ + subscription status explanation Error creating address @@ -3027,6 +3330,11 @@ This is your own one-time link! 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 @@ -3042,20 +3350,30 @@ This is your own one-time link! ¡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 No comment provided by engineer. + + Error deleting chat + Error al eliminar el chat + alert title + Error deleting chat database Error al eliminar base de datos - No comment provided by engineer. + alert title Error deleting chat! ¡Error al eliminar chat! - No comment provided by engineer. + alert title Error deleting connection @@ -3065,12 +3383,12 @@ This is your own one-time link! Error deleting database Error al eliminar base de datos - No comment provided by engineer. + alert title Error deleting old database Error al eliminar base de datos antigua - No comment provided by engineer. + alert title Error deleting token @@ -3105,7 +3423,7 @@ This is your own one-time link! Error exporting chat database Error al exportar base de datos - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3115,7 +3433,7 @@ This is your own one-time link! Error importing chat database Error al importar base de datos - No comment provided by engineer. + alert title Error joining group @@ -3137,6 +3455,11 @@ This is your own one-time link! Error al abrir chat No comment provided by engineer. + + Error opening group + Error al abrir el grupo + No comment provided by engineer. + Error receiving file Error al recibir archivo @@ -3152,10 +3475,25 @@ This is your own one-time link! Error al reconectar con los servidores No comment provided by engineer. + + Error registering for notifications + Error al registrarse para notificaciones + alert title + + + Error rejecting contact request + Error al rechazar la solicitud del contacto + alert title + Error removing member - Error al eliminar miembro - No comment provided by engineer. + Error al expulsar miembro + alert title + + + Error reordering lists + Error al reorganizar listas + alert title Error resetting statistics @@ -3167,6 +3505,11 @@ This is your own one-time link! 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 @@ -3217,6 +3560,11 @@ This is your own one-time link! Error al enviar mensaje No comment provided by engineer. + + Error setting auto-accept + Error al configurar auto aceptar + No comment provided by engineer. + Error setting delivery receipts! ¡Error al configurar confirmaciones de entrega! @@ -3235,7 +3583,7 @@ This is your own one-time link! Error switching profile Error al cambiar perfil - No comment provided by engineer. + alert title Error switching profile! @@ -3247,6 +3595,11 @@ This is your own one-time link! 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 @@ -3290,7 +3643,14 @@ This is your own one-time link! Error: %@ Error: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + Error: %@. + server test error Error: URL is invalid @@ -3327,6 +3687,11 @@ This is your own one-time link! Expandir chat item action + + Expired + Expirado + token status text + Export database Exportar base de datos @@ -3367,20 +3732,35 @@ This is your own one-time link! ¡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 - No comment provided by engineer. + file error alert title File errors: @@ -3389,9 +3769,16 @@ This is your own one-time link! %@ 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 borrado o cancelado. + Archivo no encontrado, probablemente haya sido eliminado o cancelado. file error text @@ -3444,6 +3831,11 @@ This is your own one-time link! Archivos y multimedia chat feature + + Files and media are prohibited in this chat. + Los archivos y multimedia no están permitidos en este chat. + No comment provided by engineer. + Files and media are prohibited. Los archivos y multimedia no están permitidos en este grupo. @@ -3484,6 +3876,26 @@ This is your own one-time link! Encuentra chats mas rápido No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + La huella en la dirección del servidor de destino no coincide con el certificado: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + La huella en la dirección del servidor de reenvío no coincide con el certificado: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + La huella en la dirección del servidor no coincide con el certificado. + server test error + + + Fingerprint in server address does not match certificate: %@. + La huella en la dirección del servidor no coincide con el certificado: %@. + No comment provided by engineer. + Fix Reparar @@ -3514,6 +3926,11 @@ This is your own one-time link! 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 %@: @@ -3529,9 +3946,14 @@ This is your own one-time link! 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 el enrutamiento privado + Para enrutamiento privado No comment provided by engineer. @@ -3585,9 +4007,9 @@ This is your own one-time link! 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 %1$@ failed to connect to destination server %2$@. Please try later. + El servidor de reenvío %1$@ no ha podido conectarse al servidor de destino %2$@. Por favor, intentalo más tarde. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3653,6 +4075,11 @@ Error: %2$@ 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! @@ -3676,7 +4103,7 @@ Error: %2$@ Group already exists! ¡El grupo ya existe! - No comment provided by engineer. + new chat sheet title Group display name @@ -3743,6 +4170,11 @@ Error: %2$@ El perfil de grupo se almacena en los dispositivos, no en los servidores. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + El perfil del grupo ha cambiado. Si lo guardas, el perfil actualizado se enviará a los miembros del grupo. + alert message + Group welcome message Mensaje de bienvenida en grupos @@ -3750,12 +4182,17 @@ Error: %2$@ 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. @@ -3763,6 +4200,11 @@ Error: %2$@ 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 @@ -3795,7 +4237,7 @@ Error: %2$@ Hide: - Ocultar: + Oculta: No comment provided by engineer. @@ -3823,6 +4265,11 @@ Error: %2$@ Cómo ayuda a la privacidad No comment provided by engineer. + + How it works + Cómo funciona + alert button + How to Cómo @@ -3965,6 +4412,16 @@ More improvements are coming soon! Sonido de llamada No comment provided by engineer. + + Inappropriate content + Contenido inapropiado + report reason + + + Inappropriate profile + Perfil inapropiado + report reason + Incognito Incógnito @@ -4057,6 +4514,31 @@ More improvements are coming soon! 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 @@ -4075,7 +4557,7 @@ More improvements are coming soon! Invalid link Enlace no válido - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4185,32 +4667,27 @@ More improvements are coming soon! Join - Unirte + Unirme swipe action + + Join as %@ + Unirme como %@ + No comment provided by engineer. + Join group - Unirte al grupo - No comment provided by engineer. + Unirme al grupo + new chat sheet title Join group conversations Unirse a la conversación del grupo No comment provided by engineer. - - Join group? - ¿Unirte al grupo? - No comment provided by engineer. - Join incognito - Unirte en modo incógnito - No comment provided by engineer. - - - Join with current profile - Unirte con el perfil actual + Unirme en modo incógnito No comment provided by engineer. @@ -4218,7 +4695,7 @@ More improvements are coming soon! This is your link for group %@! ¿Unirse a tu grupo? ¡Este es tu enlace para el grupo %@! - No comment provided by engineer. + new chat action Joining group @@ -4242,9 +4719,14 @@ This is your link for group %@! Keep unused invitation? - ¿Guardar invitación no usada? + ¿Guardar enlace no usado? alert title + + Keep your chats clean + Mantén los chats limpios + No comment provided by engineer. + Keep your connections Conserva tus conexiones @@ -4300,6 +4782,11 @@ This is your link for group %@! ¿Salir del grupo? No comment provided by engineer. + + Less traffic on mobile networks. + Menos tráfico en redes móviles. + No comment provided by engineer. + Let's talk in SimpleX Chat Hablemos en SimpleX Chat @@ -4330,6 +4817,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! @@ -4340,6 +4842,11 @@ This is your link for group %@! Mensajes en vivo No comment provided by engineer. + + Loading profile… + Cargando perfil. . . + in progress text + Local name Nombre local @@ -4415,14 +4922,34 @@ This is your link for group %@! Miembro No comment provided by engineer. + + Member %@ + Miembro %@ + past/unknown group member + + + Member admission + Admisión de miembros + No comment provided by engineer. + Member inactive Miembro inactivo item status text + + Member is deleted - can't accept request + Miembro eliminado, no puede aceptar solicitudes + No comment provided by engineer. + + + 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. + El rol del miembro cambiará a "%@". Se notificará en el chat. No comment provided by engineer. @@ -4437,14 +4964,19 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! - El miembro será eliminado del chat. ¡No podrá deshacerse! + 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. + + Member will join the group, accept member? + El miembro se unirá al grupo, ¿aceptas al miembro? + alert message + Members can add message reactions. Los miembros pueden añadir reacciones a los mensajes. @@ -4455,6 +4987,11 @@ This is your link for group %@! 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. @@ -4480,6 +5017,11 @@ This is your link for group %@! 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 @@ -4510,6 +5052,11 @@ This is your link for group %@! Mensaje reenviado item status text + + Message instantly once you tap Connect. + Tras pulsar Contactar, mensajea ya. + No comment provided by engineer. + 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. @@ -4585,11 +5132,21 @@ This is your link for group %@! Mensajes No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + Los mensajes están protegidos mediante **cifrado de extremo a extremo**. + No comment provided by engineer. + Messages from %@ will be shown! - ¡Los mensajes de %@ serán mostrados! + ¡Los mensajes nuevos 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 @@ -4602,7 +5159,7 @@ This is your link for group %@! Messages were deleted after you selected them. - Los mensajes han sido borrados después de seleccionarlos. + Los mensajes han sido eliminados después de seleccionarlos. alert message @@ -4690,6 +5247,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! @@ -4718,7 +5280,12 @@ This is your link for group %@! Mute Silenciar - swipe action + notification label action + + + Mute all + Silenciar todo + notification label action Muted when inactive! @@ -4768,7 +5335,12 @@ This is your link for group %@! Network status Estado de la red - No comment provided by engineer. + alert title + + + New + Nuevo + token status text New Passcode @@ -4782,7 +5354,7 @@ This is your link for group %@! New SOCKS credentials will be used for each server. - Se usarán credenciales SOCKS nuevas por cada servidor. + Se usarán credenciales SOCKS nuevas para cada servidor. No comment provided by engineer. @@ -4820,6 +5392,11 @@ This is your link for group %@! Eventos nuevos notification + + New group role: Moderator + Nuevo rol de grupo: Moderador + No comment provided by engineer. + New in %@ Nuevo en %@ @@ -4835,6 +5412,11 @@ This is your link for group %@! Nuevo rol de miembro No comment provided by engineer. + + New member wants to join the group. + Un miembro nuevo desea unirse al grupo. + rcv group event chat item + New message Mensaje nuevo @@ -4860,6 +5442,26 @@ 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 chats with members + Sin chats + No comment provided by engineer. + No contacts selected Ningún contacto seleccionado @@ -4907,12 +5509,17 @@ This is your link for group %@! No media & file servers. - Ningún servidor de archivos y multimedia. + Sin servidores para archivos y multimedia. servers error + + No message + Ningún mensaje + No comment provided by engineer. + No message servers. - Ningún servidor de mensajes. + Sin servidores para mensajes. servers error @@ -4935,9 +5542,14 @@ This is your link for group %@! Sin permiso para grabar mensajes de voz No comment provided by engineer. + + No private routing session + Ninguna sesión con enrutamiento privado + alert title + No push server - Ningún servidor push + Sin servidores push No comment provided by engineer. @@ -4947,24 +5559,34 @@ This is your link for group %@! No servers for private message routing. - Ningún servidor para enrutamiento privado. + Sin servidores para enrutamiento privado. servers error No servers to receive files. - Ningún servidor para recibir archivos. + Sin servidores para recibir archivos. servers error No servers to receive messages. - Ningún servidor para recibir mensajes. + Sin servidores para recibir mensajes. servers error No servers to send files. - Ningún servidor para enviar archivos. + 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. @@ -4975,6 +5597,11 @@ This is your link for group %@! ¡No compatible! No comment provided by engineer. + + Notes + Notas + No comment provided by engineer. + Nothing selected Nada seleccionado @@ -4995,11 +5622,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. @@ -5022,7 +5659,9 @@ This is your link for group %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5065,7 +5704,7 @@ Requiere activación de la VPN. Only delete conversation - Sólo borrar la conversación + Eliminar sólo la conversación No comment provided by engineer. @@ -5083,6 +5722,16 @@ Requiere activación de la VPN. 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. @@ -5103,6 +5752,11 @@ Requiere activación de la VPN. Sólo tú puedes enviar mensajes temporales. No comment provided by engineer. + + Only you can send files and media. + Sólo tú puedes enviar archivos y multimedia. + No comment provided by engineer. + Only you can send voice messages. Sólo tú puedes enviar mensajes de voz. @@ -5128,6 +5782,11 @@ Requiere activación de la VPN. Sólo tu contacto puede enviar mensajes temporales. No comment provided by engineer. + + Only your contact can send files and media. + Sólo tu contacto puede enviar archivos y multimedia. + No comment provided by engineer. + Only your contact can send voice messages. Sólo tu contacto puede enviar mensajes de voz. @@ -5136,7 +5795,7 @@ Requiere activación de la VPN. Open Abrir - No comment provided by engineer. + alert action Open Settings @@ -5151,28 +5810,73 @@ Requiere activación de la VPN. Open chat Abrir chat - No comment provided by engineer. + new chat action Open chat console Abrir consola de Chat authentication reason + + Open clean link + Abrir enlace limpio + alert action + Open conditions Abrir condiciones No comment provided by engineer. + + Open full link + Abrir enlace completo + alert action + Open group Grupo abierto - No comment provided by engineer. + new chat action + + + Open link? + ¿Abrir enlace? + alert title Open migration to another device Abrir menú migración a otro dispositivo authentication reason + + Open new chat + Abrir chat nuevo + new chat action + + + Open new group + Abrir grupo nuevo + new chat action + + + Open to accept + Abrir para aceptar + No comment provided by engineer. + + + Open to connect + Abrir para conectar + No comment provided by engineer. + + + Open to join + Abre para unirte + No comment provided by engineer. + + + Open to use bot + Abre para usar el bot + No comment provided by engineer. + Opening app… Iniciando aplicación… @@ -5210,7 +5914,7 @@ Requiere activación de la VPN. Or show this code - O muestra este código QR + O muestra el código QR No comment provided by engineer. @@ -5218,6 +5922,11 @@ Requiere activación de la VPN. 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 Otro @@ -5275,11 +5984,6 @@ Requiere activación de la VPN. Contraseña para hacerlo visible No comment provided by engineer. - - Past member %@ - Miembro pasado %@ - past/unknown group member - Paste desktop address Pegar dirección de ordenador @@ -5350,7 +6054,7 @@ Por favor, comparte cualquier otro problema con los desarrolladores. Please check your network connection with %@ and try again. Comprueba tu conexión de red con %@ e inténtalo de nuevo. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5386,7 +6090,7 @@ Error: %@ Please remember or store it securely - there is no way to recover a lost passcode! - Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay forma de recuperar un código perdido! + Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay manera de recuperar un código perdido! No comment provided by engineer. @@ -5401,14 +6105,34 @@ Error: %@ Please store passphrase securely, you will NOT be able to access chat if you lose it. - Guarda la contraseña de forma segura, NO podrás acceder al chat si la pierdes. + Guarda la contraseña de forma segura, NO podrás acceder al chat si se pierde. 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. + Guarda la contraseña de forma segura, NO podrás cambiarla si se pierde. 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 group moderators to review your request to join the group. + Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo. + snd group event chat item + + + 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 @@ -5419,11 +6143,6 @@ Error: %@ Puerto No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Posiblemente la huella del certificado en la dirección del servidor es incorrecta - server test error - Preserve the last message draft, with attachments. Conserva el último borrador del mensaje con los datos adjuntos. @@ -5459,16 +6178,31 @@ Error: %@ 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 @@ -5492,7 +6226,12 @@ Error: %@ Private routing error Error de enrutamiento privado - No comment provided by engineer. + alert title + + + Private routing timeout + Timeout enrutamiento privado + alert title Profile and server connections @@ -5544,6 +6283,11 @@ 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 se permite enviar enlaces SimpleX. @@ -5576,7 +6320,7 @@ Error: %@ Protect app screen - Proteger la pantalla de la aplicación + Proteger la pantalla No comment provided by engineer. @@ -5591,6 +6335,11 @@ Actívalo en ajustes de *Servidores y Redes*. ¡Protege tus perfiles con contraseña! No comment provided by engineer. + + Protocol background timeout + Timeout protocolo en segundo plano + No comment provided by engineer. + Protocol timeout Timeout protocolo @@ -5638,7 +6387,7 @@ Actívalo en ajustes de *Servidores y Redes*. Reachable chat toolbar - Barra de herramientas accesible + Barra de menú accesible No comment provided by engineer. @@ -5696,11 +6445,6 @@ Actívalo en ajustes de *Servidores y Redes*. Recibido: %@ copied message info - - Received file event - Evento de archivo recibido - notification - Received message Mensaje entrante @@ -5773,7 +6517,7 @@ Actívalo en ajustes de *Servidores y Redes*. Reconnect server to force message delivery. It uses additional traffic. - Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional. + Reconectar con el servidor para forzar la entrega de mensajes. Se usa tráfico adicional. No comment provided by engineer. @@ -5801,11 +6545,27 @@ Actívalo en ajustes de *Servidores y Redes*. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5815,7 +6575,12 @@ Actívalo en ajustes de *Servidores y Redes*. Reject contact request Rechazar solicitud de contacto - No comment provided by engineer. + alert title + + + Reject member? + ¿Rechazar al miembro? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5842,6 +6607,11 @@ Actívalo en ajustes de *Servidores y Redes*. Eliminar imagen No comment provided by engineer. + + Remove link tracking + Limpiar enlaces de seguimiento + No comment provided by engineer. + Remove member Expulsar miembro @@ -5857,6 +6627,11 @@ Actívalo en ajustes de *Servidores y Redes*. ¿Eliminar contraseña de Keychain? No comment provided by engineer. + + Removes messages and blocks members. + Elimina mensajes y bloquea miembros. + No comment provided by engineer. + Renegotiate Renegociar @@ -5872,11 +6647,6 @@ Actívalo en ajustes de *Servidores y Redes*. ¿Renegociar cifrado? No comment provided by engineer. - - Repeat connection request? - ¿Repetir solicitud de conexión? - No comment provided by engineer. - Repeat download Repetir descarga @@ -5887,11 +6657,6 @@ Actívalo en ajustes de *Servidores y Redes*. Repetir importación No comment provided by engineer. - - Repeat join request? - ¿Repetir solicitud de admisión? - No comment provided by engineer. - Repeat upload Repetir subida @@ -5902,6 +6667,61 @@ Actívalo en ajustes de *Servidores y Redes*. 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 sent to moderators + Informe enviado a los moderadores + alert title + + + 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 @@ -5980,7 +6800,7 @@ Actívalo en ajustes de *Servidores y Redes*. Retry Reintentar - No comment provided by engineer. + alert action Reveal @@ -5992,11 +6812,21 @@ Actívalo en ajustes de *Servidores y Redes*. Revisar condiciones No comment provided by engineer. - - Review later - Revisar más tarde + + Review group members + Revisa los miembros del grupo No comment provided by engineer. + + Review members + Revisar miembros + admission stage + + + Review members before admitting ("knocking"). + Revisa a los miembros antes de admitirles en el grupo. + admission stage description + Revoke Revocar @@ -6046,13 +6876,23 @@ Actívalo en ajustes de *Servidores y Redes*. Save Guardar alert button - chat item action +chat item action Save (and notify contacts) Guardar (y notificar contactos) alert button + + Save (and notify members) + Guardar (y notificar miembros) + alert button + + + Save admission settings? + ¿Guardar configuración? + alert title + Save and notify contact Guardar y notificar contacto @@ -6078,6 +6918,16 @@ Actívalo en ajustes de *Servidores y Redes*. Guardar perfil de grupo No comment provided by engineer. + + Save group profile? + ¿Guardar perfil del grupo? + alert title + + + Save list + Guardar lista + No comment provided by engineer. + Save passphrase and open chat Guardar contraseña y abrir el chat @@ -6175,7 +7025,7 @@ Actívalo en ajustes de *Servidores y Redes*. Scan server QR code - Escanear código QR del servidor + Escanear código QR No comment provided by engineer. @@ -6268,6 +7118,11 @@ Actívalo en ajustes de *Servidores y Redes*. Envía un mensaje en vivo: se actualizará para el (los) destinatario(s) a medida que se escribe No comment provided by engineer. + + Send contact request? + ¿Enviar solicitud de contacto? + No comment provided by engineer. + Send delivery receipts to Enviar confirmaciones de entrega a @@ -6318,6 +7173,11 @@ Actívalo en ajustes de *Servidores y Redes*. Enviar notificaciones No comment provided by engineer. + + Send private reports + Envía informes privados + No comment provided by engineer. + Send questions and ideas Consultas y sugerencias @@ -6328,6 +7188,16 @@ Actívalo en ajustes de *Servidores y Redes*. Enviar confirmaciones No comment provided by engineer. + + Send request + Enviar solicitud + No comment provided by engineer. + + + Send request without message + Enviar solicitud sin mensaje + No comment provided by engineer. + Send them from gallery or custom keyboards. Envíalos desde la galería o desde teclados personalizados. @@ -6338,6 +7208,11 @@ Actívalo en ajustes de *Servidores y Redes*. Se envían hasta 100 mensajes más recientes a los miembros nuevos. No comment provided by engineer. + + Send your private feedback to groups. + Envía tu comentario privado a los grupos. + No comment provided by engineer. + Sender cancelled file transfer. El remitente ha cancelado la transferencia de archivos. @@ -6403,11 +7278,6 @@ Actívalo en ajustes de *Servidores y Redes*. Directamente No comment provided by engineer. - - Sent file event - Evento de archivo enviado - notification - Sent message Mensaje saliente @@ -6478,14 +7348,14 @@ Actívalo en ajustes de *Servidores y Redes*. 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 + + Server requires authorization to create queues, check password. + El servidor requiere autorización para crear colas, comprueba la contraseña. server test error - - Server requires authorization to upload, check password - El servidor requiere autorización para subir, comprueba la contraseña + + Server requires authorization to upload, check password. + El servidor requiere autorización para subir, comprueba la contraseña. server test error @@ -6520,7 +7390,7 @@ Actívalo en ajustes de *Servidores y Redes*. Servers statistics will be reset - this cannot be undone! - Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse! + Las estadísticas de los servidores serán restablecidas. ¡No puede deshacerse! No comment provided by engineer. @@ -6533,6 +7403,11 @@ Actívalo en ajustes de *Servidores y Redes*. 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… @@ -6553,6 +7428,16 @@ Actívalo en ajustes de *Servidores y Redes*. Úsalo en lugar de la autenticación del sistema. No comment provided by engineer. + + Set member admission + Admisión miembro + 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 @@ -6568,6 +7453,11 @@ Actívalo en ajustes de *Servidores y Redes*. Escribe la contraseña para exportar No comment provided by engineer. + + Set profile bio and welcome message. + Añade mensaje de bienvenida y biografía del perfil. + No comment provided by engineer. + Set the message shown to new members! ¡Guarda un mensaje para ser mostrado a los miembros nuevos! @@ -6597,7 +7487,7 @@ Actívalo en ajustes de *Servidores y Redes*. Share Compartir alert action - chat item action +chat item action Share 1-time link @@ -6639,9 +7529,19 @@ Actívalo en ajustes de *Servidores y Redes*. Compartir enlace No comment provided by engineer. + + Share old address + Compartir dirección antigua + alert button + + + Share old link + Comparte enlace completo + alert button + Share profile - Comparte perfil + Perfil a compartir No comment provided by engineer. @@ -6659,6 +7559,26 @@ Actívalo en ajustes de *Servidores y Redes*. Compartir con contactos No comment provided by engineer. + + Share your address + Comparte tu dirección + No comment provided by engineer. + + + Short SimpleX address + Direcciones SimpleX cortas + No comment provided by engineer. + + + Short description + Descripción corta + No comment provided by engineer. + + + Short link + Enlace corto + No comment provided by engineer. + Show QR code Mostrar código QR @@ -6701,7 +7621,7 @@ Actívalo en ajustes de *Servidores y Redes*. Show: - Mostrar: + Muestra: No comment provided by engineer. @@ -6716,7 +7636,7 @@ Actívalo en ajustes de *Servidores y Redes*. SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. - Simplex Chat y Flux han acordado incluir servidores operados por Flux en la aplicación + Simplex Chat y Flux han acordado incluir en la aplicación servidores operados por Flux. No comment provided by engineer. @@ -6751,14 +7671,24 @@ Actívalo en ajustes de *Servidores y Redes*. SimpleX address and 1-time links are safe to share via any messenger. - Compartir los enlaces de un uso y las direcciones SimpleX es seguro a través de cualquier medio. + 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? + ¿Dirección SimpleX o enlace de un solo uso? No comment provided by engineer. + + SimpleX address settings + Auto aceptar configuración + alert title + + + SimpleX channel link + Enlace de canal SimpleX + simplex link type + SimpleX contact address Dirección de contacto SimpleX @@ -6799,6 +7729,11 @@ Actívalo en ajustes de *Servidores y Redes*. Protocolos de SimpleX auditados por Trail of Bits. No comment provided by engineer. + + SimpleX relay link + Enlace de servidor SimpleX + simplex link type + Simplified incognito mode Modo incógnito simplificado @@ -6821,7 +7756,7 @@ Actívalo en ajustes de *Servidores y Redes*. Small groups (max 20) - Grupos pequeños (máx. 20) + Grupos pequeños (max. 20) No comment provided by engineer. @@ -6861,6 +7796,12 @@ Actívalo en ajustes de *Servidores y Redes*. Alguien notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Cuadrada, circular o cualquier forma intermedia. @@ -6908,7 +7849,7 @@ Actívalo en ajustes de *Servidores y Redes*. 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 SimpleX. 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. @@ -6946,6 +7887,11 @@ Actívalo en ajustes de *Servidores y Redes*. Parando chat No comment provided by engineer. + + Storage + Almacenamiento + No comment provided by engineer. + Strong Fuerte @@ -6958,7 +7904,7 @@ Actívalo en ajustes de *Servidores y Redes*. Subscribed - Suscrito + Suscritas No comment provided by engineer. @@ -7001,11 +7947,21 @@ Actívalo en ajustes de *Servidores y Redes*. Conexión TCP No comment provided by engineer. + + TCP connection bg timeout + Timeout conexión TCP sp + No comment provided by engineer. + TCP connection timeout Timeout de la conexión TCP No comment provided by engineer. + + TCP port for messaging + Puerto TCP para mensajes + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -7028,7 +7984,22 @@ Actívalo en ajustes de *Servidores y Redes*. Take picture - Tomar foto + Hacer foto + No comment provided by engineer. + + + Tap Connect to chat + Pulsa Conectar para chatear + No comment provided by engineer. + + + Tap Connect to send request + Pulsa Conectar para enviar solicitud + No comment provided by engineer. + + + Tap Connect to use bot + Pulsa Conectar para usar el bot No comment provided by engineer. @@ -7036,6 +8007,11 @@ Actívalo en ajustes de *Servidores y Redes*. Pulsa Crear dirección SimpleX en el menú para crearla más tarde. No comment provided by engineer. + + Tap Join group + Pulsa Unirme al grupo + No comment provided by engineer. + Tap button Pulsa el botón @@ -7063,7 +8039,7 @@ Actívalo en ajustes de *Servidores y Redes*. Tap to paste link - Pulsa para pegar el enlacePulsa para pegar enlace + Pulsa aquí para pegar el enlace No comment provided by engineer. @@ -7074,13 +8050,18 @@ Actívalo en ajustes de *Servidores y Redes*. Temporary file error Error en archivo temporal - No comment provided by engineer. + file error alert title Test failed at step %@. Prueba no superada en el paso %@. server test failure + + Test notifications + Probar notificaciones + No comment provided by engineer. + Test server Probar servidor @@ -7108,7 +8089,7 @@ Actívalo en ajustes de *Servidores y Redes*. Thanks to the users – contribute via Weblate! - ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate + ¡Agradecimiento a los colaboradores! Puedes contribuir a través de Weblate No comment provided by engineer. @@ -7118,6 +8099,11 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + La dirección pasará a ser corta y tu perfil será compartido mediante la dirección. + alert message + 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. @@ -7140,7 +8126,7 @@ 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. @@ -7178,6 +8164,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El hash del mensaje anterior es diferente. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + El enlace será corto y el perfil del grupo se compartirá mediante el enlace. + alert message + The message will be deleted for all members. El mensaje se eliminará para todos los miembros. @@ -7203,21 +8194,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. La base de datos antigua no se eliminó durante la migración, puede eliminarse. No comment provided by engineer. - - The profile is 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 same conditions will apply to operator(s): **%@**. - Las mismas condiciones se aplicarán a el/los operador(es) **%@**. - No comment provided by engineer. - The second preset operator in the app! ¡Segundo operador predefinido! @@ -7231,21 +8212,21 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The sender will NOT be notified El remitente NO será notificado - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. - Lista de servidores para las conexiones nuevas del perfil **%@**. + Servidores para conexiones nuevas en tu perfil **%@**. No comment provided by engineer. The servers for new files of your current chat profile **%@**. - Los servidores para archivos nuevos en tu perfil actual **%@**. + 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. @@ -7283,6 +8264,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. @@ -7305,7 +8291,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This display name is invalid. Please choose another name. - Éste nombre mostrado no es válido. Por favor, elije otro nombre. + Éste nombre mostrado no es válido. Por favor, elige otro nombre. No comment provided by engineer. @@ -7318,14 +8304,9 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Este grupo ya no existe. No comment provided by engineer. - - This is your own SimpleX address! - ¡Esta es tu propia dirección SimpleX! - No comment provided by engineer. - - - This is your own one-time link! - ¡Este es tu propio enlace de un solo uso! + + 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. @@ -7333,11 +8314,26 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. 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. + + This setting is for your current profile **%@**. + Esta configuración se aplica al perfil actual **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + Mensajes temporales activados sólo para los contactos nuevos. + No comment provided by engineer. + Title Título @@ -7387,7 +8383,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.
To protect your privacy, SimpleX uses separate IDs 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. + Para proteger tu privacidad, SimpleX usa identificadores distintos para cada uno de tus contactos. No comment provided by engineer. @@ -7420,14 +8416,24 @@ Se te pedirá que completes la autenticación antes de activar esta función.
Para enviar No comment provided by engineer. + + To send commands you must be connected. + Para enviar comandos debes estar conectado. + alert message + To support instant push notifications the chat database has to be migrated. Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + Para usar otro perfil tras el intento de conexión, elimina el chat y usa el enlace de nuevo. + alert message + To use the servers of **%@**, accept conditions of use. - Para usar los servidores de **%@**, acepta las condiciones de uso. + Para usar los servidores de **%@**, debes aceptar las condiciones de uso. No comment provided by engineer. @@ -7445,6 +8451,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.
Activa incógnito al conectar. No comment provided by engineer. + + Token status: %@. + Estado token: %@. + token status + Toolbar opacity Opacidad barra @@ -7465,15 +8476,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.
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: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Intentando conectar con el servidor usado para recibir mensajes de este contacto. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + Intentando conectar con el servidor usado para recibir mensajes de esta conexión. + subscription status explanation Turkish interface @@ -7512,7 +8518,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. Unblock member for all? - ¿Desbloquear miembro para todos? + ¿Desbloquear al miembro para todos? No comment provided by engineer. @@ -7583,7 +8589,7 @@ 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 el enlace haya sido usado, podría ser un error. Por favor, notifícalo. + 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. @@ -7610,13 +8616,18 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Unmute Activar audio - swipe action + notification label action Unread No leído swipe action + + 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. @@ -7642,16 +8653,51 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión ¿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. + Para actualizar la configuración el cliente se reconectará a todos los servidores. No comment provided by engineer. + + Upgrade + Actualizar + alert button + + + Upgrade address + Actualizar dirección + No comment provided by engineer. + + + Upgrade address? + ¿Actualizar la dirección? + alert message + Upgrade and open chat Actualizar y abrir Chat No comment provided by engineer. + + Upgrade group link? + ¿Actualizar enlace de grupo? + alert message + + + Upgrade link + Añadir enlace + No comment provided by engineer. + + + Upgrade your address + Actualiza tu dirección + No comment provided by engineer. + Upload errors Errores en subida @@ -7702,6 +8748,16 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión ¿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 @@ -7710,21 +8766,21 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use current profile Usar perfil actual - No comment provided by engineer. + new chat action Use for files - Usar para archivos + Uso para archivos No comment provided by engineer. Use for messages - Usar para mensajes + Uso para mensajes No comment provided by engineer. Use for new connections - Usar para conexiones nuevas + Para conexiones nuevas No comment provided by engineer. @@ -7737,10 +8793,15 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar interfaz de llamada de iOS No comment provided by engineer. + + Use incognito profile + Usar perfil incógnito + No comment provided by engineer. + Use new incognito profile Usar nuevo perfil incógnito - No comment provided by engineer. + new chat action Use only local notifications? @@ -7754,7 +8815,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use private routing with unknown servers. - Usar enrutamiento privado con servidores de retransmisión desconocidos. + Usar enrutamiento privado con servidores de mensaje desconocidos. No comment provided by engineer. @@ -7777,6 +8838,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usa la aplicación con una sola mano. No comment provided by engineer. + + Use web port + Usar puerto web + No comment provided by engineer. + User selection Selección de usuarios @@ -7967,6 +9033,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Mensaje de bienvenida demasiado largo No comment provided by engineer. + + Welcome your contacts 👋 + Da la bienvenida a tus contactos 👋 + No comment provided by engineer. + What's new Novedades @@ -8090,12 +9161,12 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión You are already connecting to %@. Ya estás conectando con %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! ¡Ya estás conectando mediante este enlace de un solo uso! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8105,38 +9176,38 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión You are already joining the group %@. Ya estás uniéndote al grupo %@. - No comment provided by engineer. - - - You are already joining the group via this link! - ¡Ya estás uniéndote al grupo mediante este enlace! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Ya estás uniéndote al grupo mediante este enlace. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? ¡En proceso de unirte al grupo! ¿Repetir solicitud de admisión? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Estás conectado al servidor usado para recibir mensajes de este contacto. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + Estás conectado al servidor usado para recibir mensajes de esta conexión. + subscription status explanation You are invited to group Has sido invitado a un grupo No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + No estás conectado al servidor usado para recibir mensajes de esta conexión (no suscrito). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. - No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado. + No tienes conexión directa a estos servidores. Los mensajes destinados a estos usan enrutamiento privado. No comment provided by engineer. @@ -8149,11 +9220,6 @@ Repeat join request? Puedes cambiar la posición de la barra desde el menú Apariencia. No comment provided by engineer. - - You can configure operators in Network & servers settings. - Puedes configurar los operadores desde Servidores y Redes. - No comment provided by engineer. - You can configure servers via settings. Puedes configurar los servidores a través de su configuración. @@ -8211,7 +9277,7 @@ 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. - Puedes compartir un enlace o código QR para que cualquiera pueda unirse al grupo. Si decides eliminarlo más tarde, los miembros del grupo se mantendrán. + Puedes compartir el enlace o el código QR para que cualquiera pueda unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo. No comment provided by engineer. @@ -8241,13 +9307,18 @@ 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. + Puedes ver el enlace de invitación de nuevo en los detalles de la conexión. + alert message + + + You can view your reports in Chat with admins. + Puedes ver tus informes en Chat con administradores. alert message You can't send messages! ¡No puedes enviar mensajes! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8259,17 +9330,12 @@ Repeat join request? Tu decides quién se conecta. No comment provided by engineer. - - You have already requested connection via this address! - ¡Ya has solicitado la conexión mediante esta dirección! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Ya has solicitado la conexión ¿Repetir solicitud? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8308,7 +9374,7 @@ Repeat connection request? You need to allow your contact to call to be able to call them. - Necesitas permitir que tus contacto llamen para poder llamarles. + Debes permitir que tus contacto te llamen para poder llamarles. No comment provided by engineer. @@ -8326,6 +9392,16 @@ 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 able to send messages **only after your request is accepted**. + Podrás enviar mensajes **después de que tu solicitud sea aceptada**. + 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 revisa más tarde. @@ -8351,11 +9427,6 @@ Repeat connection request? Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano. No comment provided by engineer. - - You will connect to all group members. - Te conectarás con todos los miembros del grupo. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. @@ -8363,12 +9434,12 @@ Repeat connection request? You will stop receiving messages from this chat. Chat history will be preserved. - Dejarás de recibir mensajes de este chat. El historial del chat se conserva. + Dejarás de recibir mensajes del chat. El historial del chat se conserva. No comment provided by engineer. You will stop receiving messages from this group. Chat history will be preserved. - Dejarás de recibir mensajes de este grupo. El historial del chat se conservará. + Dejarás de recibir mensajes del grupo. El historial del chat se conservará. No comment provided by engineer. @@ -8391,16 +9462,16 @@ Repeat connection request? 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 business contact + Mi contacto empresarial + No comment provided by engineer. + Your calls Llamadas @@ -8426,11 +9497,21 @@ Repeat connection request? Mis perfiles No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Tu chat ha sido movido a %@ pero ha ocurrido un error inesperado mientras se te redirigía al perfil. + alert message + + + Your connection was moved to %@ but an error happened when switching 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 + Mi contacto + 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 (%@). @@ -8461,6 +9542,11 @@ Repeat connection request? Tu perfil actual No comment provided by engineer. + + Your group + Mi grupo + No comment provided by engineer. + Your preferences Mis preferencias @@ -8481,9 +9567,14 @@ Repeat connection request? El perfil **%@** será compartido. No comment provided by engineer. + + 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 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. + Tu perfil se almacena en tu dispositivo y se comparte sólo con tus contactos. Los servidores SimpleX no pueden ver tu perfil. No comment provided by engineer. @@ -8491,11 +9582,6 @@ Repeat connection request? Tu perfil ha sido modificado. Si lo guardas la actualización será enviada a todos tus contactos. alert message - - Your profile, contacts and delivered messages are stored on your device. - Tu perfil, contactos y mensajes se almacenan en tu dispositivo. - No comment provided by engineer. - Your random profile Tu perfil aleatorio @@ -8546,6 +9632,11 @@ Repeat connection request? y después elige: No comment provided by engineer. + + accepted %@ + %@ aceptado + rcv group event chat item + accepted call llamada aceptada @@ -8556,6 +9647,11 @@ Repeat connection request? invitación aceptada chat list item title + + accepted you + te ha aceptado + rcv group event chat item + admin administrador @@ -8576,6 +9672,11 @@ Repeat connection request? acordando cifrado… chat item text + + all + todos + member criteria value + all members todos los miembros @@ -8591,6 +9692,11 @@ Repeat connection request? y %lld evento(s) más No comment provided by engineer. + + archived report + informes archivados + No comment provided by engineer. + attempts intentos @@ -8629,7 +9735,8 @@ Repeat connection request? blocked by admin bloqueado por administrador - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8656,6 +9763,11 @@ Repeat connection request? llamando… call status + + can't send messages + no se pueden enviar mensajes + No comment provided by engineer. + cancelled %@ cancelado %@ @@ -8706,14 +9818,9 @@ Repeat connection request? conectado No comment provided by engineer. - - connected directly - conectado directamente - rcv group event chat item - connecting - conectando + conectando... No comment provided by engineer. @@ -8761,6 +9868,16 @@ Repeat connection request? el contacto %1$@ ha cambiado a %2$@ profile update event chat item + + contact deleted + contacto eliminado + No comment provided by engineer. + + + contact disabled + contacto desactivado + No comment provided by engineer. + contact has e2e encryption el contacto dispone de cifrado de extremo a extremo @@ -8771,6 +9888,16 @@ Repeat connection request? el contacto no dispone de cifrado de extremo a extremo No comment provided by engineer. + + contact not ready + en espera de ser aceptado + No comment provided by engineer. + + + contact should accept… + el contacto debe aceptarte… + No comment provided by engineer. + creator creador @@ -8799,7 +9926,8 @@ Repeat connection request? default (%@) predeterminado (%@) - pref value + delete after time +pref value default (no) @@ -8823,7 +9951,7 @@ Repeat connection request? deleted group - grupo eliminado + ha eliminado el grupo rcv group event chat item @@ -8926,31 +10054,31 @@ Repeat connection request? error No comment provided by engineer. - - event happened - evento ocurrido - No comment provided by engineer. - expired expirados No comment provided by engineer. - - for better metadata privacy. - para mejorar la privacidad de los metadatos. - No comment provided by engineer. - forwarded reenviado No comment provided by engineer. + + group + grupo + shown on group welcome message + group deleted grupo eliminado No comment provided by engineer. + + group is deleted + el grupo ha sido eliminado + No comment provided by engineer. + group profile updated perfil de grupo actualizado @@ -9046,11 +10174,6 @@ Repeat connection request? cursiva No comment provided by engineer. - - join as %@ - unirte como %@ - No comment provided by engineer. - left ha salido @@ -9076,6 +10199,11 @@ Repeat connection request? conectado rcv group event chat item + + member has old version + el miembro usa una versión antigua + No comment provided by engineer. + message mensaje @@ -9106,20 +10234,20 @@ Repeat connection request? moderado por %@ marked deleted chat item preview text + + moderator + moderador + member role + months meses time unit - - mute - silenciar - No comment provided by engineer. - never nunca - No comment provided by engineer. + delete after time new message @@ -9136,11 +10264,21 @@ Repeat connection request? sin cifrar No comment provided by engineer. + + no subscription + sin suscripciones + No comment provided by engineer. + no text sin texto copied message info in history + + not synchronized + no sincronizado + No comment provided by engineer. + observer observador @@ -9150,8 +10288,9 @@ Repeat connection request? off desactivado enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9193,6 +10332,21 @@ Repeat connection request? p2p No comment provided by engineer. + + pending + pendiente + No comment provided by engineer. + + + pending approval + pendiente de aprobación + No comment provided by engineer. + + + pending review + pendiente de revisión + No comment provided by engineer. + quantum resistant e2e encryption cifrado e2e resistente a tecnología cuántica @@ -9208,6 +10362,11 @@ Repeat connection request? confirmación recibida… No comment provided by engineer. + + rejected + rechazado + No comment provided by engineer. + rejected call llamada rechazada @@ -9228,6 +10387,11 @@ Repeat connection request? dirección de contacto eliminada profile update event chat item + + removed from group + expulsado del grupo + No comment provided by engineer. + removed profile picture ha eliminado la imagen del perfil @@ -9238,11 +10402,41 @@ Repeat connection request? te ha expulsado rcv group event chat item + + request is sent + petición enviada + No comment provided by engineer. + + + request to join rejected + petición para unirse rechazada + No comment provided by engineer. + + + requested connection + conexión solicitada + rcv group event chat item + + + requested connection from group %@ + conexión solicitada desde el grupo %@ + rcv direct event chat item + requested to connect solicitado para conectar chat list item title + + review + por revisar + No comment provided by engineer. + + + reviewed by admins + en revisión por los administradores + No comment provided by engineer. + saved guardado @@ -9278,11 +10472,6 @@ Repeat connection request? código de seguridad cambiado chat item text - - send direct message - Enviar mensaje directo - No comment provided by engineer. - server queue info: %1$@ @@ -9342,11 +10531,6 @@ last received msg: %2$@ estado desconocido No comment provided by engineer. - - unmute - activar sonido - No comment provided by engineer. - unprotected con IP desprotegida @@ -9437,10 +10621,10 @@ last received msg: %2$@ tu No comment provided by engineer. - - you are invited to group - has sido invitado a un grupo - No comment provided by engineer. + + you accepted this member + has aceptado al miembro + snd group event chat item you are observer @@ -9511,7 +10695,7 @@ last received msg: %2$@
- +
@@ -9548,7 +10732,7 @@ last received msg: %2$@
- +
@@ -9570,7 +10754,7 @@ last received msg: %2$@
- +
@@ -9578,6 +10762,11 @@ last received msg: %2$@ %d evento(s) nuevo(s) notification body + + From %d chat(s) + De %d chat(s) + notification body + From: %@ De: %@ @@ -9593,16 +10782,11 @@ last received msg: %2$@ Mensajes nuevos notification - - New messages in %d chats - Mensajes nuevos en %d chat(s) - notification body -
- +
@@ -9624,7 +10808,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/es.xcloc/contents.json b/apps/ios/SimpleX Localizations/es.xcloc/contents.json index 340591e607..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 2caa98e25b..56fa4a1485 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (voidaan kopioida) @@ -189,6 +177,10 @@ %d sek time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d ohitettua viestiä @@ -254,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 @@ -309,11 +296,6 @@ %u viestit ohitettu. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) No comment provided by engineer. @@ -322,11 +304,6 @@ (this device v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. @@ -387,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). @@ -422,11 +394,6 @@ - historian muokkaaminen. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec time to disappear @@ -439,7 +406,8 @@ 1 day 1 päivä - time interval + delete after time +time interval 1 hour @@ -454,12 +422,18 @@ 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 @@ -484,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> @@ -555,8 +524,17 @@ Accept Hyväksy accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + alert action + + + Accept as observer + alert action Accept conditions @@ -567,6 +545,10 @@ Hyväksy yhteyspyyntö? No comment provided by engineer. + + Accept contact request + alert title + Accept contact request from %@? Hyväksy kontaktipyyntö %@:ltä? @@ -575,8 +557,12 @@ Accept incognito Hyväksy tuntematon - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + alert title Accepted conditions @@ -590,6 +576,10 @@ Acknowledgement errors No comment provided by engineer. + + Active + token status text + Active connections No comment provided by engineer. @@ -603,6 +593,14 @@ Add friends No comment provided by engineer. + + Add list + No comment provided by engineer. + + + Add message + placeholder for sending contact request + Add profile Lisää profiili @@ -627,6 +625,10 @@ Lisää toiseen laitteeseen No comment provided by engineer. + + Add to list + No comment provided by engineer. + Add welcome message Lisää tervetuloviesti @@ -692,6 +694,10 @@ Advanced settings No comment provided by engineer. + + All + No comment provided by engineer. + All app data is deleted. Kaikki sovelluksen tiedot poistetaan. @@ -702,6 +708,10 @@ 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. @@ -737,6 +747,14 @@ 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ä. @@ -774,6 +792,10 @@ Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia) @@ -808,6 +830,10 @@ 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. @@ -852,6 +878,10 @@ Salli kontaktiesi lähettää katoavia viestejä. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Salli kontaktiesi lähettää ääniviestejä. @@ -864,11 +894,11 @@ Already connecting! - No comment provided by engineer. + new chat sheet title Already joining the group! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -884,6 +914,10 @@ Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti. No comment provided by engineer. + + Another reason + report reason + Answer call Vastaa puheluun @@ -907,6 +941,10 @@ App encrypts new local files (except videos). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon Sovelluksen kuvake @@ -949,6 +987,18 @@ 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. @@ -957,6 +1007,18 @@ 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. @@ -1025,10 +1087,6 @@ Hyväksy kuvat automaattisesti No comment provided by engineer. - - Auto-accept settings - alert title - Back Takaisin @@ -1060,6 +1118,10 @@ Better groups No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. No comment provided by engineer. @@ -1077,6 +1139,10 @@ Better notifications No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ No comment provided by engineer. @@ -1085,6 +1151,14 @@ Better user experience No comment provided by engineer. + + Bio + No comment provided by engineer. + + + Bio too large + alert title + Black No comment provided by engineer. @@ -1125,6 +1199,10 @@ Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sekä sinä että kontaktisi voivat käyttää viestireaktioita. @@ -1145,6 +1223,10 @@ Sekä sinä että kontaktisi voitte lähettää katoavia viestejä. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sekä sinä että kontaktisi voitte lähettää ääniviestejä. @@ -1162,11 +1244,25 @@ Business chats No comment provided by engineer. + + Business connection + 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! @@ -1193,6 +1289,10 @@ Can't call member No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Kontaktia ei voi kutsua! @@ -1211,7 +1311,8 @@ Cancel Peruuta alert action - alert button +alert button +new chat action Cancel migration @@ -1244,6 +1345,10 @@ Muuta No comment provided by engineer. + + Change automatic message deletion? + alert title + Change chat profiles authentication reason @@ -1292,7 +1397,7 @@ Change self-destruct passcode Vaihda itsetuhoutuva pääsykoodi authentication reason - set passcode view +set passcode view Chat @@ -1304,7 +1409,7 @@ Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1382,11 +1487,27 @@ Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + + + Chat with members before they join. + No comment provided by engineer. + Chats Keskustelut No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1446,6 +1567,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. @@ -1463,6 +1592,10 @@ Color mode No comment provided by engineer. + + Community guidelines violation + report reason + Compare file Vertaa tiedostoa @@ -1491,15 +1624,7 @@ Conditions of use - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1518,6 +1643,10 @@ Määritä ICE-palvelimet No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Vahvista @@ -1563,6 +1692,10 @@ Confirm upload No comment provided by engineer. + + Confirmed + token status text + Connect Yhdistä @@ -1572,9 +1705,8 @@ Connect automatically No comment provided by engineer. - - Connect incognito - Yhdistä Incognito + + Connect faster! 🚀 No comment provided by engineer. @@ -1585,37 +1717,33 @@ Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link Yhdistä linkin kautta - No comment provided by engineer. + new chat sheet title Connect via one-time link Yhdistä kertalinkillä - No comment provided by engineer. + new chat sheet title Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1664,16 +1792,29 @@ This is your own one-time link! Connection and servers status. No comment provided by engineer. + + Connection blocked + No comment provided by engineer. + Connection error Yhteysvirhe - No comment provided by engineer. + alert title Connection error (AUTH) 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. @@ -1683,6 +1824,10 @@ This is your own one-time link! Yhteyspyyntö lähetetty! No comment provided by engineer. + + Connection requires encryption renegotiation. + No comment provided by engineer. + Connection security No comment provided by engineer. @@ -1694,7 +1839,7 @@ This is your own one-time link! Connection timeout Yhteyden aikakatkaisu - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1742,6 +1887,10 @@ This is your own one-time link! Kontaktin asetukset No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -1756,6 +1905,10 @@ 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 @@ -1824,6 +1977,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/). 💻 @@ -1831,6 +1988,7 @@ This is your own one-time link! Create profile + Luo profiilisi No comment provided by engineer. @@ -1838,9 +1996,8 @@ This is your own one-time link! Luo jono server test step - - Create secret group - Luo salainen ryhmä + + Create your address No comment provided by engineer. @@ -2029,8 +2186,7 @@ This is your own one-time link! Delete Poista alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2068,6 +2224,10 @@ This is your own one-time link! Delete chat No comment provided by engineer. + + Delete chat messages from your device. + No comment provided by engineer. + Delete chat profile Poista keskusteluprofiili @@ -2078,6 +2238,10 @@ This is your own one-time link! Poista keskusteluprofiili? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2155,6 +2319,10 @@ This is your own one-time link! Poista linkki? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Poista jäsenviesti? @@ -2168,7 +2336,7 @@ This is your own one-time link! Delete messages Poista viestit - No comment provided by engineer. + alert button Delete messages after @@ -2204,6 +2372,10 @@ This is your own one-time link! Poista jono server test step + + Delete report + No comment provided by engineer. + Delete up to 20 messages at once. No comment provided by engineer. @@ -2254,11 +2426,19 @@ This is your own one-time link! Toimituskuittaukset! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Kuvaus No comment provided by engineer. + + Description too large + alert title + Desktop address No comment provided by engineer. @@ -2349,6 +2529,14 @@ This is your own one-time link! 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 @@ -2432,6 +2620,10 @@ This is your own one-time link! Do not use credentials with proxy. No comment provided by engineer. + + Documents: + No comment provided by engineer. + Don't create address Älä luo osoitetta @@ -2442,9 +2634,17 @@ This is your own one-time link! Ä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 + alert action + + + Done No comment provided by engineer. @@ -2455,7 +2655,7 @@ This is your own one-time link! Download alert button - chat item action +chat item action Download errors @@ -2514,6 +2714,10 @@ This is your own one-time link! Muokkaa ryhmäprofiilia No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable Salli @@ -2524,8 +2728,8 @@ This is your own one-time link! Salli (pidä ohitukset) No comment provided by engineer. - - Enable Flux + + Enable Flux in Network & servers settings for better metadata privacy. No comment provided by engineer. @@ -2541,12 +2745,16 @@ This is your own one-time link! Enable automatic message deletion? Ota automaattinen viestien poisto käyttöön? - No comment provided by engineer. + alert title Enable camera access No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all Salli kaikille @@ -2660,6 +2868,10 @@ This is your own one-time link! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Syötä pääsykoodi @@ -2730,6 +2942,10 @@ This is your own one-time link! Virhe kontaktipyynnön hyväksymisessä No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Virhe lisättäessä jäseniä @@ -2739,11 +2955,19 @@ This is your own one-time link! Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Virhe osoitteenvaihdossa No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile No comment provided by engineer. @@ -2756,15 +2980,23 @@ This is your own one-time link! Error changing setting Virhe asetuksen muuttamisessa - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2781,6 +3013,10 @@ This is your own one-time link! Virhe ryhmälinkin luomisessa No comment provided by engineer. + + Error creating list + alert title + Error creating member contact No comment provided by engineer. @@ -2794,20 +3030,28 @@ This is your own one-time link! Virhe profiilin luomisessa! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Virhe tiedoston salauksen purussa No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Virhe keskustelujen tietokannan poistamisessa - No comment provided by engineer. + alert title Error deleting chat! Virhe keskutelun poistamisessa! - No comment provided by engineer. + alert title Error deleting connection @@ -2817,12 +3061,12 @@ This is your own one-time link! Error deleting database Virhe tietokannan poistamisessa - No comment provided by engineer. + alert title Error deleting old database Virhe vanhan tietokannan poistamisessa - No comment provided by engineer. + alert title Error deleting token @@ -2856,7 +3100,7 @@ This is your own one-time link! Error exporting chat database Virhe vietäessä keskustelujen tietokantaa - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -2865,7 +3109,7 @@ This is your own one-time link! Error importing chat database Virhe keskustelujen tietokannan tuonnissa - No comment provided by engineer. + alert title Error joining group @@ -2884,6 +3128,10 @@ This is your own one-time link! Error opening chat No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file Virhe tiedoston vastaanottamisessa @@ -2897,10 +3145,22 @@ This is your own one-time link! Error reconnecting servers No comment provided by engineer. + + Error registering for notifications + alert title + + + Error rejecting contact request + alert title + Error removing member Virhe poistettaessa jäsentä - No comment provided by engineer. + alert title + + + Error reordering lists + alert title Error resetting statistics @@ -2911,6 +3171,10 @@ This is your own one-time link! Virhe ICE-palvelimien tallentamisessa No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile Virhe ryhmäprofiilin tallentamisessa @@ -2957,6 +3221,10 @@ This is your own one-time link! Virhe viestin lähettämisessä No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Virhe toimituskuittauksien asettamisessa! @@ -2974,7 +3242,7 @@ This is your own one-time link! Error switching profile - No comment provided by engineer. + alert title Error switching profile! @@ -2986,6 +3254,10 @@ This is your own one-time link! 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ä @@ -3026,7 +3298,13 @@ This is your own one-time link! Error: %@ Virhe: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3060,6 +3338,10 @@ This is your own one-time link! Expand chat item action + + Expired + token status text + Export database Vie tietokanta @@ -3098,24 +3380,41 @@ This is your own one-time link! 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 - No comment provided by engineer. + 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 @@ -3166,6 +3465,10 @@ This is your own one-time link! Tiedostot ja media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Tiedostot ja media ovat tässä ryhmässä kiellettyjä. @@ -3203,6 +3506,23 @@ This is your own one-time link! Löydä keskustelut nopeammin No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Korjaa @@ -3233,6 +3553,10 @@ This is your own one-time link! 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 @@ -3246,6 +3570,10 @@ This is your own one-time link! 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. @@ -3291,8 +3619,8 @@ This is your own one-time link! No comment provided by engineer. - Forwarding server %@ failed to connect to destination server %@. Please try later. - No comment provided by engineer. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3350,6 +3678,10 @@ Error: %2$@ GIFit ja tarrat No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3369,7 +3701,7 @@ Error: %2$@ Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3436,6 +3768,10 @@ Error: %2$@ Ryhmäprofiili tallennetaan jäsenten laitteille, ei palvelimille. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Ryhmän tervetuloviesti @@ -3451,11 +3787,19 @@ Error: %2$@ 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 @@ -3513,6 +3857,10 @@ Error: %2$@ How it helps privacy No comment provided by engineer. + + How it works + alert button + How to Miten @@ -3645,6 +3993,14 @@ More improvements are coming soon! In-call sounds No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Incognito @@ -3734,6 +4090,26 @@ More improvements are coming soon! 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. @@ -3749,7 +4125,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -3857,32 +4233,29 @@ More improvements are coming soon! Liity swipe action + + Join as %@ + Liity %@:nä + No comment provided by engineer. + Join group Liity ryhmään - No comment provided by engineer. + new chat sheet title Join group conversations No comment provided by engineer. - - Join group? - No comment provided by engineer. - Join incognito Liity incognito-tilassa No comment provided by engineer. - - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -3905,6 +4278,10 @@ This is your link for group %@! Keep unused invitation? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections Pidä kontaktisi @@ -3958,6 +4335,10 @@ This is your link for group %@! Poistu ryhmästä? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat Jutellaan SimpleX Chatissa @@ -3985,6 +4366,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! @@ -3995,6 +4388,10 @@ This is your link for group %@! Live-viestit No comment provided by engineer. + + Loading profile… + in progress text + Local name Paikallinen nimi @@ -4068,10 +4465,26 @@ This is your link for group %@! Jäsen No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + No comment provided by engineer. + Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + Member reports + chat feature + Member role will be changed to "%@". All chat members will be notified. No comment provided by engineer. @@ -4095,6 +4508,10 @@ This is your link for group %@! Jäsen poistetaan ryhmästä - tätä ei voi perua! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Ryhmän jäsenet voivat lisätä viestireaktioita. @@ -4105,6 +4522,10 @@ This is your link for group %@! 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. @@ -4129,6 +4550,10 @@ This is your link for group %@! 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. @@ -4156,6 +4581,10 @@ This is your link for group %@! Message forwarded item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. item status description @@ -4222,10 +4651,18 @@ This is your link for group %@! Viestit ja tiedostot No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + Messages received No comment provided by engineer. @@ -4314,6 +4751,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! @@ -4340,7 +4781,11 @@ This is your link for group %@! Mute Mykistä - swipe action + notification label action + + + Mute all + notification label action Muted when inactive! @@ -4385,7 +4830,11 @@ This is your link for group %@! Network status Verkon tila - No comment provided by engineer. + alert title + + + New + token status text New Passcode @@ -4431,6 +4880,10 @@ This is your link for group %@! New events notification + + New group role: Moderator + No comment provided by engineer. + New in %@ Uutta %@ @@ -4445,6 +4898,10 @@ This is your link for group %@! Uusi jäsenrooli No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Uusi viesti @@ -4469,6 +4926,22 @@ 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 chats with members + No comment provided by engineer. + No contacts selected Kontakteja ei ole valittu @@ -4516,6 +4989,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -4537,6 +5014,10 @@ This is your link for group %@! Ei lupaa ääniviestin tallentamiseen No comment provided by engineer. + + No private routing session + alert title + No push server Paikallinen @@ -4563,6 +5044,14 @@ This is your link for group %@! 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. @@ -4572,6 +5061,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Notes + No comment provided by engineer. + Nothing selected No comment provided by engineer. @@ -4590,10 +5083,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. @@ -4615,7 +5116,9 @@ This is your link for group %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -4674,6 +5177,14 @@ Edellyttää VPN:n sallimista. 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. @@ -4694,6 +5205,10 @@ Edellyttää VPN:n sallimista. Vain sinä voit lähettää katoavia viestejä. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Vain sinä voit lähettää ääniviestejä. @@ -4719,6 +5234,10 @@ Edellyttää VPN:n sallimista. Vain kontaktisi voi lähettää katoavia viestejä. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Vain kontaktisi voi lähettää ääniviestejä. @@ -4726,7 +5245,7 @@ Edellyttää VPN:n sallimista. Open - No comment provided by engineer. + alert action Open Settings @@ -4740,25 +5259,61 @@ Edellyttää VPN:n sallimista. Open chat Avaa keskustelu - No comment provided by engineer. + new chat action Open chat console Avaa keskustelukonsoli authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group - No comment provided by engineer. + new chat action + + + Open link? + alert title Open migration to another device authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -4795,6 +5350,10 @@ Edellyttää VPN:n sallimista. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other No comment provided by engineer. @@ -4848,10 +5407,6 @@ Edellyttää VPN:n sallimista. Salasana näytettäväksi No comment provided by engineer. - - Past member %@ - past/unknown group member - Paste desktop address No comment provided by engineer. @@ -4913,7 +5468,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. Tarkista verkkoyhteytesi %@:lla ja yritä uudelleen. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -4969,6 +5524,22 @@ 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 group moderators to review your request to join the group. + snd group event chat item + + + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + token info + Polish interface Puolalainen käyttöliittymä @@ -4978,11 +5549,6 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen - server test error - Preserve the last message draft, with attachments. Säilytä viimeinen viestiluonnos liitteineen. @@ -5015,16 +5581,28 @@ Error: %@ 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. @@ -5043,7 +5621,11 @@ Error: %@ Private routing error - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5093,6 +5675,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. @@ -5136,6 +5722,10 @@ Enable in *Network & servers* settings. Suojaa keskusteluprofiilisi salasanalla! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout Protokollan aikakatkaisu @@ -5233,11 +5823,6 @@ Enable in *Network & servers* settings. Vastaanotettu klo: %@ copied message info - - Received file event - Tiedoston vastaanottotapahtuma - notification - Received message Vastaanotettu viesti @@ -5328,11 +5913,24 @@ Enable in *Network & servers* settings. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5342,7 +5940,11 @@ Enable in *Network & servers* settings. Reject contact request Hylkää yhteyspyyntö - No comment provided by engineer. + alert title + + + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5367,6 +5969,10 @@ Enable in *Network & servers* settings. Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Poista jäsen @@ -5382,6 +5988,10 @@ Enable in *Network & servers* settings. Poista tunnuslause avainnipusta? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate Neuvottele uudelleen @@ -5397,10 +6007,6 @@ Enable in *Network & servers* settings. Uudelleenneuvottele salaus? No comment provided by engineer. - - Repeat connection request? - No comment provided by engineer. - Repeat download No comment provided by engineer. @@ -5409,10 +6015,6 @@ Enable in *Network & servers* settings. Repeat import No comment provided by engineer. - - Repeat join request? - No comment provided by engineer. - Repeat upload No comment provided by engineer. @@ -5422,6 +6024,50 @@ Enable in *Network & servers* settings. 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 sent to moderators + alert title + + + 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 @@ -5494,7 +6140,7 @@ Enable in *Network & servers* settings. Retry - No comment provided by engineer. + alert action Reveal @@ -5505,10 +6151,18 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later + + Review group members No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Peruuta @@ -5554,13 +6208,21 @@ Enable in *Network & servers* settings. Save Tallenna alert button - chat item action +chat item action Save (and notify contacts) Tallenna (ja ilmoita kontakteille) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Tallenna ja ilmoita kontaktille @@ -5585,6 +6247,14 @@ Enable in *Network & servers* settings. Tallenna ryhmäprofiili No comment provided by engineer. + + Save group profile? + alert title + + + Save list + No comment provided by engineer. + Save passphrase and open chat Tallenna tunnuslause ja avaa keskustelu @@ -5760,6 +6430,10 @@ Enable in *Network & servers* settings. Lähetä live-viesti - se päivittyy vastaanottajille, kun kirjoitat sitä No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to Lähetä toimituskuittaukset vastaanottajalle @@ -5805,6 +6479,10 @@ Enable in *Network & servers* settings. Lähetys ilmoitukset No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Lähetä kysymyksiä ja ideoita @@ -5815,6 +6493,14 @@ Enable in *Network & servers* settings. Lähetä kuittaukset No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. Lähetä ne galleriasta tai mukautetuista näppäimistöistä. @@ -5824,6 +6510,10 @@ Enable in *Network & servers* settings. Send up to 100 last messages to new members. No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. Lähettäjä peruutti tiedoston siirron. @@ -5888,11 +6578,6 @@ Enable in *Network & servers* settings. Sent directly No comment provided by engineer. - - Sent file event - Lähetetty tiedosto tapahtuma - notification - Sent message Lähetetty viesti @@ -5951,13 +6636,13 @@ Enable in *Network & servers* settings. Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana server test error @@ -6000,6 +6685,10 @@ Enable in *Network & servers* settings. Aseta 1 päivä No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… Aseta kontaktin nimi… @@ -6019,6 +6708,14 @@ Enable in *Network & servers* settings. Aseta se järjestelmän todennuksen sijaan. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Aseta pääsykoodi @@ -6033,6 +6730,10 @@ Enable in *Network & servers* settings. Aseta tunnuslause vientiä varten No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! Aseta uusille jäsenille näytettävä viesti! @@ -6060,7 +6761,7 @@ Enable in *Network & servers* settings. Share Jaa alert action - chat item action +chat item action Share 1-time link @@ -6098,6 +6799,14 @@ Enable in *Network & servers* settings. Jaa linkki No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile No comment provided by engineer. @@ -6115,6 +6824,22 @@ Enable in *Network & servers* settings. Jaa kontaktien kanssa No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -6207,6 +6932,14 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX address settings + alert title + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX-yhteystiedot @@ -6244,6 +6977,10 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode No comment provided by engineer. @@ -6298,6 +7035,11 @@ Enable in *Network & servers* settings. Joku notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. No comment provided by engineer. @@ -6377,6 +7119,10 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Storage + No comment provided by engineer. + Strong blur media @@ -6425,11 +7171,19 @@ Enable in *Network & servers* settings. TCP connection No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -6454,10 +7208,26 @@ Enable in *Network & servers* settings. Ota kuva No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join group + No comment provided by engineer. + Tap button Napauta painiketta @@ -6492,13 +7262,17 @@ Enable in *Network & servers* settings. Temporary file error - No comment provided by engineer. + 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 @@ -6536,6 +7310,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + 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. @@ -6592,6 +7370,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Edellisen viestin tarkiste on erilainen. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. Viesti poistetaan kaikilta jäseniltä. @@ -6615,19 +7397,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa. No comment provided by engineer. - - The profile is only shared with your contacts. - Profiili jaetaan vain kontaktiesi kanssa. - No comment provided by engineer. - The same conditions will apply to operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! No comment provided by engineer. @@ -6640,7 +7413,7 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut. The sender will NOT be notified Lähettäjälle EI ilmoiteta - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -6687,6 +7460,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. @@ -6718,23 +7495,31 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Tätä ryhmää ei enää ole olemassa. No comment provided by engineer. - - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time 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. 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title No comment provided by engineer. @@ -6809,11 +7594,19 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. @@ -6831,6 +7624,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Toggle incognito when connecting. No comment provided by engineer. + + Token status: %@. + token status + Toolbar opacity No comment provided by engineer. @@ -6848,15 +7645,9 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote 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: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -6983,13 +7774,17 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Unmute Poista mykistys - swipe action + notification label action Unread Lukematon swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. No comment provided by engineer. @@ -7013,16 +7808,44 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Update settings? No comment provided by engineer. + + Updated conditions + 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. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat Päivitä ja avaa keskustelu No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors No comment provided by engineer. @@ -7066,6 +7889,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja 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 @@ -7074,7 +7905,7 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use current profile Käytä nykyistä profiilia - No comment provided by engineer. + new chat action Use for files @@ -7098,10 +7929,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Käytä iOS:n puhelujen käyttöliittymää No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile Käytä uutta incognito-profiilia - No comment provided by engineer. + new chat action Use only local notifications? @@ -7132,6 +7967,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use the app with one hand. No comment provided by engineer. + + Use web port + No comment provided by engineer. + User selection No comment provided by engineer. @@ -7305,6 +8144,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Welcome message is too long No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new Uusimmat @@ -7413,11 +8256,11 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7425,31 +8268,30 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja 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. + new chat sheet message You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Sinut on kutsuttu ryhmään No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. No comment provided by engineer. @@ -7463,10 +8305,6 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. - - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. No comment provided by engineer. @@ -7550,10 +8388,14 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Et voi lähettää viestejä! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -7565,14 +8407,10 @@ Repeat join request? 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. - You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -7626,6 +8464,14 @@ Repeat connection request? Lähetit ryhmäkutsun No comment provided by engineer. + + You should receive notifications. + token info + + + You will be able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Sinut yhdistetään ryhmään, kun ryhmän isännän laite on online-tilassa, odota tai tarkista myöhemmin! @@ -7650,10 +8496,6 @@ Repeat connection request? Sinun on tunnistauduttava, kun käynnistät sovelluksen tai jatkat sen käyttöä 30 sekunnin tauon jälkeen. No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia. @@ -7688,16 +8530,15 @@ Repeat connection request? 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 business contact + No comment provided by engineer. + Your calls Puhelusi @@ -7722,8 +8563,16 @@ Repeat connection request? Keskusteluprofiilisi No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. + No comment provided by engineer. + + + Your contact No comment provided by engineer. @@ -7755,6 +8604,10 @@ Repeat connection request? Nykyinen profiilisi No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences Asetuksesi @@ -7774,6 +8627,11 @@ Repeat connection request? Profiilisi **%@** jaetaan. No comment provided by engineer. + + 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 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. @@ -7783,11 +8641,6 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message - - Your profile, contacts and delivered messages are stored on your device. - Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi. - No comment provided by engineer. - Your random profile Satunnainen profiilisi @@ -7837,6 +8690,10 @@ Repeat connection request? edellä, valitse sitten: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call hyväksytty puhelu @@ -7846,6 +8703,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin ylläpitäjä @@ -7865,6 +8726,10 @@ Repeat connection request? hyväksyy salausta… chat item text + + all + member criteria value + all members feature role @@ -7878,6 +8743,10 @@ Repeat connection request? and %lld other events No comment provided by engineer. + + archived report + No comment provided by engineer. + attempts No comment provided by engineer. @@ -7911,7 +8780,8 @@ Repeat connection request? blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -7937,6 +8807,10 @@ Repeat connection request? soittaa… call status + + can't send messages + No comment provided by engineer. + cancelled %@ peruutettu %@ @@ -7987,10 +8861,6 @@ Repeat connection request? yhdistetty No comment provided by engineer. - - connected directly - rcv group event chat item - connecting yhdistää @@ -8040,6 +8910,14 @@ Repeat connection request? contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption kontaktilla on e2e-salaus @@ -8050,6 +8928,14 @@ Repeat connection request? kontaktilla ei ole e2e-salausta No comment provided by engineer. + + contact not ready + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator luoja @@ -8077,7 +8963,8 @@ Repeat connection request? default (%@) oletusarvo (%@) - pref value + delete after time +pref value default (no) @@ -8202,28 +9089,27 @@ Repeat connection request? virhe No comment provided by engineer. - - event happened - tapahtuma tapahtui - No comment provided by engineer. - expired No comment provided by engineer. - - for better metadata privacy. - No comment provided by engineer. - forwarded No comment provided by engineer. + + group + shown on group welcome message + group deleted ryhmä poistettu No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated ryhmäprofiili päivitetty @@ -8317,11 +9203,6 @@ Repeat connection request? kursivoitu No comment provided by engineer. - - join as %@ - Liity %@:nä - No comment provided by engineer. - left poistunut @@ -8346,6 +9227,10 @@ Repeat connection request? yhdistetty rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8375,19 +9260,19 @@ Repeat connection request? %@ moderoi marked deleted chat item preview text + + moderator + member role + months kuukautta time unit - - mute - No comment provided by engineer. - never ei koskaan - No comment provided by engineer. + delete after time new message @@ -8404,11 +9289,19 @@ Repeat connection request? ei e2e-salausta No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text ei tekstiä copied message info in history + + not synchronized + No comment provided by engineer. + observer tarkkailija @@ -8418,8 +9311,9 @@ Repeat connection request? off pois enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -8458,6 +9352,18 @@ Repeat connection request? vertais No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -8472,6 +9378,10 @@ Repeat connection request? vahvistus saatu… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call hylätty puhelu @@ -8491,6 +9401,10 @@ Repeat connection request? removed contact address profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profile update event chat item @@ -8500,10 +9414,34 @@ Repeat connection request? poisti sinut rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved No comment provided by engineer. @@ -8536,10 +9474,6 @@ Repeat connection request? turvakoodi on muuttunut chat item text - - send direct message - No comment provided by engineer. - server queue info: %1$@ @@ -8590,10 +9524,6 @@ last received msg: %2$@ unknown status No comment provided by engineer. - - unmute - No comment provided by engineer. - unprotected No comment provided by engineer. @@ -8678,10 +9608,9 @@ last received msg: %2$@ you No comment provided by engineer. - - you are invited to group - sinut on kutsuttu ryhmään - No comment provided by engineer. + + you accepted this member + snd group event chat item you are observer @@ -8750,7 +9679,7 @@ last received msg: %2$@
- +
@@ -8786,7 +9715,7 @@ last received msg: %2$@
- +
@@ -8808,13 +9737,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -8827,15 +9760,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -8854,7 +9783,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json index 78ce40cec5..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 148156b07c..67485353d2 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (peut être copié) @@ -114,10 +102,12 @@ %@ server + Serveur %@ No comment provided by engineer. %@ servers + Serveurs %@ No comment provided by engineer. @@ -200,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 @@ -270,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 @@ -325,11 +315,6 @@ %u messages sautés. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nouveau) @@ -340,11 +325,6 @@ (cet appareil v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Ajouter un contact** : pour créer un nouveau lien d'invitation. @@ -382,6 +362,7 @@ **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. @@ -409,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). @@ -450,11 +426,6 @@ - l'historique de modification. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -468,7 +439,8 @@ 1 day 1 jour - time interval + delete after time +time interval 1 hour @@ -483,19 +455,28 @@ 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. @@ -513,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> @@ -574,6 +550,7 @@ About operators + À propos des opérateurs No comment provided by engineer. @@ -585,11 +562,23 @@ Accept Accepter accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Accepter en tant que membre + alert action + + + Accept as observer + Accepter en tant qu'observateur + alert action Accept conditions + Accepter les conditions No comment provided by engineer. @@ -597,6 +586,11 @@ Accepter la demande de connexion ? No comment provided by engineer. + + Accept contact request + Accepter la demande de contact + alert title + Accept contact request from %@? Accepter la demande de contact de %@ ? @@ -605,11 +599,17 @@ Accept incognito Accepter en incognito - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Accepter le membre + alert title Accepted conditions + Conditions acceptées No comment provided by engineer. @@ -622,6 +622,11 @@ Erreur d'accusé de réception No comment provided by engineer. + + Active + Actif + token status text + Active connections Connections actives @@ -634,8 +639,19 @@ Add friends + Ajouter des amis No comment provided by engineer. + + Add list + Ajouter une liste + No comment provided by engineer. + + + Add message + Ajouter un message + placeholder for sending contact request + Add profile Ajouter un profil @@ -653,6 +669,7 @@ Add team members + Ajouter des membres à l'équipe No comment provided by engineer. @@ -660,6 +677,11 @@ 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 @@ -667,14 +689,17 @@ 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. @@ -704,10 +729,12 @@ 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. @@ -730,6 +757,11 @@ 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. @@ -740,6 +772,11 @@ 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. @@ -757,6 +794,7 @@ 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. @@ -779,6 +817,16 @@ 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 + Tous les serveurs + No comment provided by engineer. + All your contacts will remain connected. Tous vos contacts resteront connectés. @@ -819,6 +867,11 @@ Autoriser la rétrogradation No comment provided by engineer. + + Allow files and media only if your contact allows them. + Permettre des fichiers et des médias seulement si votre contact les permet. + 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) @@ -854,6 +907,11 @@ 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. @@ -899,6 +957,11 @@ Autorise votre contact à envoyer des messages éphémères. No comment provided by engineer. + + Allow your contacts to send files and media. + Permettre à vos contacts d'envoyer des fichiers et des médias. + No comment provided by engineer. + Allow your contacts to send voice messages. Autorise vos contacts à envoyer des messages vocaux. @@ -912,12 +975,12 @@ Already connecting! Déjà en connexion ! - No comment provided by engineer. + new chat sheet title Already joining the group! Groupe déjà rejoint ! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -934,6 +997,11 @@ 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 @@ -959,6 +1027,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 @@ -1004,6 +1076,21 @@ 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 @@ -1014,6 +1101,21 @@ 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 @@ -1084,11 +1186,6 @@ Images auto-acceptées No comment provided by engineer. - - Auto-accept settings - Paramètres de réception automatique - alert title - Back Retour @@ -1124,6 +1221,11 @@ 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. @@ -1144,6 +1246,11 @@ 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 ✅ @@ -1154,6 +1261,14 @@ Une meilleure expérience pour l'utilisateur No comment provided by engineer. + + Bio + No comment provided by engineer. + + + Bio too large + alert title + Black Noir @@ -1204,6 +1319,10 @@ Flouter les médias No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Vous et votre contact pouvez ajouter des réactions aux messages. @@ -1224,6 +1343,10 @@ Vous et votre contact êtes tous deux en mesure d'envoyer des messages éphémères. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Vous et votre contact êtes tous deux en mesure d'envoyer des messages vocaux. @@ -1236,10 +1359,21 @@ Business address + Adresse professionnelle No comment provided by engineer. Business chats + Discussions professionnelles + No comment provided by engineer. + + + Business connection + No comment provided by engineer. + + + Businesses + Entreprises No comment provided by engineer. @@ -1247,6 +1381,15 @@ 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é ! @@ -1277,6 +1420,10 @@ Impossible d'appeler le membre No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Impossible d'inviter le contact ! @@ -1296,7 +1443,8 @@ Cancel Annuler alert action - alert button +alert button +new chat action Cancel migration @@ -1333,8 +1481,14 @@ 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 @@ -1381,19 +1535,22 @@ Change self-destruct passcode Modifier le code d'autodestruction authentication reason - set passcode view +set passcode view Chat + Discussions No comment provided by engineer. Chat already exists + La discussion existe déjà No comment provided by engineer. Chat already exists! - No comment provided by engineer. + La discussion existe déjà ! + new chat sheet title Chat colors @@ -1472,10 +1629,24 @@ 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. + + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + + + Chat with members before they join. No comment provided by engineer. @@ -1483,12 +1654,18 @@ Discussions No comment provided by engineer. + + Chats with members + 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. @@ -1546,6 +1723,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 ? @@ -1566,6 +1753,11 @@ Mode de couleur No comment provided by engineer. + + Community guidelines violation + Infraction aux règles communautaires + report reason + Compare file Comparer le fichier @@ -1583,38 +1775,37 @@ 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 - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + Conditions d'utilisation + alert button 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. @@ -1622,6 +1813,11 @@ 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 @@ -1672,6 +1868,11 @@ Confirmer la transmission No comment provided by engineer. + + Confirmed + Confirmé + token status text + Connect Se connecter @@ -1682,9 +1883,8 @@ Connexion automatique No comment provided by engineer. - - Connect incognito - Se connecter incognito + + Connect faster! 🚀 No comment provided by engineer. @@ -1697,44 +1897,39 @@ Connectez-vous à vos amis plus rapidement. No comment provided by engineer. - - Connect to yourself? - Se connecter à soi-même ? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Se connecter à soi-même ? C'est votre propre adresse SimpleX ! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Se connecter à soi-même ? Il s'agit de votre propre lien unique ! - No comment provided by engineer. + new chat sheet title Connect via contact address Se connecter via l'adresse de contact - No comment provided by engineer. + new chat sheet title Connect via link Se connecter via un lien - No comment provided by engineer. + new chat sheet title Connect via one-time link Se connecter via un lien unique - No comment provided by engineer. + new chat sheet title Connect with %@ Se connecter avec %@ - No comment provided by engineer. + new chat action Connected @@ -1791,16 +1986,33 @@ Il s'agit de votre propre lien unique ! É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 - No comment provided by engineer. + alert title Connection error (AUTH) 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 @@ -1811,8 +2023,14 @@ Il s'agit de votre propre lien unique ! 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. @@ -1823,7 +2041,7 @@ Il s'agit de votre propre lien unique ! Connection timeout Délai de connexion - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1875,6 +2093,10 @@ Il s'agit de votre propre lien unique ! Préférences de contact No comment provided by engineer. + + Contact requests from groups + 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 ! @@ -1890,6 +2112,11 @@ 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 @@ -1932,6 +2159,7 @@ Il s'agit de votre propre lien unique ! Create 1-time link + Créer un lien unique No comment provided by engineer. @@ -1964,6 +2192,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/). 💻 @@ -1979,9 +2212,8 @@ Il s'agit de votre propre lien unique ! Créer une file d'attente server test step - - Create secret group - Créer un groupe secret + + Create your address No comment provided by engineer. @@ -2021,6 +2253,7 @@ Il s'agit de votre propre lien unique ! 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. @@ -2180,8 +2413,7 @@ Il s'agit de votre propre lien unique ! Delete Supprimer alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2220,6 +2452,12 @@ Il s'agit de votre propre lien unique ! Delete chat + Supprimer la discussion + No comment provided by engineer. + + + Delete chat messages from your device. + Supprimer les messages de chat de votre appareil. No comment provided by engineer. @@ -2232,8 +2470,13 @@ Il s'agit de votre propre lien unique ! Supprimer le profil du chat ? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? + Supprimer la discussion ? No comment provided by engineer. @@ -2311,6 +2554,11 @@ Il s'agit de votre propre lien unique ! Supprimer le lien ? No comment provided by engineer. + + Delete list? + Supprimer la liste ? + alert title + Delete member message? Supprimer le message de ce membre ? @@ -2324,7 +2572,7 @@ Il s'agit de votre propre lien unique ! Delete messages Supprimer les messages - No comment provided by engineer. + alert button Delete messages after @@ -2361,6 +2609,11 @@ Il s'agit de votre propre lien unique ! 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. @@ -2398,6 +2651,7 @@ Il s'agit de votre propre lien unique ! Delivered even when Apple drops them. + Distribués même quand Apple les oublie. No comment provided by engineer. @@ -2415,11 +2669,19 @@ Il s'agit de votre propre lien unique ! Justificatifs de réception ! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Description No comment provided by engineer. + + Description too large + alert title + Desktop address Adresse de bureau @@ -2502,6 +2764,7 @@ Il s'agit de votre propre lien unique ! Direct messages between members are prohibited in this chat. + Les messages directs entre membres sont interdits dans cette discussion. No comment provided by engineer. @@ -2519,6 +2782,16 @@ Il s'agit de votre propre lien unique ! 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 @@ -2609,6 +2882,11 @@ Il s'agit de votre propre lien unique ! 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 @@ -2619,9 +2897,19 @@ Il s'agit de votre propre lien unique ! 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 + alert action + + + Done + Terminé No comment provided by engineer. @@ -2633,7 +2921,7 @@ Il s'agit de votre propre lien unique ! Download Télécharger alert button - chat item action +chat item action Download errors @@ -2687,6 +2975,7 @@ Il s'agit de votre propre lien unique ! E2E encrypted notifications. + Notifications chiffrées E2E. No comment provided by engineer. @@ -2699,6 +2988,10 @@ Il s'agit de votre propre lien unique ! Modifier le profil du groupe No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable Activer @@ -2709,8 +3002,9 @@ Il s'agit de votre propre lien unique ! Activer (conserver les remplacements) No comment provided by engineer. - - Enable Flux + + 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. @@ -2726,13 +3020,17 @@ Il s'agit de votre propre lien unique ! Enable automatic message deletion? Activer la suppression automatique des messages ? - No comment provided by engineer. + alert title Enable camera access Autoriser l'accès à la caméra No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all Activer pour tous @@ -2853,6 +3151,11 @@ Il s'agit de votre propre lien unique ! 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 @@ -2920,6 +3223,7 @@ Il s'agit de votre propre lien unique ! Error accepting conditions + Erreur lors de la validation des conditions alert title @@ -2927,6 +3231,10 @@ Il s'agit de votre propre lien unique ! Erreur de validation de la demande de contact No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Erreur lors de l'ajout de membre·s @@ -2934,13 +3242,22 @@ Il s'agit de votre propre lien unique ! Error adding server + Erreur lors de l'ajout du serveur alert title + + Error adding short link + No comment provided by engineer. + Error changing address Erreur de changement d'adresse No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile Erreur lors du changement de profil de connexion @@ -2954,17 +3271,26 @@ Il s'agit de votre propre lien unique ! Error changing setting Erreur de changement de paramètre - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2981,6 +3307,11 @@ Il s'agit de votre propre lien unique ! 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 @@ -2996,20 +3327,29 @@ Il s'agit de votre propre lien unique ! 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 No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Erreur lors de la suppression de la base de données du chat - No comment provided by engineer. + alert title Error deleting chat! Erreur lors de la suppression du chat ! - No comment provided by engineer. + alert title Error deleting connection @@ -3019,12 +3359,12 @@ Il s'agit de votre propre lien unique ! Error deleting database Erreur lors de la suppression de la base de données - No comment provided by engineer. + alert title Error deleting old database Erreur lors de la suppression de l'ancienne base de données - No comment provided by engineer. + alert title Error deleting token @@ -3059,7 +3399,7 @@ Il s'agit de votre propre lien unique ! Error exporting chat database Erreur lors de l'exportation de la base de données du chat - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3069,7 +3409,7 @@ Il s'agit de votre propre lien unique ! Error importing chat database Erreur lors de l'importation de la base de données du chat - No comment provided by engineer. + alert title Error joining group @@ -3078,6 +3418,7 @@ Il s'agit de votre propre lien unique ! Error loading servers + Erreur de chargement des serveurs alert title @@ -3090,6 +3431,10 @@ Il s'agit de votre propre lien unique ! Erreur lors de l'ouverture du chat No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file Erreur lors de la réception du fichier @@ -3105,10 +3450,24 @@ Il s'agit de votre propre lien unique ! Erreur de reconnexion des serveurs No comment provided by engineer. + + Error registering for notifications + Erreur lors de l'inscription aux notifications + alert title + + + Error rejecting contact request + alert title + Error removing member Erreur lors de la suppression d'un membre - No comment provided by engineer. + alert title + + + Error reordering lists + Erreur lors de la réorganisation des listes + alert title Error resetting statistics @@ -3120,6 +3479,11 @@ Il s'agit de votre propre lien unique ! 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 @@ -3137,6 +3501,7 @@ Il s'agit de votre propre lien unique ! Error saving servers + Erreur d'enregistrement des serveurs alert title @@ -3169,6 +3534,10 @@ Il s'agit de votre propre lien unique ! Erreur lors de l'envoi du message No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Erreur lors de la configuration des accusés de réception ! @@ -3187,7 +3556,7 @@ Il s'agit de votre propre lien unique ! Error switching profile Erreur lors du changement de profil - No comment provided by engineer. + alert title Error switching profile! @@ -3199,6 +3568,11 @@ Il s'agit de votre propre lien unique ! 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 @@ -3211,6 +3585,7 @@ Il s'agit de votre propre lien unique ! Error updating server + Erreur de mise à jour du serveur alert title @@ -3241,7 +3616,13 @@ Il s'agit de votre propre lien unique ! Error: %@ Erreur : %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3260,6 +3641,7 @@ Il s'agit de votre propre lien unique ! Errors in servers configuration. + Erreurs dans la configuration des serveurs. servers error @@ -3277,6 +3659,11 @@ Il s'agit de votre propre lien unique ! Étendre chat item action + + Expired + Expiré + token status text + Export database Exporter la base de données @@ -3317,20 +3704,35 @@ Il s'agit de votre propre lien unique ! 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 - No comment provided by engineer. + file error alert title File errors: @@ -3339,6 +3741,13 @@ Il s'agit de votre propre lien unique ! %@ 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é. @@ -3394,6 +3803,10 @@ Il s'agit de votre propre lien unique ! Fichiers et médias chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Les fichiers et les médias sont interdits dans ce groupe. @@ -3434,6 +3847,23 @@ Il s'agit de votre propre lien unique ! Recherche de message plus rapide No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Réparer @@ -3464,8 +3894,13 @@ Il s'agit de votre propre lien unique ! 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 @@ -3475,14 +3910,21 @@ Il s'agit de votre propre lien unique ! 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. @@ -3531,9 +3973,9 @@ Il s'agit de votre propre lien unique ! 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 %1$@ failed to connect to destination server %2$@. Please try later. + Le serveur de redirection %1$@ n'a pas réussi à se connecter au serveur de destination %2$@. Veuillez réessayer plus tard. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3599,6 +4041,10 @@ Erreur : %2$@ GIFs et stickers No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Bonjour ! @@ -3622,7 +4068,7 @@ Erreur : %2$@ Group already exists! Ce groupe existe déjà ! - No comment provided by engineer. + new chat sheet title Group display name @@ -3689,6 +4135,10 @@ Erreur : %2$@ Le profil du groupe est stocké sur les appareils des membres, pas sur les serveurs. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Message d'accueil du groupe @@ -3704,11 +4154,19 @@ Erreur : %2$@ 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é @@ -3761,12 +4219,18 @@ Erreur : %2$@ 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 + alert button + How to Comment faire @@ -3909,6 +4373,14 @@ D'autres améliorations sont à venir ! Sons d'appel No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Incognito @@ -4001,6 +4473,26 @@ D'autres améliorations sont à venir ! 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 @@ -4019,7 +4511,7 @@ D'autres améliorations sont à venir ! Invalid link Lien invalide - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4063,6 +4555,7 @@ D'autres améliorations sont à venir ! Invite to chat + Inviter à discuter No comment provided by engineer. @@ -4131,37 +4624,32 @@ D'autres améliorations sont à venir ! Rejoindre swipe action + + Join as %@ + rejoindre entant que %@ + No comment provided by engineer. + Join group Rejoindre le groupe - No comment provided by engineer. + new chat sheet title Join group conversations Participez aux conversations de groupe No comment provided by engineer. - - Join group? - Rejoindre le groupe ? - No comment provided by engineer. - Join incognito Rejoindre en incognito No comment provided by engineer. - - Join with current profile - Rejoindre avec le profil actuel - No comment provided by engineer. - Join your group? This is your link for group %@! Rejoindre votre groupe ? Voici votre lien pour le groupe %@ ! - No comment provided by engineer. + new chat action Joining group @@ -4188,6 +4676,10 @@ Voici votre lien pour le groupe %@ ! Conserver l'invitation inutilisée ? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections Conserver vos connexions @@ -4225,10 +4717,12 @@ Voici votre lien pour le groupe %@ ! Leave chat + Quitter la discussion No comment provided by engineer. Leave chat? + Quitter la discussion ? No comment provided by engineer. @@ -4241,6 +4735,10 @@ Voici votre lien pour le groupe %@ ! Quitter le groupe ? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat Discutons sur SimpleX Chat @@ -4271,6 +4769,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 ! @@ -4281,6 +4791,10 @@ Voici votre lien pour le groupe %@ ! Messages dynamiques No comment provided by engineer. + + Loading profile… + in progress text + Local name Nom local @@ -4356,13 +4870,30 @@ Voici votre lien pour le groupe %@ ! Membre No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + No comment provided by engineer. + Member inactive Membre inactif item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + 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. @@ -4377,6 +4908,7 @@ Voici votre lien pour le groupe %@ ! 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. @@ -4384,6 +4916,10 @@ Voici votre lien pour le groupe %@ ! Ce membre sera retiré du groupe - impossible de revenir en arrière ! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Les membres du groupe peuvent ajouter des réactions aux messages. @@ -4394,6 +4930,10 @@ Voici votre lien pour le groupe %@ ! 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. @@ -4419,6 +4959,10 @@ Voici votre lien pour le groupe %@ ! Les membres du groupe peuvent envoyer des messages vocaux. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menus @@ -4449,6 +4993,10 @@ Voici votre lien pour le groupe %@ ! Message transféré item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. Le message peut être transmis plus tard si le membre devient actif. @@ -4524,11 +5072,19 @@ Voici votre lien pour le groupe %@ ! Messages No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! 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 @@ -4629,6 +5185,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 ! @@ -4641,6 +5201,7 @@ Voici votre lien pour le groupe %@ ! More reliable notifications + Notifications plus fiables No comment provided by engineer. @@ -4656,7 +5217,11 @@ Voici votre lien pour le groupe %@ ! Mute Muet - swipe action + notification label action + + + Mute all + notification label action Muted when inactive! @@ -4680,6 +5245,7 @@ Voici votre lien pour le groupe %@ ! Network decentralization + Décentralisation du réseau No comment provided by engineer. @@ -4694,6 +5260,7 @@ Voici votre lien pour le groupe %@ ! Network operator + Opérateur de réseau No comment provided by engineer. @@ -4704,7 +5271,11 @@ Voici votre lien pour le groupe %@ ! Network status État du réseau - No comment provided by engineer. + alert title + + + New + token status text New Passcode @@ -4753,8 +5324,13 @@ Voici votre lien pour le groupe %@ ! New events + Nouveaux événements notification + + New group role: Moderator + No comment provided by engineer. + New in %@ Nouveautés de la %@ @@ -4770,6 +5346,10 @@ Voici votre lien pour le groupe %@ ! Nouveau rôle No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Nouveau message @@ -4782,6 +5362,7 @@ Voici votre lien pour le groupe %@ ! New server + Nouveau serveur No comment provided by engineer. @@ -4794,6 +5375,22 @@ 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 chats with members + No comment provided by engineer. + No contacts selected Aucun contact sélectionné @@ -4841,10 +5438,16 @@ Voici votre lien pour le groupe %@ ! 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 @@ -4867,6 +5470,10 @@ Voici votre lien pour le groupe %@ ! Pas l'autorisation d'enregistrer un message vocal No comment provided by engineer. + + No private routing session + alert title + No push server No push server @@ -4879,20 +5486,32 @@ Voici votre lien pour le groupe %@ ! 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. @@ -4903,6 +5522,10 @@ Voici votre lien pour le groupe %@ ! Non compatible ! No comment provided by engineer. + + Notes + No comment provided by engineer. + Nothing selected Aucune sélection @@ -4923,10 +5546,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. @@ -4949,7 +5581,9 @@ Voici votre lien pour le groupe %@ ! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -4982,6 +5616,7 @@ Nécessite l'activation d'un VPN. Only chat owners can change preferences. + Seuls les propriétaires peuvent modifier les préférences. No comment provided by engineer. @@ -5009,6 +5644,14 @@ Nécessite l'activation d'un VPN. 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. @@ -5029,6 +5672,10 @@ Nécessite l'activation d'un VPN. Seulement vous pouvez envoyer des messages éphémères. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Vous seul pouvez envoyer des messages vocaux. @@ -5054,6 +5701,10 @@ Nécessite l'activation d'un VPN. Seulement votre contact peut envoyer des messages éphémères. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Seul votre contact peut envoyer des messages vocaux. @@ -5062,7 +5713,7 @@ Nécessite l'activation d'un VPN. Open Ouvrir - No comment provided by engineer. + alert action Open Settings @@ -5071,32 +5722,70 @@ Nécessite l'activation d'un VPN. Open changes + Ouvrir les modifications No comment provided by engineer. Open chat Ouvrir le chat - No comment provided by engineer. + new chat action Open chat console Ouvrir la console du chat authentication reason + + Open clean link + alert action + Open conditions + Ouvrir les conditions No comment provided by engineer. + + Open full link + alert action + Open group Ouvrir le groupe - No comment provided by engineer. + new chat action + + + Open link? + alert title Open migration to another device Ouvrir le transfert vers un autre appareil authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… Ouverture de l'app… @@ -5104,14 +5793,17 @@ Nécessite l'activation d'un VPN. 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. @@ -5136,6 +5828,11 @@ Nécessite l'activation d'un VPN. Or to share privately + Ou à partager en privé + No comment provided by engineer. + + + Organize chats into lists No comment provided by engineer. @@ -5195,11 +5892,6 @@ Nécessite l'activation d'un VPN. Mot de passe à entrer No comment provided by engineer. - - Past member %@ - Ancien membre %@ - past/unknown group member - Paste desktop address Coller l'adresse du bureau @@ -5270,7 +5962,7 @@ Veuillez faire part de tout autre problème aux développeurs. Please check your network connection with %@ and try again. Veuillez vérifier votre connexion réseau avec %@ et réessayer. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5329,6 +6021,22 @@ 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 group moderators to review your request to join the group. + snd group event chat item + + + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + token info + Polish interface Interface en polonais @@ -5339,11 +6047,6 @@ Erreur : %@ 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 - server test error - Preserve the last message draft, with attachments. Conserver le brouillon du dernier message, avec les pièces jointes. @@ -5356,6 +6059,7 @@ Erreur : %@ Preset servers + Serveurs prédéfinis No comment provided by engineer. @@ -5375,6 +6079,11 @@ Erreur : %@ 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. @@ -5382,11 +6091,19 @@ Erreur : %@ 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 @@ -5410,7 +6127,11 @@ Erreur : %@ Private routing error Erreur de routage privé - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5462,6 +6183,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. @@ -5509,6 +6234,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Protégez vos profils de chat par un mot de passe ! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout Délai du protocole @@ -5614,11 +6343,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Reçu le : %@ copied message info - - Received file event - Événement de fichier reçu - notification - Received message Message reçu @@ -5719,11 +6443,24 @@ Activez-le dans les paramètres *Réseau et serveurs*. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5733,7 +6470,11 @@ Activez-le dans les paramètres *Réseau et serveurs*. Reject contact request Rejeter la demande de contact - No comment provided by engineer. + alert title + + + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5760,6 +6501,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Enlever l'image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Retirer le membre @@ -5775,6 +6520,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Supprimer la phrase secrète de la keychain ? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate Renégocier @@ -5790,11 +6539,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Renégocier le chiffrement ? No comment provided by engineer. - - Repeat connection request? - Répéter la demande de connexion ? - No comment provided by engineer. - Repeat download Répéter le téléchargement @@ -5805,11 +6549,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Répéter l'importation No comment provided by engineer. - - Repeat join request? - Répéter la requête d'adhésion ? - No comment provided by engineer. - Repeat upload Répéter l'envoi @@ -5820,6 +6559,50 @@ Activez-le dans les paramètres *Réseau et serveurs*. 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 sent to moderators + alert title + + + 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 @@ -5898,7 +6681,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Retry Réessayer - No comment provided by engineer. + alert action Reveal @@ -5907,12 +6690,21 @@ Activez-le dans les paramètres *Réseau et serveurs*. Review conditions + Vérifier les conditions No comment provided by engineer. - - Review later + + Review group members No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Révoquer @@ -5962,13 +6754,21 @@ Activez-le dans les paramètres *Réseau et serveurs*. Save Enregistrer alert button - chat item action +chat item action Save (and notify contacts) Enregistrer (et en informer les contacts) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Enregistrer et en informer le contact @@ -5994,6 +6794,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Enregistrer le profil du groupe No comment provided by engineer. + + Save group profile? + alert title + + + Save list + No comment provided by engineer. + Save passphrase and open chat Enregistrer la phrase secrète et ouvrir le chat @@ -6184,6 +6992,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to Envoyer les accusés de réception à @@ -6234,6 +7046,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoi de notifications No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Envoyez vos questions et idées @@ -6244,6 +7060,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoi de justificatifs No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. Envoyez-les depuis la phototèque ou des claviers personnalisés. @@ -6254,6 +7078,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoi des 100 derniers messages aux nouveaux membres. No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. L'expéditeur a annulé le transfert de fichiers. @@ -6319,11 +7147,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoyé directement No comment provided by engineer. - - Sent file event - Événement de fichier envoyé - notification - Sent message Message envoyé @@ -6361,6 +7184,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Server added to operator %@. + Serveur ajouté à l'opérateur %@. alert message @@ -6380,23 +7204,26 @@ Activez-le dans les paramètres *Réseau et serveurs*. 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 + + 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 + + Server requires authorization to upload, check password. Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe server test error @@ -6445,6 +7272,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. 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… @@ -6465,6 +7296,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Il permet de remplacer l'authentification du système. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Définir le code d'accès @@ -6480,6 +7319,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Définir la phrase secrète pour l'export No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! Choisissez un message à l'attention des nouveaux membres ! @@ -6509,7 +7352,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Share Partager alert action - chat item action +chat item action Share 1-time link @@ -6518,10 +7361,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. 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. @@ -6531,6 +7376,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Share address publicly + Partager publiquement votre adresse No comment provided by engineer. @@ -6548,6 +7394,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Partager le lien No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile Partager le profil @@ -6568,6 +7422,22 @@ Activez-le dans les paramètres *Réseau et serveurs*. Partager avec vos contacts No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + No comment provided by engineer. + Show QR code Afficher le code QR @@ -6625,6 +7495,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. 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. @@ -6659,12 +7530,23 @@ Activez-le dans les paramètres *Réseau et serveurs*. 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 address settings + Paramètres de réception automatique + alert title + + + SimpleX channel link + simplex link type + SimpleX contact address Adresse de contact SimpleX @@ -6705,6 +7587,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Protocoles SimpleX audité par Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Mode incognito simplifié @@ -6758,6 +7644,8 @@ Activez-le dans les paramètres *Réseau et serveurs*. Some servers failed the test: %@ + Certains serveurs ont échoué le test : +%@ alert message @@ -6765,6 +7653,11 @@ Activez-le dans les paramètres *Réseau et serveurs*. Quelqu'un notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. Carré, circulaire, ou toute autre forme intermédiaire. @@ -6850,6 +7743,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Arrêt du chat No comment provided by engineer. + + Storage + No comment provided by engineer. + Strong Fort @@ -6905,11 +7802,19 @@ Activez-le dans les paramètres *Réseau et serveurs*. Connexion TCP No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -6935,8 +7840,25 @@ Activez-le dans les paramètres *Réseau et serveurs*. Prendre une photo No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. + Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement. + No comment provided by engineer. + + + Tap Join group No comment provided by engineer. @@ -6977,13 +7899,17 @@ Activez-le dans les paramètres *Réseau et serveurs*. Temporary file error Erreur de fichier temporaire - No comment provided by engineer. + 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 @@ -7021,6 +7947,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + 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. @@ -7028,6 +7958,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. @@ -7047,6 +7978,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. @@ -7079,6 +8011,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le hash du message précédent est différent. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. Le message sera supprimé pour tous les membres. @@ -7104,21 +8040,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. - No comment provided by engineer. - The same conditions will apply to operator **%@**. - No comment provided by engineer. - - - The same conditions will apply to operator(s): **%@**. + 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. @@ -7129,7 +8058,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. The sender will NOT be notified L'expéditeur N'en sera PAS informé - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7138,6 +8067,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. @@ -7157,6 +8087,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. These conditions will also apply for: **%@**. + Ces conditions s'appliquent également aux : **%@**. No comment provided by engineer. @@ -7179,6 +8110,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. @@ -7214,14 +8149,8 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Ce groupe n'existe plus. No comment provided by engineer. - - This is your own SimpleX address! - Voici votre propre adresse SimpleX ! - No comment provided by engineer. - - - This is your own one-time link! - Voici votre propre lien unique ! + + 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. @@ -7229,11 +8158,23 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title Titre @@ -7261,6 +8202,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. @@ -7287,6 +8229,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s To receive + Pour recevoir No comment provided by engineer. @@ -7311,15 +8254,25 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s To send + Pour envoyer No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Pour prendre en charge les notifications push instantanées, la base de données du chat doit être migrée. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. + Pour utiliser les serveurs de **%@**, acceptez les conditions d'utilisation. No comment provided by engineer. @@ -7337,6 +8290,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s 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 @@ -7357,15 +8314,9 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s 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 : %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7414,6 +8365,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Undelivered messages + Messages non distribués No comment provided by engineer. @@ -7501,13 +8453,17 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Unmute Démute - swipe action + notification label action Unread Non lu swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. Les 100 derniers messages sont envoyés aux nouveaux membres. @@ -7533,16 +8489,44 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Mettre à jour les paramètres ? No comment provided by engineer. + + Updated conditions + No comment provided by engineer. + Updating settings will re-connect the client to all servers. La mise à jour des ces paramètres reconnectera le client à tous les serveurs. No comment provided by engineer. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat Mettre à niveau et ouvrir le chat No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors Erreurs de téléversement @@ -7575,6 +8559,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Use %@ + Utiliser %@ No comment provided by engineer. @@ -7592,6 +8577,14 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 @@ -7600,14 +8593,16 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Use current profile Utiliser le profil actuel - No comment provided by engineer. + new chat action Use for files + Utiliser pour les fichiers No comment provided by engineer. Use for messages + Utiliser pour les messages No comment provided by engineer. @@ -7625,10 +8620,14 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser l'interface d'appel d'iOS No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile Utiliser un nouveau profil incognito - No comment provided by engineer. + new chat action Use only local notifications? @@ -7652,6 +8651,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Use servers + Utiliser les serveurs No comment provided by engineer. @@ -7664,6 +8664,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser l'application d'une main. No comment provided by engineer. + + Use web port + No comment provided by engineer. + User selection Sélection de l'utilisateur @@ -7746,6 +8750,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien View conditions + Voir les conditions No comment provided by engineer. @@ -7755,6 +8760,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien View updated conditions + Voir les conditions mises à jour No comment provided by engineer. @@ -7852,6 +8858,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Le message de bienvenue est trop long No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new Quoi de neuf ? @@ -7869,6 +8879,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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. @@ -7968,17 +8979,18 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 à %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Vous êtes déjà connecté(e) via ce lien unique ! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7988,35 +9000,33 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien You are already joining the group %@. Vous êtes déjà en train de rejoindre le groupe %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Vous êtes déjà en train de rejoindre le groupe via ce lien ! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Vous êtes déjà en train de rejoindre le groupe via ce lien. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Vous êtes déjà membre de ce groupe ! Répéter la demande d'adhésion ? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Vous êtes connecté·e au serveur utilisé pour recevoir les messages de ce contact. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Vous êtes invité·e au groupe No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + 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. @@ -8032,12 +9042,9 @@ Répéter la demande d'adhésion ? Vous pouvez choisir de le modifier dans les paramètres d'apparence. No comment provided by engineer. - - You can configure operators in Network & servers settings. - 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. @@ -8082,6 +9089,7 @@ Répéter la demande d'adhésion ? 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. @@ -8124,10 +9132,14 @@ Répéter la demande d'adhésion ? Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Vous ne pouvez pas envoyer de messages ! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8139,17 +9151,12 @@ Répéter la demande d'adhésion ? 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 ! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Vous avez déjà demandé une connexion ! Répéter la demande de connexion ? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8206,6 +9213,14 @@ 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 able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Vous serez connecté·e au groupe lorsque l'appareil de l'hôte sera en ligne, veuillez attendre ou vérifier plus tard ! @@ -8231,11 +9246,6 @@ Répéter la demande de connexion ? Il vous sera demandé de vous authentifier lorsque vous démarrez ou reprenez l'application après 30 secondes en arrière-plan. No comment provided by engineer. - - You will connect to all group members. - Vous vous connecterez à tous les membres du groupe. - 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. @@ -8243,6 +9253,7 @@ Répéter la demande de connexion ? 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. @@ -8270,16 +9281,15 @@ Répéter la demande de connexion ? 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 business contact + No comment provided by engineer. + Your calls Vos appels @@ -8305,11 +9315,19 @@ Répéter la demande de connexion ? Vos profils de chat No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching 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 + 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(%@). @@ -8340,6 +9358,10 @@ Répéter la demande de connexion ? Votre profil actuel No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences Vos préférences @@ -8360,6 +9382,11 @@ Répéter la demande de connexion ? Votre profil **%@** sera partagé. No comment provided by engineer. + + 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 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. @@ -8370,11 +9397,6 @@ Répéter la demande de connexion ? Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts. alert message - - Your 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. - No comment provided by engineer. - Your random profile Votre profil aléatoire @@ -8387,6 +9409,7 @@ Répéter la demande de connexion ? Your servers + Vos serveurs No comment provided by engineer. @@ -8424,6 +9447,10 @@ Répéter la demande de connexion ? ci-dessus, puis choisissez : No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call appel accepté @@ -8431,8 +9458,13 @@ Répéter la demande de connexion ? accepted invitation + invitation acceptée chat list item title + + accepted you + rcv group event chat item + admin admin @@ -8453,6 +9485,10 @@ Répéter la demande de connexion ? négociation du chiffrement… chat item text + + all + member criteria value + all members tous les membres @@ -8468,6 +9504,10 @@ Répéter la demande de connexion ? et %lld autres événements No comment provided by engineer. + + archived report + No comment provided by engineer. + attempts tentatives @@ -8506,7 +9546,8 @@ Répéter la demande de connexion ? blocked by admin bloqué par l'administrateur - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8533,6 +9574,10 @@ Répéter la demande de connexion ? appel… call status + + can't send messages + No comment provided by engineer. + cancelled %@ annulé %@ @@ -8583,11 +9628,6 @@ Répéter la demande de connexion ? connecté No comment provided by engineer. - - connected directly - s'est connecté.e de manière directe - rcv group event chat item - connecting connexion @@ -8638,6 +9678,14 @@ Répéter la demande de connexion ? le contact %1$@ est devenu %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption Ce contact a le chiffrement de bout en bout @@ -8648,6 +9696,14 @@ Répéter la demande de connexion ? Ce contact n'a pas le chiffrement de bout en bout No comment provided by engineer. + + contact not ready + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator créateur @@ -8676,7 +9732,8 @@ Répéter la demande de connexion ? default (%@) défaut (%@) - pref value + delete after time +pref value default (no) @@ -8803,30 +9860,29 @@ Répéter la demande de connexion ? erreur No comment provided by engineer. - - event happened - event happened - No comment provided by engineer. - expired expiré No comment provided by engineer. - - for better metadata privacy. - No comment provided by engineer. - forwarded transféré No comment provided by engineer. + + group + shown on group welcome message + group deleted groupe supprimé No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated mise à jour du profil de groupe @@ -8922,11 +9978,6 @@ Répéter la demande de connexion ? italique No comment provided by engineer. - - join as %@ - rejoindre entant que %@ - No comment provided by engineer. - left a quitté @@ -8952,6 +10003,10 @@ Répéter la demande de connexion ? est connecté·e rcv group event chat item + + member has old version + No comment provided by engineer. + message message @@ -8982,20 +10037,19 @@ Répéter la demande de connexion ? modéré par %@ marked deleted chat item preview text + + moderator + member role + months mois time unit - - mute - muet - No comment provided by engineer. - never jamais - No comment provided by engineer. + delete after time new message @@ -9012,11 +10066,19 @@ Répéter la demande de connexion ? sans chiffrement de bout en bout No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text aucun texte copied message info in history + + not synchronized + No comment provided by engineer. + observer observateur @@ -9026,8 +10088,9 @@ Répéter la demande de connexion ? off off enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9069,6 +10132,18 @@ Répéter la demande de connexion ? pair-à-pair No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chiffrement e2e résistant post-quantique @@ -9084,6 +10159,10 @@ Répéter la demande de connexion ? confimation reçu… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call appel rejeté @@ -9104,6 +10183,10 @@ Répéter la demande de connexion ? suppression de l'adresse de contact profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture suppression de la photo de profil @@ -9114,10 +10197,35 @@ Répéter la demande de connexion ? vous a retiré rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect + demande à se connecter chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved enregistré @@ -9153,11 +10261,6 @@ Répéter la demande de connexion ? code de sécurité modifié chat item text - - send direct message - envoyer un message direct - No comment provided by engineer. - server queue info: %1$@ @@ -9217,11 +10320,6 @@ dernier message reçu : %2$@ statut inconnu No comment provided by engineer. - - unmute - démuter - No comment provided by engineer. - unprotected non protégé @@ -9312,10 +10410,9 @@ dernier message reçu : %2$@ vous No comment provided by engineer. - - you are invited to group - vous êtes invité·e au groupe - No comment provided by engineer. + + you accepted this member + snd group event chat item you are observer @@ -9386,7 +10483,7 @@ dernier message reçu : %2$@
- +
@@ -9423,7 +10520,7 @@ dernier message reçu : %2$@
- +
@@ -9445,34 +10542,38 @@ dernier message reçu : %2$@
- +
%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 - - New messages in %d chats - notification body -
- +
@@ -9494,7 +10595,7 @@ dernier message reçu : %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json index 22d271b92e..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 08f46bb056..f94d6cefd8 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -2876,8 +2876,8 @@ Available in v5.1 Polish interface No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -3252,12 +3252,12 @@ Available in v5.1 Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. server test error @@ -3569,8 +3569,8 @@ 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. + + Your profile is stored on your device and only shared with your contacts. No comment provided by engineer. @@ -4466,7 +4466,7 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -4643,8 +4643,8 @@ SimpleX servers cannot see your profile. yes pref value - - you are invited to group + + You are invited to group No comment provided by engineer. @@ -5405,7 +5405,7 @@ SimpleX servers cannot see your profile. Apply to - החל ל + החל על Background @@ -5579,6 +5579,112 @@ This is your own one-time link! File error שגיאה בקובץ + + Corner + קצוות + + + Use for messages + שימוש בהודעות + + + Pending + ממתין + + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + הגן על כתובת ה-IP שלך מפני ממסרי הודעות שנבחרו על ידי אנשי הקשר שלך. +הפעל בהגדרות *רשת ושרתים*. + + + To send + לשליחה + + + Do NOT send messages directly, even if your or destination server does not support private routing. + אל תשלח הודעות ישירות, גם אם שרת היעד שלך או שרת היעד אינו תומך בניתוב פרטי. + + + Servers statistics will be reset - this cannot be undone! + סטטיסטיקות השרתים יאופסו - לא ניתן לבטל פעולה זו! + + + Detailed statistics + סטטיסטיקות מפורטות + + + Reachable chat toolbar + סרגל כלים נגיש לצ'אט + + + Configure server operators + הגדרת מפעילי שרת + + + Private chats, groups and your contacts are not accessible to server operators. + צ'אטים פרטיים, קבוצות ואנשי הקשר שלך אינם נגישים למפעילי השרת. + + + pending + ממתין + + + Save and reconnect + שמור וחבר מחדש + + + Customizable message shape. + צורת הודעה הניתנת להתאמה אישית. + + + All data is kept private on your device. + כל הנתונים נשמרים פרטיים במכשיר שלך. + + + Downloading link details + הורדת פרטי קישור + + + Message shape + צורת ההודעה + + + No chats in list %@ + אין צ'אטים ברשימה %@ + + + Privacy policy and conditions of use. + מדיניות פרטיות ותנאי שימוש. + + + pending review + ממתין לסקירה + + + Save list + שמור רשימה + + + pending approval + ממתין לאישור + + + %@ downloaded + %@ יורד + + + %@ server + %@ שרת + + + %@ connected + %@ מחובר + + + %@ uploaded + %@ הועלה +
@@ -5627,4 +5733,12 @@ This is your own one-time link!
+ + + + New messages + הודעות חדשות + + +
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 2fd96e3492..2aa945f603 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,12 +173,14 @@ %lldw No comment provided by engineer. - + ( + ( No comment provided by engineer. - + ) + ) No comment provided by engineer. @@ -248,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 @@ -1518,16 +1520,19 @@ 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. @@ -1688,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. @@ -2036,8 +2043,8 @@ We will be adding server redundancy to prevent lost messages. 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 + + Fingerprint in server address does not match certificate. server test error @@ -2364,8 +2371,8 @@ 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 requires authorization to create queues, check password. server test error @@ -2612,8 +2619,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. No comment provided by engineer. @@ -3152,8 +3159,9 @@ SimpleX servers cannot see your profile. \_italic_ No comment provided by engineer. - + \`a + b` + \`a + b` No comment provided by engineer. @@ -3404,7 +3412,7 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -3569,8 +3577,8 @@ SimpleX servers cannot see your profile. yes pref value - - you are invited to group + + You are invited to group No comment provided by engineer. @@ -3613,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 231c33523d..22d223ffd9 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (másolható) @@ -39,7 +27,7 @@ ## In reply to - ## Válaszul erre: + ## Válaszul erre copied message info @@ -104,7 +92,7 @@ %@ is not verified - %@ nem hitelesített + %@ nincs hitelesítve No comment provided by engineer. @@ -164,7 +152,7 @@ %d file(s) failed to download. - %d fájlt nem sikerült letölteni. + Nem sikerült letölteni %d fájlt. forward confirmation reason @@ -202,6 +190,11 @@ %d mp time interval + + %d seconds(s) + %d másodperc + delete after time + %d skipped message(s) %d üzenet kihagyva @@ -224,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. @@ -254,7 +247,7 @@ %lld messages marked deleted - %lld törlésre megjelölt üzenet + %lld üzenet megjelölve törlésre No comment provided by engineer. @@ -272,24 +265,19 @@ %lld új kezelőfelületi nyelv No comment provided by engineer. - - %lld second(s) - %lld másodperc - No comment provided by engineer. - %lld seconds - %lld másodperc + %lld mp No comment provided by engineer. %lldd - %lldd + %lldnap No comment provided by engineer. %lldh - %lldh + %lldó No comment provided by engineer. @@ -299,27 +287,27 @@ %lldm - %lldm + %lldp No comment provided by engineer. %lldmth - %lldmth + %lldhónap 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. @@ -327,11 +315,6 @@ %u üzenet kihagyva. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (új) @@ -342,14 +325,9 @@ (ez az eszköz: v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. - **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:** ú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. @@ -359,7 +337,7 @@ **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 megosztásra kerül a SimpleX Chat-kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van. + **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. @@ -369,17 +347,17 @@ **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 az ismerőseitő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. - **Megjegyzés:** 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 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 elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik. + **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. @@ -394,7 +372,7 @@ **Warning**: the archive will be removed. - **Figyelmeztetés:** az archívum eltávolításra kerül. + **Figyelmeztetés:** az archívum el lesz távolítva. No comment provided by engineer. @@ -412,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). @@ -431,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. @@ -439,8 +412,8 @@ - optionally notify deleted contacts. - profile names with spaces. - and more! - - értesíti az ismerősöket a törlésről (nem kötelező) -- profil nevek 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.
@@ -448,16 +421,11 @@ - voice messages up to 5 minutes. - custom time to disappear. - editing history. - - 5 perc hosszúságú hangüzenetek. -- egyedi üzenet-eltűnési időkorlát. + - legfeljebb 5 perc hosszúságú hangüzenetek. +- egyéni időkorlát beállítása az üzenetek eltűnéséhez. - előzmények szerkesztése. No comment provided by engineer.
- - . - . - No comment provided by engineer. - 0 sec 0 mp @@ -471,7 +439,8 @@ 1 day 1 nap - time interval + delete after time +time interval 1 hour @@ -486,21 +455,28 @@ 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ó-hivatkozás + 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ó-hivatkozás csak *egyetlen ismerőssel használható* - személyesen vagy bármilyen üzenetküldőn keresztül megosztható. + Az egyszer használható meghívó egy hivatkozás és *csak egyetlen partnerrel használható* – személyesen vagy bármilyen üzenetváltó-alkalmazáson keresztül megosztható. No comment provided by engineer. @@ -518,11 +494,6 @@ 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> @@ -532,29 +503,29 @@ 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**. - **Az összes csevegési profiljához az alkalmazásban** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva. + **Az összes csevegési profiljához az alkalmazásban** külön TCP-kapcsolat 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. - **Az összes ismerősé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. + **Az összes partneréhez és csoporttaghoz** külön TCP-kapcsolat lesz használva. +**Megjegyzés:** ha sok kapcsolata van, akkor 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. @@ -564,17 +535,17 @@ 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?? + Megszakítja a cím módosítását? No comment provided by engineer. About SimpleX Chat - SimpleX Chat névjegye + A SimpleX Chat névjegye No comment provided by engineer. @@ -584,15 +555,26 @@ Accent - Kiemelés + Kiemelőszín No comment provided by engineer. Accept Elfogadás accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Befogadás tagként + alert action + + + Accept as observer + Befogadás megfigyelőként + alert action Accept conditions @@ -601,19 +583,29 @@ Accept connection request? - Kapcsolatkérés elfogadása? + Elfogadja a kapcsolódási kérést? No comment provided by engineer. + + Accept contact request + Partneri kapcsolatkérés elfogadása + alert title + Accept contact request from %@? - Elfogadja %@ kapcsolatkérését? + Elfogadja %@ partneri kapcsolatkérését? notification body Accept incognito - Fogadás inkognitóban - accept contact request via notification - swipe action + Elfogadás inkognitóban + alert action +swipe action + + + Accept member + Tag befogadása + alert title Accepted conditions @@ -622,14 +614,19 @@ Acknowledged - Nyugtázva + Visszaigazolt No comment provided by engineer. Acknowledgement errors - Nyugtázott hibák + Visszaigazolási hibák No comment provided by engineer. + + Active + Aktív + token status text + Active connections Aktív kapcsolatok száma @@ -637,7 +634,7 @@ 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 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. @@ -645,6 +642,16 @@ Barátok hozzáadása No comment provided by engineer. + + Add list + Lista hozzáadása + No comment provided by engineer. + + + Add message + Üzenet hozzáadása + placeholder for sending contact request + Add profile Profil hozzáadása @@ -662,7 +669,7 @@ Add team members - Csapattagok hozzáadása + Munkatársak hozzáadása No comment provided by engineer. @@ -670,6 +677,11 @@ 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 @@ -677,12 +689,12 @@ Add your team members to the conversations. - Adja hozzá csapattagjait a beszélgetésekhez. + 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 + Hozzáadott fájl- és médiakiszolgálók No comment provided by engineer. @@ -692,17 +704,17 @@ Additional accent - További kiemelés + További kiemelőszín No comment provided by engineer. Additional accent 2 - További kiemelés 2 + További kiemelőszín 2 No comment provided by engineer. Additional secondary - További másodlagos + További másodlagos szín No comment provided by engineer. @@ -712,12 +724,12 @@ 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 kliens megszakítja a cím módosítását, és a régi fogadási címet fogja használni. No comment provided by engineer. Address or 1-time link? - Cím vagy egyszer használható meghívó-hivatkozás? + Cím vagy egyszer használható meghívó? No comment provided by engineer. @@ -732,7 +744,7 @@ Admins can create the links to join groups. - Az adminisztrátorok 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. @@ -745,6 +757,11 @@ Speciális beállítások No comment provided by engineer. + + All + Összes + No comment provided by engineer. + All app data is deleted. Az összes alkalmazásadat törölve. @@ -752,17 +769,22 @@ All chats and messages will be deleted - this cannot be undone! - Az összes 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 az összes 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 biztonságban van az eszközén. + Az összes adat privát módon van tárolva az eszközén. No comment provided by engineer. @@ -772,22 +794,22 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. - Az összes üzenetet és fájlt **végpontok közötti titkosítással** küldi, a közvetlen üzenetekben pedig kvantumrezisztens biztonsággal. + Az összes üzenet és fájl **végpontok közötti titkosítással**, a közvetlen üzenetek továbbá kvantumbiztos titkosítással is rendelkeznek. No comment provided by engineer. All messages will be deleted - this cannot be undone! - Az összes üzenet tö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. - Az összes ü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! - Az összes új üzenet elrejtésre kerül tőle: %@! + %@ összes új üzenete el lesz rejtve! No comment provided by engineer. @@ -795,19 +817,29 @@ Ö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. - Az összes ismerősével kapcsolatban 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. - Az 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. - Az összes ismerőse, -beszélgetése és -fájlja biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra. + 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. @@ -817,17 +849,17 @@ Allow calls only if your contact allows them. - A hívások kezdeményezése csak abban az esetben van engedélyezve, ha az 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? - Hívások engedélyezése? + Engedélyezi a hívásokat? No comment provided by engineer. Allow disappearing messages only if your contact allows it to you. - Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha az 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. @@ -835,19 +867,24 @@ Visszafejlesztés engedélyezése No comment provided by engineer. + + Allow files and media only if your contact allows them. + A fájlok és a médiatartalmak küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) - Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha 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) No comment provided by engineer. Allow message reactions only if your contact allows them. - Az üzenetreakciók küldése csak abban az esetben van engedélyezve, 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. 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. @@ -867,7 +904,12 @@ 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. @@ -877,47 +919,52 @@ Allow to send files and media. - Fájlok és médiatartalmak 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. - A hangüzenetek küldése csak abban az esetben van engedélyezve, ha az 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. - Az üzenetreakciók küldése engedélyezve van az ismerősei számára. + 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 az ismerősei 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) - Az elküldött üzenetek végleges törlése engedélyezve van 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) No comment provided by engineer. Allow your contacts to send disappearing messages. - Az eltűnő üzenetek küldésének 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. + No comment provided by engineer. + + + Allow your contacts to send files and media. + A fájlok és a médiatartalmak küldése engedélyezve van 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 az ismerősei számára. + A hangüzenetek küldése engedélyezve van a partnerei számára. No comment provided by engineer. @@ -928,12 +975,12 @@ Already connecting! Kapcsolódás folyamatban! - No comment provided by engineer. + new chat sheet title Already joining the group! A csatlakozás folyamatban van a csoporthoz! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -942,14 +989,19 @@ Always use relay - Mindig használjon közvetítő-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 @@ -962,7 +1014,7 @@ App build: %@ - Az alkalmazás build száma: %@ + Alkalmazás összeállítási száma: %@ No comment provided by engineer. @@ -975,6 +1027,11 @@ Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével). No comment provided by engineer. + + App group: + Alkalmazáscsoport: + No comment provided by engineer. + App icon Alkalmazásikon @@ -982,12 +1039,12 @@ App passcode - Alkalmazás jelkód + Alkalmazásjelkó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. + Az alkalmazásjelkód helyettesítve lesz egy önmegsemmisítő jelkóddal. No comment provided by engineer. @@ -997,12 +1054,12 @@ App version - Alkalmazás verzió + Alkalmazás verziója No comment provided by engineer. App version: v%@ - Alkalmazás verzió: v%@ + Alkalmazás verziója: v%@ No comment provided by engineer. @@ -1017,7 +1074,22 @@ Apply to - Alkalmazás erre + Használat ehhez + 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. @@ -1027,12 +1099,27 @@ Archive contacts to chat later. - Az ismerősök archiválása a későbbi csevegéshez. + 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 ismerősök + Archivált partnerek No comment provided by engineer. @@ -1042,7 +1129,7 @@ Attach - Csatolás + Mellékelés No comment provided by engineer. @@ -1057,12 +1144,12 @@ Audio/video calls - Hang-/videóhívások + Hang- és videóhívások chat feature Audio/video calls are prohibited. - A hívások kezdeményezése le van tiltva ebben a csevegésben. + A hívások kezdeményezése le van tiltva. No comment provided by engineer. @@ -1092,7 +1179,7 @@ Auto-accept contact requests - Kapcsolatkérések automatikus elfogadása + Partneri kapcsolatkérések automatikus elfogadása No comment provided by engineer. @@ -1100,11 +1187,6 @@ Képek automatikus elfogadása No comment provided by engineer. - - Auto-accept settings - Beállítások automatikus elfogadása - alert title - Back Vissza @@ -1117,17 +1199,17 @@ Bad desktop address - Hibás számítógép cím + Hibás a számítógép címe No comment provided by engineer. Bad message ID - Téves üzenet ID + Hibás az üzenet azonosítója No comment provided by engineer. Bad message hash - Hibás az üzenet hasító értéke + Hibás az üzenet kivonata No comment provided by engineer. @@ -1137,7 +1219,12 @@ 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. @@ -1147,12 +1234,12 @@ Better messages - Jobb üzenetek + Továbbfejlesztett üzenetek No comment provided by engineer. Better networking - Jobb hálózatkezelés + Továbbfejlesztett hálózatkezelés No comment provided by engineer. @@ -1160,6 +1247,11 @@ 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 ✅ @@ -1170,6 +1262,16 @@ Továbbfejlesztett felhasználói élmény No comment provided by engineer. + + Bio + Névjegy + No comment provided by engineer. + + + Bio too large + A névjegy túl hosszú + alert title + Black Fekete @@ -1182,7 +1284,7 @@ Block for all - Letiltás az összes tag számára + Letiltás No comment provided by engineer. @@ -1192,22 +1294,22 @@ Block member - Tag letiltása + Letiltás No comment provided by engineer. Block member for all? - Az összes tag számára letiltja ezt a tagot? + Az összes tag számára letiltja a tagot? No comment provided by engineer. Block member? - Tag letiltása? + Letiltja a tagot? No comment provided by engineer. Blocked by admin - Az adminisztrátor letiltotta + Letiltva az adminisztrátor által No comment provided by engineer. @@ -1220,14 +1322,19 @@ Médiatartalom elhomályosítása No comment provided by engineer. + + Bot + Bot + 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. @@ -1240,6 +1347,11 @@ Mindkét fél küldhet eltűnő üzeneteket. No comment provided by engineer. + + Both you and your contact can send files and media. + Mindkét fél küldhet fájlokat és médiatartalmakat. + No comment provided by engineer. + Both you and your contact can send voice messages. Mindkét fél küldhet hangüzeneteket. @@ -1260,11 +1372,30 @@ Üzleti csevegések No comment provided by engineer. + + Business connection + Üzleti kapcsolat + 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: +- send only legal content in public groups. +- respect other users – no spam. + A SimpleX Chat használatával Ön elfogadja, hogy: +- csak elfogadott tartalmakat tesz közzé a nyilvános csoportokban. +- tiszteletben tartja a többi felhasználót, és nem küld kéretlen tartalmat senkinek. + No comment provided by engineer. + Call already ended! A hívás már befejeződött! @@ -1287,7 +1418,7 @@ Can't call contact - Nem lehet felhívni az ismerőst + Nem lehet felhívni a partnert No comment provided by engineer. @@ -1295,14 +1426,19 @@ Nem lehet felhívni a tagot No comment provided by engineer. + + Can't change profile + Nem lehet módosítani a profilt + alert title + Can't invite contact! - Nem lehet meghívni az ismerőst! + Nem lehet meghívni a partnert! No comment provided by engineer. Can't invite contacts! - Nem lehet meghívni az ismerősöket! + Nem lehet meghívni a partnereket! No comment provided by engineer. @@ -1314,7 +1450,8 @@ Cancel Mégse alert action - alert button +alert button +new chat action Cancel migration @@ -1338,7 +1475,7 @@ 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. + Kapacitás túllépés – a címzett nem kapta meg a korábban elküldött üzeneteket. snd error text @@ -1348,59 +1485,64 @@ 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 megváltoztatása + 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 @@ -1415,7 +1557,7 @@ Chat already exists! A csevegés már létezik! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1454,17 +1596,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 ezt az adatbázist már használta egy másik eszközön, akkor a csevegés elindítása előtt vissza kell állítania azt. No comment provided by engineer. Chat list - Csevegőlista + Csevegési lista No comment provided by engineer. @@ -1479,7 +1621,7 @@ Chat preferences were changed. - A csevegési beállítások megváltoztak. + A csevegési beállítások módosultak. alert message @@ -1494,12 +1636,27 @@ Chat will be deleted for all members - this cannot be undone! - A csevegés minden tag számára törlésre kerül - ezt a műveletet nem lehet visszavonni! + 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örlésre kerül az Ön számára - ezt a műveletet nem lehet visszavonni! + A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza! + No comment provided by engineer. + + + Chat with admins + Csevegés az adminisztrátorokkal + chat toolbar + + + Chat with member + Csevegés a taggal + No comment provided by engineer. + + + Chat with members before they join. + Csevegés a tagokkal mielőtt csatlakoznának. No comment provided by engineer. @@ -1507,6 +1664,11 @@ Csevegések No comment provided by engineer. + + Chats with members + Csevegés a tagokkal + No comment provided by engineer. + Check messages every 20 min. Üzenetek ellenőrzése 20 percenként. @@ -1544,17 +1706,17 @@ Chunks deleted - Törölt fájltöredékek + Törölt töredékek No comment provided by engineer. Chunks downloaded - Letöltött fájltöredékek + Letöltött töredékek No comment provided by engineer. Chunks uploaded - Feltöltött fájltöredékek + Feltöltött töredékek No comment provided by engineer. @@ -1569,12 +1731,22 @@ Clear conversation? - Üzenetek 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 kiürítése? + Kiüríti a privát jegyzeteket? No comment provided by engineer. @@ -1592,14 +1764,19 @@ 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ökével. + Biztonsági kódok összehasonlítása a partnerekével. No comment provided by engineer. @@ -1609,7 +1786,7 @@ Conditions accepted on: %@. - Feltételek elfogadva ekkor: %@. + Feltételek elfogadásának ideje: %@. No comment provided by engineer. @@ -1625,17 +1802,7 @@ Conditions of use Használati feltételek - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltető számára. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - A feltételek el lesznek fogadva a következő üzemeltető(k) számára: **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1644,12 +1811,12 @@ Conditions will be accepted on: %@. - A feltételek ekkor lesznek elfogadva: %@. + 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 elfogadásra kerülnek az engedélyezett üzemeltető számára: %@. + 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. @@ -1657,6 +1824,11 @@ 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 @@ -1669,7 +1841,7 @@ Confirm contact deletion? - Biztosan törli az ismerőst? + Biztosan törli a partnert? No comment provided by engineer. @@ -1707,6 +1879,11 @@ Feltöltés megerősítése No comment provided by engineer. + + Confirmed + Megerősítve + token status text + Connect Kapcsolódás @@ -1717,9 +1894,9 @@ Kapcsolódás automatikusan No comment provided by engineer. - - Connect incognito - Kapcsolódás inkognitóban + + Connect faster! 🚀 + Gyorsabb kapcsolódás! 🚀 No comment provided by engineer. @@ -1729,47 +1906,42 @@ Connect to your friends faster. - Kapcsolódjon gyorsabban az ismerőseihez. - No comment provided by engineer. - - - Connect to yourself? - Kapcsolódás saját magához? + Kapcsolódjon gyorsabban a partnereihez. No comment provided by engineer. Connect to yourself? This is your own SimpleX address! - Kapcsolódás saját magához? -Ez az Ön SimpleX-címe! - No comment provided by engineer. + Kapcsolódik saját magához? +Ez a saját SimpleX-címe! + new chat sheet title Connect to yourself? This is your own one-time link! - Kapcsolódás saját magához? -Ez az Ön egyszer használható meghívó-hivatkozása! - No comment provided by engineer. + Kapcsolódik saját magához? +Ez a saját egyszer használható meghívója! + new chat sheet title Connect via contact address Kapcsolódás a kapcsolattartási címen keresztül - No comment provided by engineer. + new chat sheet title Connect via link Kapcsolódás egy hivatkozáson keresztül - No comment provided by engineer. + new chat sheet title Connect via one-time link - Kapcsolódás egyszer használható meghívó-hivatkozáson keresztül - No comment provided by engineer. + Kapcsolódás egyszer használható meghívón keresztül + new chat sheet title Connect with %@ Kapcsolódás a következővel: %@ - No comment provided by engineer. + new chat action Connected @@ -1808,7 +1980,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Connecting to contact, please wait or check later! - Kapcsolódás az ismerőshöz, várjon vagy ellenőrizze később! + Kapcsolódás a partnerhez, várjon vagy ellenőrizze később! No comment provided by engineer. @@ -1826,16 +1998,33 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Kapcsolatok- és kiszolgálók állapotának megjelenítése. No comment provided by engineer. + + Connection blocked + A kapcsolat le van tiltva + No comment provided by engineer. + Connection error Kapcsolódási hiba - No comment provided by engineer. + alert title Connection error (AUTH) 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 @@ -1843,7 +2032,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Connection request sent! - Kapcsolatkérés elküldve! + Kapcsolódá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. @@ -1859,7 +2053,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Connection timeout Időtúllépés kapcsolódáskor - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1873,32 +2067,32 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Contact allows - Ismerős engedélyezi + Partner engedélyezi No comment provided by engineer. Contact already exists - Az ismerős már létezik + A partner már létezik No comment provided by engineer. Contact deleted! - Ismerős törölve! + 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 deleted. - Törölt ismerős. + Törölt partner. No comment provided by engineer. @@ -1908,24 +2102,34 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Contact preferences - Ismerős beállításai + Partnerbeállítások + No comment provided by engineer. + + + Contact requests from groups + Partneri kapcsolatkérések a csoportokból No comment provided by engineer. Contact will be deleted - this cannot be undone! - Az ismerős törlésre fog kerülni - ez a művelet nem vonható vissza! + 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ősei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat. + 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 @@ -1948,7 +2152,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Core version: v%@ - Alapverziószám: v%@ + Fő verzió: v%@ No comment provided by engineer. @@ -1958,7 +2162,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Correct name to %@? - Név javítása erre: %@? + Helyesbíti a nevet a következőre: %@? No comment provided by engineer. @@ -1968,7 +2172,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Create 1-time link - Egyszer használható meghívó-hivatkozás létrehozása + Egyszer használható meghívó létrehozása No comment provided by engineer. @@ -1978,7 +2182,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Create a group using a random profile. - Csoport létrehozása véletlenszerűen létrehozott profillal. + Csoport létrehozása véletlenszerű profillal. No comment provided by engineer. @@ -2001,6 +2205,11 @@ Ez az Ön egyszer használható meghívó-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 a [számítógép-alkalmazásban](https://simplex.chat/downloads/). 💻 @@ -2013,17 +2222,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Create queue - Sorbaállítás létrehozása + Várólista létrehozása server test step - - Create secret group - Titkos csoport létrehozása + + Create your address + Saját cím létrehozása No comment provided by engineer. Create your profile - Saját profil létrehozása + Profil létrehozása No comment provided by engineer. @@ -2033,12 +2242,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Created at - Létrehozva ekkor: + Létrehozva No comment provided by engineer. Created at: %@ - Létrehozva ekkor: %@ + Létrehozva: %@ copied message info @@ -2058,7 +2267,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 ezen a hivatkozáson keresztül vizsgálhatja felül: + 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: No comment provided by engineer. @@ -2073,17 +2282,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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. - Testreszabható üzenetbuborékok. + Személyre szabható üzenetbuborékok. No comment provided by engineer. @@ -2113,7 +2322,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Database IDs and Transport isolation option. - Adatbázis-azonosítók és átvitel-izolációs beállítások. + Adatbázis-azonosítók és átvitelelkülönítési beállítások. No comment provided by engineer. @@ -2129,14 +2338,14 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Database encryption passphrase will be updated and stored in the keychain. - Az adatbázis titkosítási jelmondata frissítve lesz és a kulcstartóban kerül tárolásra. + 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. - Az datbázis titkosítási jelmondata frissítve lesz. + Az adatbázis titkosítási jelmondata frissítve lesz. No comment provided by engineer. @@ -2147,12 +2356,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Database is encrypted using a random passphrase, you can change it. - Az adatbázis egy véletlenszerű jelmondattal van titkosítva, ami 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őtt változtassa meg. + Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt módosítsa. No comment provided by engineer. @@ -2167,7 +2376,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Database passphrase is different from saved in the keychain. - Az adatbázis jelmondata eltér a kulcstartóban mentettől. + Az adatbázis jelmondata nem egyezik a kulcstartóba mentettől. No comment provided by engineer. @@ -2183,20 +2392,20 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Database will be encrypted and the passphrase stored in the keychain. - Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstartóban kerül tárolásra. + 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 átköltöztetésre kerül + Az adatbázis az alkalmazás újraindításakor lesz átköltöztetve No comment provided by engineer. @@ -2211,19 +2420,18 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Decryption error - Titkosítás visszafejtési hiba + Titkosítás-visszafejtési hiba message decrypt error item Delete Törlés alert action - chat item action - swipe action +swipe action Delete %lld messages of members? - Tagok %lld üzenetének törlése? + Törli a tagok %lld üzenetét? No comment provided by engineer. @@ -2238,7 +2446,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete address? - Cím törlése? + Törli a címet? No comment provided by engineer. @@ -2253,7 +2461,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete and notify contact - Törlés, és az ismerős értesítése + Törlés, és a partner értesítése No comment provided by engineer. @@ -2261,6 +2469,11 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Csevegés törlése No comment provided by engineer. + + Delete chat messages from your device. + Csevegési üzenetek törlése az eszközről. + No comment provided by engineer. + Delete chat profile Csevegési profil törlése @@ -2268,12 +2481,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete chat profile? - Csevegési profil törlése? + Törli a csevegési profilt? No comment provided by engineer. + + Delete chat with member? + Törli a taggal való csevegést? + alert title + Delete chat? - Csevegés törlése? + Törli a csevegést? No comment provided by engineer. @@ -2283,12 +2501,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete contact - Ismerős törlése + Partner törlése No comment provided by engineer. Delete contact? - Ismerős törlése? + Törli a partnert? No comment provided by engineer. @@ -2308,7 +2526,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete files and media? - Fájlok és a médiatartalmak törlése? + Törli a fájlokat és a médiatartalmakat? No comment provided by engineer. @@ -2333,7 +2551,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete group? - Csoport törlése? + Törli a csoportot? No comment provided by engineer. @@ -2343,28 +2561,33 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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ének 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 @@ -2378,7 +2601,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete old database? - Régi adatbázis törlése? + Törli a régi adatbázist? No comment provided by engineer. @@ -2388,7 +2611,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete pending connection? - Függőben lévő ismerőskérelem törlése? + Törli a függőben lévő kapcsolatot? No comment provided by engineer. @@ -2398,9 +2621,14 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete queue - Sorbaállítás törlése + Várólista 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. @@ -2408,7 +2636,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete user profile? - Felhasználói profil törlése? + Törli a felhasználói profilt? No comment provided by engineer. @@ -2423,12 +2651,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 @@ -2448,7 +2676,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delivery receipts are disabled! - A kézbesítési jelentések ki vannak kapcsolva! + A kézbesítési jelentések le vannak tiltva! No comment provided by engineer. @@ -2456,11 +2684,21 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Kézbesítési jelentések! No comment provided by engineer. + + Deprecated options + Elavult beállítások + No comment provided by engineer. + Description Leírás No comment provided by engineer. + + Description too large + A leírás túl hosszú + alert title + Desktop address Számítógép címe @@ -2468,7 +2706,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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. + A számítógép-alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással. No comment provided by engineer. @@ -2478,17 +2716,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Destination server address of %@ is incompatible with forwarding server %@ settings. - A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival. + A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbítókiszolgáló beállításaival. No comment provided by engineer. Destination server error: %@ - Célkiszolgáló hiba: %@ + 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. + A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbítókiszolgálóval. No comment provided by engineer. @@ -2533,7 +2771,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Different names, avatars and transport isolation. - Különböző nevek, profilképek és átvitel-izoláció. + Különböző nevek, profilképek és átvitelelkülönítés. No comment provided by engineer. @@ -2548,12 +2786,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Direct messages between members are prohibited. - A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. + 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) + Letiltás (egyéni beállítások megtartása) No comment provided by engineer. @@ -2561,9 +2799,19 @@ Ez az Ön egyszer használható meghívó-hivatkozá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 az összes tag számára + Letiltás No comment provided by engineer. @@ -2588,17 +2836,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Disappearing messages are prohibited. - Az eltűnő üzenetek küldése le van tiltva ebben a csoportban. + 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 @@ -2608,12 +2856,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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. @@ -2623,17 +2871,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + 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. + NE használjon privát útválasztást. No comment provided by engineer. @@ -2643,12 +2891,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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. + Ne használja a hitelesítési adatokat proxyval. + No comment provided by engineer. + + + Documents: + Dokumentumok: No comment provided by engineer. @@ -2661,9 +2914,19 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 + alert action + + + Done + Kész No comment provided by engineer. @@ -2675,7 +2938,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Download Letöltés alert button - chat item action +chat item action Download errors @@ -2719,7 +2982,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Duplicate display name! - Duplikált megjelenített név! + Duplikált megjelenítendő név! No comment provided by engineer. @@ -2739,7 +3002,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Edit group profile - A csoport profiljának szerkesztése + Csoportprofil szerkesztése + No comment provided by engineer. + + + Empty message! + Üres üzenet! No comment provided by engineer. @@ -2749,12 +3017,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Enable (keep overrides) - Engedélyezés (felülírások megtartásával) + Engedélyezés (egyéni beállítások megtartása) No comment provided by engineer. - - Enable Flux - Flux engedélyezése + + 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. @@ -2769,12 +3037,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 - Kamera hozzáférés engedélyezése + Kamera-hozzáférés engedélyezése + No comment provided by engineer. + + + Enable disappearing messages by default. + Eltűnő üzenetek engedélyezése alapértelmezetten. No comment provided by engineer. @@ -2789,7 +3062,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Enable instant notifications? - Azonnali értesítések engedélyezése? + Engedélyezi az azonnali értesítéseket? No comment provided by engineer. @@ -2804,7 +3077,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Enable periodic notifications? - Időszakos értesítések engedélyezése? + Engedélyezi az időszakos értesítéseket? No comment provided by engineer. @@ -2824,7 +3097,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Enabled for - Számukra engedélyezve: + Számukra engedélyezve No comment provided by engineer. @@ -2834,7 +3107,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Encrypt database? - Adatbázis titkosítása? + Titkosítja az adatbázist? No comment provided by engineer. @@ -2844,7 +3117,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Encrypt stored files & media - A tárolt fájlok- és a médiatartalmak titkosítása + Tárolt fájlok és médiatartalmak titkosítása No comment provided by engineer. @@ -2859,7 +3132,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Encrypted message: app is stopped - Titkosított üzenet: az alkalmazás leállt + Titkosított üzenet: az alkalmazás megállt notification @@ -2889,17 +3162,22 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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. @@ -2909,22 +3187,22 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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. @@ -2939,12 +3217,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Enter welcome message… - Üdvözlőüzenet megadása… + Adja meg az üdvözlőüzenetet… placeholder Enter welcome message… (optional) - Üdvözlőüzenet megadása… (nem kötelező) + Adja meg az üdvözlőüzenetet… (nem kötelező) placeholder @@ -2959,252 +3237,317 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error aborting address change - Hiba a cím 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 a feltételek elfogadásakor + Hiba történt a feltételek elfogadásakor alert title Error accepting contact request - Hiba történt a kapcsolatkérés elfogadásakor + Hiba történt a partneri kapcsolatkérés elfogadásakor No comment provided by engineer. + + Error accepting member + Hiba a tag befogadásakor + alert title + 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 a kiszolgáló hozzáadásakor + Hiba történt a kiszolgáló hozzáadásakor alert title + + Error adding short link + Hiba történt a rövid hivatkozás hozzáadásakor + No comment provided by engineer. + Error changing address - Hiba a cím megváltoztatásakor + Hiba történt a cím módosításakor No comment provided by engineer. + + Error changing chat profile + Hiba a csevegési profil módosításakor + alert title + Error changing connection profile - Hiba a kapcsolati profilra való váltáskor + 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 - No comment provided by engineer. + Hiba történt a beállítás módosításakor + alert title Error changing to incognito! - Hiba az inkognitóprofilra való váltáskor! + 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 a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később. - No comment provided by engineer. + Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + Hiba történt a kapcsolódáskor ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál: %@ + subscription status explanation Error creating address - Hiba a cím 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 csoporthivatkozás 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 + Hiba a csevegés törlésekor + alert title + Error deleting chat database - Hiba a csevegési adatbázis törlésekor - No comment provided by engineer. + Hiba történt a csevegési adatbázis törlésekor + alert title Error deleting chat! - Hiba a csevegés törlésekor! - No comment provided by engineer. + Hiba történt a csevegés törlésekor! + alert title Error deleting connection - Hiba a kapcsolat 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 - No comment provided by engineer. + Hiba történt az adatbázis törlésekor + alert title Error deleting old database - Hiba a régi adatbázis törlésekor - No comment provided by engineer. + Hiba történt a régi adatbázis törlésekor + alert title 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ó-profil törlésekor + Hiba történt a felhasználói 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 - No comment provided by engineer. + Hiba történt a csevegési adatbázis exportálásakor + alert title Error exporting theme: %@ - Hiba a téma exportálásakor: %@ + 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 - No comment provided by engineer. + Hiba történt a csevegési adatbázis importálásakor + alert title 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 + Hiba történt a kiszolgálók betöltésekor alert title Error migrating settings - Hiba a beallítások átköltöztetésekor + 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 opening group + Hiba történt a csoport 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 a kiszolgálóhoz való újrakapcsolódáskor + Hiba történt a kiszolgálóhoz való újrakapcsolódáskor No comment provided by engineer. Error reconnecting servers - Hiba a kiszolgálókhoz való újrakapcsolódáskor + 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 rejecting contact request + Hiba történt a partneri kapcsolatkérés elutasításakor + alert title + Error removing member - Hiba a tag eltávolításakor - No comment provided by engineer. + Hiba történt a tag eltávolításakor + alert title + + + Error reordering lists + Hiba történt a listák újrarendezésekor + alert title Error resetting statistics - Hiba a statisztikák visszaállításakor + 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 csoportprofil mentésekor + Hiba történt a csoportprofil mentésekor No comment provided by engineer. Error saving passcode - Hiba a jelkód mentésekor + Hiba történt a jelkód mentésekor No comment provided by engineer. Error saving passphrase to keychain - Hiba a jelmondat kulcstartó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 a kiszolgálók mentésekor + 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ói jelszó 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ásakor: %@ + 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. @@ -3214,7 +3557,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 auto-accept + Hiba az automatikus elfogadás beállításakor No comment provided by engineer. @@ -3224,42 +3572,47 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 a profilváltáskor - No comment provided by engineer. + Hiba történt a profilváltáskor + alert title Error switching profile! - Hiba a profilváltásakor! + Hiba történt a profilváltáskor! alertTitle Error synchronizing connection - Hiba a kapcsolat szinkronizálásakor + 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 csoporthivatkozá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 a kiszolgáló frissítésekor + Hiba történt a kiszolgáló frissítésekor alert title @@ -3269,17 +3622,17 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error updating user privacy - Hiba a felhasználói adatvédelem 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 hitelesítésekor: + Hiba történt a jelmondat hitelesítésekor: No comment provided by engineer. @@ -3290,7 +3643,14 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Error: %@ Hiba: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + Hiba: %@. + server test error Error: URL is invalid @@ -3327,6 +3687,11 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Kibontás chat item action + + Expired + Lejárt + token status text + Export database Adatbázis exportálása @@ -3364,7 +3729,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Fast and no wait until the sender is online! - Gyors és nem kell várni, amíg a feladó online lesz! + Gyors és nem kell várni, amíg az üzenetküldő online lesz! + No comment provided by engineer. + + + Faster deletion of groups. + Gyorsabb csoporttörlés. No comment provided by engineer. @@ -3372,15 +3742,25 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 - No comment provided by engineer. + file error alert title File errors: @@ -3389,24 +3769,31 @@ Ez az Ön egyszer használható meghívó-hivatkozása! %@ 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. + 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: %@ + Fájlkiszolgáló-hiba: %@ file error text File status - Fájlállapot + Fájl állapota No comment provided by engineer. File status: %@ - Fájlállapot: %@ + Fájl állapota: %@ copied message info @@ -3444,19 +3831,24 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Fájlok és médiatartalmak chat feature + + Files and media are prohibited in this chat. + A fájlok és a médiatartalmak küldése le van tiltva ebben a csevegésben. + No comment provided by engineer. + Files and media are prohibited. - A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban. + A fájlok és a médiatartalmak küldése le van tiltva. No comment provided by engineer. Files and media not allowed - A fájlok- és médiatartalmak nincsenek engedélyezve + A fájlok és a médiatartalmak küldése nincs engedélyezve No comment provided by engineer. Files and media prohibited! - A fájlok- és a médiatartalmak küldése le van tiltva! + A fájlok és a médiatartalmak küldése le van tiltva! No comment provided by engineer. @@ -3484,6 +3876,26 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Csevegési üzenetek gyorsabb megtalálása No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal. + server test error + + + Fingerprint in server address does not match certificate: %@. + A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@. + No comment provided by engineer. + Fix Javítás @@ -3506,7 +3918,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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. @@ -3514,6 +3926,11 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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: @@ -3526,7 +3943,12 @@ Ez az Ön egyszer használható meghívó-hivatkozása! 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 az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni. + 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. @@ -3546,7 +3968,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Forward %d message(s)? - %d üzenet továbbítása? + Továbbít %d üzenetet? alert title @@ -3561,7 +3983,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Forward messages without files? - Üzenetek továbbítása fájlok nélkül? + Továbbítja az üzeneteket fájlok nélkül? alert message @@ -3576,7 +3998,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Forwarded from - Továbbítva innen: + Továbbítva innen No comment provided by engineer. @@ -3585,31 +4007,31 @@ Ez az Ön egyszer használható meghívó-hivatkozá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 csatlakozni a(z) %@ célkiszolgálóhoz. Próbálja meg később. - No comment provided by engineer. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + A(z) %1$@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később. + alert message Forwarding server address is incompatible with network settings: %@. - A továbbító-kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. + A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. No comment provided by engineer. Forwarding server version is incompatible with network settings: %@. - A továbbító-kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@. + A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@. No comment provided by engineer. Forwarding server: %1$@ Destination server error: %2$@ - Továbbító-kiszolgáló: %1$@ -Célkiszolgáló hiba:%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$@ + Továbbítókiszolgáló: %1$@ Hiba: %2$@ snd error text @@ -3635,12 +4057,12 @@ Hiba: %2$@ Fully decentralized – visible only to members. - Teljesen decentralizált - csak a 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. @@ -3653,6 +4075,11 @@ Hiba: %2$@ 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! @@ -3676,7 +4103,7 @@ Hiba: %2$@ Group already exists! A csoport már létezik! - No comment provided by engineer. + new chat sheet title Group display name @@ -3685,7 +4112,7 @@ Hiba: %2$@ Group full name (optional) - Csoport teljes neve (nem kötelező) + A csoport teljes neve (nem kötelező) No comment provided by engineer. @@ -3725,7 +4152,7 @@ Hiba: %2$@ Group moderation - Csoport moderáció + Csoport moderálása No comment provided by engineer. @@ -3740,9 +4167,14 @@ Hiba: %2$@ 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. + A csoportprofil a tagok eszközein tárolódik, nem a kiszolgálókon. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + Csoportprofil módosítva. Ha menti, akkor a frissített profil el lesz küldve a csoporttagoknak. + alert message + Group welcome message A csoport üdvözlőüzenete @@ -3750,12 +4182,17 @@ Hiba: %2$@ Group will be deleted for all members - this cannot be undone! - A csoport törlésre kerül az összes 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. @@ -3763,6 +4200,11 @@ Hiba: %2$@ 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 @@ -3795,7 +4237,7 @@ Hiba: %2$@ Hide: - Elrejtés: + Elrejtve: No comment provided by engineer. @@ -3805,7 +4247,7 @@ Hiba: %2$@ 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. @@ -3823,6 +4265,11 @@ Hiba: %2$@ Hogyan segíti az adatvédelmet No comment provided by engineer. + + How it works + Hogyan működik + alert button + How to Hogyan @@ -3830,12 +4277,12 @@ Hiba: %2$@ How to use it - Hogyan használja + Használati útmutató No comment provided by engineer. How to use your servers - Saját kiszolgálók használata + Hogyan használja a saját kiszolgálóit No comment provided by engineer. @@ -3860,7 +4307,7 @@ Hiba: %2$@ 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 eltávolításra kerül! + 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. @@ -3870,7 +4317,7 @@ Hiba: %2$@ 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 felajánlásra kerül az adatbázis átköltözteté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. @@ -3895,7 +4342,7 @@ Hiba: %2$@ Immune to spam - Spam és visszaélések elleni védelem + Védett a kéretlen tartalommal szemben No comment provided by engineer. @@ -3905,7 +4352,7 @@ Hiba: %2$@ Import chat database? - Csevegési adatbázis importálása? + Importálja a csevegési adatbázist? No comment provided by engineer. @@ -3915,7 +4362,7 @@ Hiba: %2$@ Import failed - Sikertelen importálás + Nem sikerült az importálás No comment provided by engineer. @@ -3942,12 +4389,12 @@ További fejlesztések hamarosan! Improved privacy and security - Fejlesztett adatvédelem és biztonság + Továbbfejlesztett adatvédelem és biztonság No comment provided by engineer. Improved server configuration - Javított kiszolgáló konfiguráció + Továbbfejlesztett kiszolgálókonfiguráció No comment provided by engineer. @@ -3957,7 +4404,7 @@ További fejlesztések hamarosan! In reply to - Válasz neki + Válaszul erre No comment provided by engineer. @@ -3965,6 +4412,16 @@ További fejlesztések hamarosan! 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ó @@ -3982,7 +4439,7 @@ További fejlesztések hamarosan! 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 az összes ismerőséhez ú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. @@ -4037,13 +4494,13 @@ További fejlesztések hamarosan! Instant - Azonnal + 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. @@ -4057,6 +4514,31 @@ További fejlesztések hamarosan! 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 @@ -4075,7 +4557,7 @@ További fejlesztések hamarosan! Invalid link Érvénytelen hivatkozás - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4139,17 +4621,17 @@ További fejlesztések hamarosan! Irreversible message deletion is prohibited. - Az üzenetek végleges törlése le van tiltva ebben a csoportban. + 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 névtelen 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 az ismerőse régi adatbázis biztonsági mentést használt. + Ez akkor fordulhat elő, ha Ön vagy a partnere egy régi adatbázis biztonsági mentését használta. No comment provided by engineer. @@ -4159,7 +4641,7 @@ További fejlesztések hamarosan! 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 egy régi adatbázis biztonsági mentését használta. 3. A kapcsolat sérült. No comment provided by engineer. @@ -4188,19 +4670,19 @@ További fejlesztések hamarosan! Csatlakozás swipe action + + Join as %@ + Csatlakozás mint: %@ + No comment provided by engineer. + Join group - Csatlakozás csoporthoz - No comment provided by engineer. + Csatlakozás a csoporthoz + new chat sheet title Join group conversations - Csatlakozás csoportos beszélgetésekhez - No comment provided by engineer. - - - Join group? - Csatlakozik a csoporthoz? + Csatlakozás a csoportbeszélgetésekhez No comment provided by engineer. @@ -4208,17 +4690,12 @@ További fejlesztések hamarosan! Csatlakozás inkognitóban No comment provided by engineer. - - Join with current profile - Csatlakozás a jelenlegi profillal - No comment provided by engineer. - Join your group? This is your link for group %@! Csatlakozik a csoportjához? -Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! - No comment provided by engineer. +Ez a saját hivatkozása a(z) %@ nevű csoporthoz! + new chat action Joining group @@ -4227,7 +4704,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Keep - Megtart + Megtartás alert action @@ -4242,9 +4719,14 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Keep unused invitation? - Fel nem használt meghívó megtartása? + Megtartja a fel nem használt meghívót? alert title + + Keep your chats clean + Tartsa tisztán a csevegéseit + No comment provided by engineer. + Keep your connections Kapcsolatok megtartása @@ -4287,7 +4769,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Leave chat? - Csevegés elhagyása? + Elhagyja a csevegést? No comment provided by engineer. @@ -4297,7 +4779,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Leave group? - Csoport elhagyása? + Elhagyja a csoportot? + No comment provided by engineer. + + + Less traffic on mobile networks. + Kevesebb adatforgalom a mobilhálózatokon. No comment provided by engineer. @@ -4330,6 +4817,21 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! 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. + Live message! Élő üzenet! @@ -4340,6 +4842,11 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Élő üzenetek No comment provided by engineer. + + Loading profile… + Profil betöltése… + in progress text + Local name Helyi név @@ -4372,7 +4879,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! 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 nincsenek duplikálva. + 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. @@ -4402,7 +4909,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Media & file servers - Média és fájlkiszolgálók + Fájl- és médiakiszolgálók No comment provided by engineer. @@ -4415,69 +4922,104 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Tag No comment provided by engineer. + + Member %@ + %@ ismeretlen vagy már nem tag + past/unknown group member + + + Member admission + Tagbefogadás + No comment provided by engineer. + Member inactive Inaktív tag item status text + + Member is deleted - can't accept request + A tag törölve lett – nem lehet elfogadni a kérést + No comment provided by engineer. + + + Member reports + Tagok jelentései + chat feature + Member role will be changed to "%@". All chat members will be notified. - A tag szerepeköre meg fog változni a következőre: "%@". A csevegés tagjai értesítést fognak kapni. + 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 csoportban az összes tag értesítve lesz. + 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 - ezt a műveletet nem lehet visszavonni! + 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. + + Member will join the group, accept member? + A tag csatlakozni akar a csoporthoz, befogadja a tagot? + alert message + Members can add message reactions. - Csoporttagok üzenetreakciókat adhatnak hozzá. + A tagok reakciókat adhatnak hozzá az üzenetekhez. No comment provided by engineer. Members can irreversibly delete sent messages. (24 hours) - A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) + 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 csoport tagjai küldhetnek SimpleX-hivatkozásokat. + A tagok küldhetnek SimpleX-hivatkozásokat. No comment provided by engineer. Members can send direct messages. - A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. + A tagok küldhetnek egymásnak közvetlen üzeneteket. No comment provided by engineer. Members can send disappearing messages. - A csoport tagjai küldhetnek eltűnő üzeneteket. + A tagok küldhetnek eltűnő üzeneteket. No comment provided by engineer. Members can send files and media. - A csoport tagjai küldhetnek fájlokat és médiatartalmakat. + A tagok küldhetnek fájlokat és médiatartalmakat. No comment provided by engineer. Members can send voice messages. - A csoport tagjai küldhetnek hangüzeneteket. + A tagok küldhetnek hangüzeneteket. + No comment provided by engineer. + + + Mention members 👋 + Tagok említése 👋 No comment provided by engineer. @@ -4510,6 +5052,11 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Továbbított üzenet item status text + + Message instantly once you tap Connect. + Az üzenet azonnal megjelenik, amint a kapcsolódás gombra koppint. + No comment provided by engineer. + 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. @@ -4517,7 +5064,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Message queue info - Üzenet-sorbaállítási információ + Üzenet várólista információi No comment provided by engineer. @@ -4527,17 +5074,17 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Message reactions are prohibited in this chat. - Az üzenetreakciók küldése le van tiltva ebben a csevegésben. + A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben. No comment provided by engineer. Message reactions are prohibited. - Az üzenetreakciók küldése le van tiltva ebben a csoportban. + A reakciók hozzáadása az üzenetekhez le van tiltva. No comment provided by engineer. Message reception - Üzenetjelentés + Üzenetfogadás No comment provided by engineer. @@ -4547,7 +5094,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Message shape - Üzenetbuborék formája + Üzenetbuborék alakja No comment provided by engineer. @@ -4557,12 +5104,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Message status - Üzenetállapot + Üzenet állapota No comment provided by engineer. Message status: %@ - Üzenetállapot: %@ + Üzenet állapota: %@ copied message info @@ -4585,11 +5132,21 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Üzenetek és fájlok No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + Az üzenetek **végpontok közötti titkosítással** vannak védve. + No comment provided by engineer. + 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 @@ -4602,17 +5159,17 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Messages were deleted after you selected them. - Az üzeneteket törölték miután kiválasztotta őket. + 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érülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi. + Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve. 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érülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi. + Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve. No comment provided by engineer. @@ -4657,7 +5214,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Migration error: - Átköltöztetés hiba: + Átköltöztetési hiba: No comment provided by engineer. @@ -4682,14 +5239,19 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Moderated at - Moderálva ekkor: + Moderálva No comment provided by engineer. Moderated at: %@ - Moderálva ekkor: %@ + Moderálva: %@ copied message info + + More + Továbbiak + swipe action + More improvements are coming soon! Hamarosan további fejlesztések érkeznek! @@ -4707,18 +5269,23 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! 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 Multiple chat profiles - Több csevegőprofil + Több csevegési profil No comment provided by engineer. Mute Némítás - swipe action + notification label action + + + Mute all + Összes némítása + notification label action Muted when inactive! @@ -4737,7 +5304,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Network connection - Internetkapcsolat + Hálózati kapcsolat No comment provided by engineer. @@ -4747,7 +5314,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! 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. + Hálózati problémák – az üzenet többszöri elküldési kísérlet után lejárt. snd error text @@ -4757,7 +5324,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Network operator - Hálózati üzemeltető + Hálózatüzemeltető No comment provided by engineer. @@ -4768,7 +5335,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Network status Hálózat állapota - No comment provided by engineer. + alert title + + + New + Új + token status text New Passcode @@ -4777,17 +5349,17 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! 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. + Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítési adatok lesznek használva. 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. + Az összes kiszolgálóhoz új, SOCKS-hitelesítési adatok lesznek használva. No comment provided by engineer. New chat - Új beszélgetés + Új csevegés No comment provided by engineer. @@ -4797,7 +5369,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! New contact request - Új kapcsolatkérés + Új partneri kapcsolatkérés notification @@ -4812,7 +5384,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! New display name - Új megjelenítési név + Új megjelenítendő név No comment provided by engineer. @@ -4820,6 +5392,11 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Új események notification + + New group role: Moderator + Új szerepkör: Moderátor + No comment provided by engineer. + New in %@ Újdonságok a(z) %@ verzióban @@ -4835,6 +5412,11 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Új tag szerepköre No comment provided by engineer. + + New member wants to join the group. + Új tag szeretne csatlakozni a csoporthoz. + rcv group event chat item + New message Új üzenet @@ -4860,14 +5442,34 @@ Ez az Ön hivatkozása a(z) %@ nevű 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 chats with members + Nincsenek csevegések a tagokkal + No comment provided by engineer. + No contacts selected - Nincs kiválasztva ismerős + 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. @@ -4877,7 +5479,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No device token! - Nincs kiszüléktoken! + Nincs készüléktoken! No comment provided by engineer. @@ -4907,12 +5509,17 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No media & file servers. - Nincsenek média- és fájlkiszolgálók. + Nincsenek fájl- és médiakiszolgálók. servers error + + No message + Nincs üzenet + No comment provided by engineer. + No message servers. - Nincsenek üzenet-kiszolgálók. + Nincsenek üzenetkiszolgálók. servers error @@ -4935,6 +5542,11 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Nincs engedély a hangüzenet rögzítésére No comment provided by engineer. + + No private routing session + Nincs privát útválasztási munkamenet + alert title + No push server Helyi @@ -4952,22 +5564,32 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No servers to receive files. - Nincsenek fájlfogadó-kiszolgálók. + Nincsenek fájlfogadási kiszolgálók. servers error No servers to receive messages. - Nincsenek üzenetfogadó-kiszolgálók. + Nincsenek üzenetfogadási kiszolgálók. servers error No servers to send files. - Nincsenek fájlküldő-kiszolgálók. + Nincsenek fájlküldési 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. + Nincsenek felhasználói azonosítók. No comment provided by engineer. @@ -4975,9 +5597,14 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Nem kompatibilis! No comment provided by engineer. + + Notes + Jegyzetek + No comment provided by engineer. + Nothing selected - Nincs kiválasztva semmi + Nincs semmi kijelölve No comment provided by engineer. @@ -4995,11 +5622,21 @@ Ez az Ön hivatkozása a(z) %@ nevű 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. @@ -5022,7 +5659,9 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Ok Rendben - alert button + alert action +alert button +new chat action Old database @@ -5031,36 +5670,36 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! One-time invitation link - Egyszer használható meghívó-hivatkozás + Egyszer használható meghívó No comment provided by engineer. Onion hosts will be **required** for connection. Requires compatible VPN. - Onion-kiszolgálók **szükségesek** a kapcsolódáshoz. + 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 compatible VPN. - Onion-kiszolgálók használata, ha azok rendelkezésre állnak. + 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 chat owners can change preferences. - Csak a csevegés tulajdonosai módosíthatják a beállításokat. + 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. - Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket. + 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. @@ -5070,27 +5709,37 @@ VPN engedélyezése szükséges. 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 az üzenet küldője é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. @@ -5103,6 +5752,11 @@ VPN engedélyezése szükséges. Csak Ön tud eltűnő üzeneteket küldeni. No comment provided by engineer. + + Only you can send files and media. + Csak Ön küldhet fájlokat és médiatartalmakat. + No comment provided by engineer. + Only you can send voice messages. Csak Ön tud hangüzeneteket küldeni. @@ -5110,33 +5764,38 @@ VPN engedélyezése szükséges. Only your contact can add message reactions. - Csak az ismerőse tud üzenetreakciókat küldeni. + 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őse 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őse 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 files and media. + Csak a partnere küldhet fájlokat és médiatartalmakat. No comment provided by engineer. Only your contact can send voice messages. - Csak az ismerőse 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 @@ -5145,34 +5804,79 @@ VPN engedélyezése szükséges. Open changes - Változások megnyitása + Módosítások megtekintése No comment provided by engineer. Open chat Csevegés megnyitása - No comment provided by engineer. + new chat action Open chat console - Csevegés konzol megnyitása + Csevegési konzol megnyitása authentication reason + + Open clean link + Tiszta hivatkozás megnyitása + alert action + Open conditions Feltételek megnyitása No comment provided by engineer. + + Open full link + Teljes hivatkozás megnyitása + alert action + Open group Csoport megnyitása - No comment provided by engineer. + new chat action + + + Open link? + Megnyitja a hivatkozást? + 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 new chat + Új csevegés megnyitása + new chat action + + + Open new group + Új csoport megnyitása + new chat action + + + Open to accept + Megnyitás az elfogadáshoz + No comment provided by engineer. + + + Open to connect + Megnyitás a kapcsolódáshoz + No comment provided by engineer. + + + Open to join + Megnyitás a csatlakozáshoz + No comment provided by engineer. + + + Open to use bot + Megnyitás a bot használatához + No comment provided by engineer. + Opening app… Az alkalmazás megnyitása… @@ -5185,7 +5889,7 @@ VPN engedélyezése szükséges. Operator server - Kiszolgáló üzemeltető + Kiszolgáló-üzemeltető alert title @@ -5218,6 +5922,11 @@ VPN engedélyezése szükséges. 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 @@ -5247,7 +5956,7 @@ VPN engedélyezése szükséges. Passcode changed! - A jelkód megváltozott! + A jelkód módosult! No comment provided by engineer. @@ -5257,7 +5966,7 @@ VPN engedélyezése szükséges. Passcode not changed! - A jelkód nem változott! + A jelkód nem módosult! No comment provided by engineer. @@ -5272,14 +5981,9 @@ VPN engedélyezése szükséges. Password to show - Jelszó megjelenítése + Jelszó a megjelenítéshez No comment provided by engineer. - - Past member %@ - (Már nem tag) %@ - past/unknown group member - Paste desktop address Számítógép címének beillesztése @@ -5307,7 +6011,7 @@ VPN engedélyezése szükséges. Periodic - Rendszeresen + Időszakos No comment provided by engineer. @@ -5327,12 +6031,12 @@ VPN engedélyezése szükséges. Please ask your contact to enable calls. - Kérje meg az ismerősét, hogy engedélyezze a hívásokat. + Kérje meg a partnerét, hogy engedélyezze a hívásokat. No comment provided by engineer. Please ask your contact to enable sending voice messages. - Kérje meg az ismerősét, 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. @@ -5344,17 +6048,17 @@ Minden további problémát osszon meg a fejlesztőkkel. 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 az 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 a hálózati kapcsolatát a következővel: %@, és próbálja újra. - No comment provided by engineer. + Ellenőrizze a hálózati kapcsolatát a vele: %@, és próbálja újra. + alert message Please check yours and your contact preferences. - Ellenőrizze a saját- és az ismerőse beállításait. + Ellenőrizze a saját- és a partnere beállításait. No comment provided by engineer. @@ -5376,17 +6080,17 @@ Hiba: %@ Please enter correct current passphrase. - Adja meg a helyes, jelenlegi 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. @@ -5406,9 +6110,29 @@ 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 group moderators to review your request to join the group. + Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérését. + snd group event chat item + + + 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 @@ -5419,11 +6143,6 @@ Hiba: %@ 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 - server test error - Preserve the last message draft, with attachments. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. @@ -5431,7 +6150,7 @@ Hiba: %@ 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. @@ -5456,12 +6175,22 @@ Hiba: %@ Privacy for your customers. - Az Ön ügyfeleinek adatvédelme. + 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 kiszolgálók üzemeltetői számára. No comment provided by engineer. @@ -5469,6 +6198,11 @@ Hiba: %@ 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 @@ -5491,8 +6225,13 @@ Hiba: %@ Private routing error - Privát útválasztáshiba - No comment provided by engineer. + Privát útválasztási hiba + alert title + + + Private routing timeout + Privát útválasztás időtúllépése + alert title Profile and server connections @@ -5521,7 +6260,7 @@ Hiba: %@ Profile update will be sent to your contacts. - A profilfrissítés elküldésre került az ismerősök számára. + A profilfrissítés el lesz küldve a partnerei számára. alert message @@ -5531,17 +6270,22 @@ Hiba: %@ 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. - Az üzenetreakciók küldése le van tiltva. + 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. @@ -5561,7 +6305,7 @@ Hiba: %@ Prohibit sending files and media. - Fájlok- és a médiatartalmak küldésének letiltása. + A fájlok és a médiatartalmak küldése le van tiltva. No comment provided by engineer. @@ -5582,8 +6326,8 @@ Hiba: %@ Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings. - Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben. -Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. + 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. @@ -5591,6 +6335,11 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Védje meg a csevegési profiljait egy jelszóval! No comment provided by engineer. + + Protocol background timeout + Protokoll időtúllépése a háttérben + No comment provided by engineer. + Protocol timeout Protokoll időtúllépése @@ -5598,7 +6347,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Protocol timeout per KB - Protokoll időtúllépése KB-onként + Protokoll időtúllépése kB-onként No comment provided by engineer. @@ -5628,7 +6377,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Quantum resistant encryption - Kvantumrezisztens titkosítás + Kvantumbiztos titkosítás No comment provided by engineer. @@ -5638,7 +6387,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Reachable chat toolbar - Könnyen elérhető eszköztár + Könnyen elérhető csevegési eszköztár No comment provided by engineer. @@ -5673,7 +6422,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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). + További információ a [GitHub-tárolónkban](https://github.com/simplex-chat/simplex-chat#readme). No comment provided by engineer. @@ -5688,19 +6437,14 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Received at - Fogadva ekkor: + Fogadva No comment provided by engineer. Received at: %@ - Fogadva ekkor: %@ + Fogadva: %@ copied message info - - Received file event - Fogadott fájlesemény - notification - Received message Fogadott üzenetbuborék színe @@ -5723,7 +6467,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. + Az üzenetfogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása akkor fejeződik be, amikor az üzenetküldési kiszolgáló online lesz. No comment provided by engineer. @@ -5733,7 +6477,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Receiving via - Fogadás a + Fogadás a következőn keresztül: No comment provided by engineer. @@ -5768,7 +6512,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Reconnect all servers? - Újrakapcsolódás az összes kiszolgálóhoz? + Újrakapcsolódik az összes kiszolgálóhoz? No comment provided by engineer. @@ -5778,22 +6522,22 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Reconnect server? - Újrakapcsolódás a kiszolgálóhoz? + Ú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 @@ -5801,30 +6545,51 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) - Elutasítás (a feladó NEM kap értesítést) + Elutasítás (a kérés küldője NEM fog értesítést kapni) No comment provided by engineer. Reject contact request - Kapcsolatkérés elutasítása - No comment provided by engineer. + Partneri kapcsolatkérés elutasítása + alert title + + + Reject member? + Elutasítja a tagot? + alert title Relay server is only used if necessary. Another party can observe your IP address. - A közvetítő-kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet. + A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. No comment provided by engineer. Relay server protects your IP address, but it can observe the duration of the call. - A közvetítő-kiszolgáló megvédi az IP-címet, de megfigyelheti a hívás időtartamát. + A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. No comment provided by engineer. @@ -5834,7 +6599,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Remove archive? - Archívum eltávolítása? + Eltávolítja az archívumot? No comment provided by engineer. @@ -5842,6 +6607,11 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Kép eltávolítása No comment provided by engineer. + + Remove link tracking + Nyomonkövetési paraméterek eltávolítása a hivatkozásokból + No comment provided by engineer. + Remove member Eltávolítás @@ -5849,17 +6619,22 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Remove member? - Biztosan eltávolítja? + Eltávolítja a tagot? No comment provided by engineer. Remove passphrase from keychain? - Jelmondat eltávolítása a kulcstartóból? + Eltávolítja a jelmondatot a kulcstartóból? + No comment provided by engineer. + + + Removes messages and blocks members. + Üzenetek eltávolítása és a tagok tiltása. No comment provided by engineer. Renegotiate - Újraegyzetetés + Újraegyeztetés No comment provided by engineer. @@ -5869,12 +6644,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Renegotiate encryption? - Titkosítás újraegyeztetése? - No comment provided by engineer. - - - Repeat connection request? - Kapcsolatkérés megismétlése? + Újraegyezteti a titkosítást? No comment provided by engineer. @@ -5887,11 +6657,6 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Importálás ismét No comment provided by engineer. - - Repeat join request? - Csatlakozáskérés megismétlése? - No comment provided by engineer. - Repeat upload Feltöltés ismét @@ -5902,6 +6667,61 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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 sent to moderators + A jelentés el lett küldve a moderátoroknak + alert title + + + 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 Szükséges @@ -5924,7 +6744,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Reset all statistics? - Az összes statisztika visszaállítása? + Visszaállítja az összes statisztikát? No comment provided by engineer. @@ -5969,18 +6789,18 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. Retry Újrapróbálkozás - No comment provided by engineer. + alert action Reveal @@ -5992,11 +6812,21 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Feltételek felülvizsgálata No comment provided by engineer. - - Review later - Felülvizsgálat később + + Review group members + Csoporttagok áttekintése No comment provided by engineer. + + Review members + Tagok áttekintése + admission stage + + + Review members before admitting ("knocking"). + Tagok áttekintése a befogadás előtt (kopogtatás). + admission stage description + Revoke Visszavonás @@ -6009,7 +6839,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Revoke file? - Fájl visszavonása? + Visszavonja a fájlt? No comment provided by engineer. @@ -6046,16 +6876,26 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Save Mentés alert button - chat item action +chat item action Save (and notify contacts) - Mentés és az ismerősök értesítése + Mentés (és a partnerek értesítése) alert button + + Save (and notify members) + Mentés (és a tagok értesítése) + alert button + + + Save admission settings? + Menti a befogadási beállításokat? + alert title + Save and notify contact - Mentés és az ismerős értesítése + Mentés és a partner értesítése alert button @@ -6078,6 +6918,16 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Csoportprofil mentése No comment provided by engineer. + + Save group profile? + Menti a csoportprofilt? + alert title + + + Save list + Lista mentése + No comment provided by engineer. + Save passphrase and open chat Jelmondat mentése és a csevegés megnyitása @@ -6090,7 +6940,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Save preferences? - Beállítások mentése? + Menti a beállításokat? alert title @@ -6105,17 +6955,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Save servers? - Kiszolgálók mentése? + Menti a kiszolgálókat? alert title Save welcome message? - Üdvözlőüzenet mentése? + Menti az üdvözlőüzenetet? No comment provided by engineer. Save your profile? - Profil mentése? + Menti a profilt? alert title @@ -6125,12 +6975,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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 - Elmentve innen: + Mentve innen No comment provided by engineer. @@ -6160,7 +7010,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. @@ -6170,12 +7020,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Scan security code from your contact's app. - Biztonsági kód beolvasása az ismerősének alkalmazásából. + 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 + Kiszolgáló QR-kódjának beolvasása No comment provided by engineer. @@ -6185,7 +7035,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. @@ -6195,12 +7045,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Secondary - Másodlagos + Másodlagos szín No comment provided by engineer. Secure queue - Biztonságos sorbaállítás + Biztonságos várólista server test step @@ -6220,22 +7070,22 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Select - Kiválasztás + Kijelölés chat item action Select chat profile - Csevegési profil kiválasztása + Csevegési profil kijelölése No comment provided by engineer. Selected %lld - %lld kiválasztva + %lld kijelölve No comment provided by engineer. Selected chat preferences prohibit this message. - A kiválasztott csevegési beállítások tiltják ezt az üzenetet. + A kijelölt csevegési beállítások tiltják ezt az üzenetet. No comment provided by engineer. @@ -6245,17 +7095,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. @@ -6265,7 +7115,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. + + + Send contact request? + Elküldi a partneri kapcsolatkérést? No comment provided by engineer. @@ -6290,7 +7145,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Send link previews - Hivatkozás előnézetek küldése + Hivatkozások előnézetének megjelenítése No comment provided by engineer. @@ -6305,12 +7160,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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 az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + 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 az Ön 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. No comment provided by engineer. @@ -6318,9 +7173,14 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Értesítések küldése No comment provided by engineer. + + Send private reports + Privát jelentések küldése + No comment provided by engineer. + Send questions and ideas - Ötletek és kérdések beküldése + Ötletek és javaslatok No comment provided by engineer. @@ -6328,14 +7188,29 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Kézbesítési jelentések küldése No comment provided by engineer. + + Send request + Kérés küldése + No comment provided by engineer. + + + Send request without message + Kérés küldése üzenet nélkül + No comment provided by engineer. + Send 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. + + + Send your private feedback to groups. + Küldjön privát visszajelzést a csoportoknak. No comment provided by engineer. @@ -6345,17 +7220,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Sender may have deleted the connection request. - A küldő törölhette a kapcsolatkérést. + A kérés küldője törölhette a kapcsolódá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ő összes ismerőse 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 az összes ismerőse 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. @@ -6365,7 +7240,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Sending receipts is disabled for %lld contacts - A kézbesítési jelentések le vannak tiltva %lld ismerősnél + A kézbesítési jelentések le vannak tiltva %lld partnernél No comment provided by engineer. @@ -6375,7 +7250,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Sending receipts is enabled for %lld contacts - A kézbesítési jelentések engedélyezve vannak %lld ismerősnél + A kézbesítési jelentések engedélyezve vannak %lld partnernél No comment provided by engineer. @@ -6385,17 +7260,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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 @@ -6403,11 +7278,6 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Közvetlenül küldött No comment provided by engineer. - - Sent file event - Elküldött fájlesemény - notification - Sent message Üzenetbuborék színe @@ -6420,7 +7290,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. @@ -6435,7 +7305,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Sent via proxy - Proxyn keresztül küldve + Proxyn keresztül küldött No comment provided by engineer. @@ -6465,27 +7335,27 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Server operator changed. - A kiszolgáló üzemeltetője megváltozott. + A kiszolgáló üzemeltetője módosult. alert title Server operators - Kiszolgáló-üzemeltetők + Kiszolgálóüzemeltetők No comment provided by engineer. Server protocol changed. - A kiszolgáló-protokoll megváltozott. + 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 sorbaállítás létrehozásához, ellenőrizze jelszavát + + Server requires authorization to create queues, check password. + A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát. 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 + + Server requires authorization to upload, check password. + A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze a jelszavát. server test error @@ -6520,7 +7390,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Servers statistics will be reset - this cannot be undone! - A kiszolgálók statisztikái visszaállnak - ez a művelet nem vonható vissza! + A kiszolgálók statisztikái visszaállnak – ez a művelet nem vonható vissza! No comment provided by engineer. @@ -6533,9 +7403,14 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. @@ -6550,7 +7425,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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 member admission + Tagbefogadás beállítása + 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. @@ -6568,9 +7453,14 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Jelmondat beállítása az exportáláshoz No comment provided by engineer. + + Set profile bio and welcome message. + Névjegy és üdvözlőüzenet beállítása a profilokhoz. + No comment provided by engineer. + 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. @@ -6585,28 +7475,28 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Settings were changed. - A beállítások megváltoztak. + 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 alert action - chat item action +chat item action Share 1-time link - Egyszer használható 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ó-hivatkozás megosztása egy baráttal + Egyszer használható meghívó megosztása egy baráttal No comment provided by engineer. @@ -6626,7 +7516,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share address with contacts? - Megosztja a címet az ismerőseivel? + Megosztja a címet a partnereivel? alert title @@ -6636,9 +7526,19 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share link - Hivatkozás megosztása + Megosztás No comment provided by engineer. + + Share old address + Régi cím megosztása + alert button + + + Share old link + Régi (hosszú) hivatkozás megosztása + alert button + Share profile Profil megosztása @@ -6646,17 +7546,37 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share this 1-time invite link - Egyszer használható 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 SimpleX-ben + Megosztás a SimpleXben No comment provided by engineer. Share with contacts - Megosztás az ismerősökkel + Megosztás a partnerekkel + No comment provided by engineer. + + + Share your address + Saját cím megosztása + No comment provided by engineer. + + + Short SimpleX address + Rövid SimpleX-cím + No comment provided by engineer. + + + Short description + Rövid leírás + No comment provided by engineer. + + + Short link + Rövid hivatkozás No comment provided by engineer. @@ -6676,12 +7596,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Show last messages - Szobák utolsó üzeneteinek megjelenítése a listanézetben + Legutóbbi üzenetek előnézetének megjelenítése No comment provided by engineer. Show message status - Üzenetállapot megjelenítése + Üzenet állapotának megjelenítése No comment provided by engineer. @@ -6691,7 +7611,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Show preview - Értesítés előnézete + Értesítésekben megjelenő információk No comment provided by engineer. @@ -6701,7 +7621,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Show: - Megjelenítés: + Megjelenítve: No comment provided by engineer. @@ -6751,14 +7671,24 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX address and 1-time links are safe to share via any messenger. - A SimpleX-cím és az egyszer használható meghívó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül. + A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó-alkalmazáson keresztül. No comment provided by engineer. SimpleX address or 1-time link? - SimpleX-cím vagy egyszer használható meghívó-hivatkozás? + SimpleX-cím vagy egyszer használható meghívó? No comment provided by engineer. + + SimpleX address settings + Beállítások automatikus elfogadása + alert title + + + SimpleX channel link + SimpleX-csatornahivatkozás + simplex link type + SimpleX contact address SimpleX kapcsolattartási cím @@ -6781,7 +7711,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX links are prohibited. - A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban. + A SimpleX-hivatkozások küldése le van tiltva. No comment provided by engineer. @@ -6791,14 +7721,19 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX one-time invitation - Egyszer használható SimpleX-meghívó-hivatkozás + 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. + A SimpleX protokollokat a Trail of Bits auditálta. No comment provided by engineer. + + SimpleX relay link + SimpleX továbbítókiszolgáló-hivatkozás + simplex link type + Simplified incognito mode Egyszerűsített inkognitómód @@ -6836,7 +7771,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Some file(s) were not exported: - Néhány fájl nem került exportálásra: + Néhány fájl nem lett exportálva: No comment provided by engineer. @@ -6861,6 +7796,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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. @@ -6873,7 +7814,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Start chat? - Csevegés indítása? + Elindítja a csevegést? No comment provided by engineer. @@ -6883,7 +7824,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Starting from %@. - Kezdve ettől %@. + Statisztikagyűjtés kezdete: %@. No comment provided by engineer. @@ -6913,7 +7854,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Stop chat? - Csevegési szolgáltatás megállítása? + Megállítja a csevegést? No comment provided by engineer. @@ -6923,12 +7864,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Stop receiving file? - Fájlfogadás megállítása? + Megállítja a fájlfogadást? No comment provided by engineer. Stop sending file? - Fájlküldés megállítása? + Megállítja a fájlküldést? No comment provided by engineer. @@ -6938,7 +7879,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Stop sharing address? - Címmegosztás megállítása? + Megállítja a címmegosztást? alert title @@ -6946,6 +7887,11 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Csevegés megállítása folyamatban No comment provided by engineer. + + Storage + Tárhely + No comment provided by engineer. + Strong Erős @@ -6983,7 +7929,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Switch chat profile for 1-time invitations. - Csevegési profilváltás az egyszer használható meghívó-hivatkozásokhoz. + Csevegési profilváltás az egyszer használható meghívókhoz. No comment provided by engineer. @@ -6993,17 +7939,27 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. System authentication - Rendszerhitelesítés + Rendszer-hitelesítés No comment provided by engineer. TCP connection - TCP kapcsolat + TCP-kapcsolat + No comment provided by engineer. + + + TCP connection bg timeout + TCP-kapcsolat időtúllépése a háttérben No comment provided by engineer. TCP connection timeout - TCP kapcsolat időtúllépése + 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. @@ -7031,11 +7987,31 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Kép készítése No comment provided by engineer. + + Tap Connect to chat + Koppintson a „Kapcsolódás” gombra a csevegéshez + No comment provided by engineer. + + + Tap Connect to send request + Koppintson a „Kapcsolódás” gombra a kérés elküldéséhez + No comment provided by engineer. + + + Tap Connect to use bot + Koppintson a „Kapcsolódás” gombra a bot használatához + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. No comment provided by engineer. + + Tap Join group + Koppintson a „Csatlakozás a csoporthoz” gombra + No comment provided by engineer. + Tap button Koppintson a @@ -7048,7 +8024,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Tap to activate profile. - A profil aktiválásához koppintson az ikonra. + Koppintson ide a profil aktiválásához. No comment provided by engineer. @@ -7058,7 +8034,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Tap to join incognito - Koppintson ide az inkognitóban való csatlakozáshoz + Koppintson ide az inkognitóban való kapcsolódáshoz No comment provided by engineer. @@ -7068,19 +8044,24 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Tap to scan - Koppintson ide a QR-kód beolvasáshoz + Koppintson ide a QR-kód beolvasásához No comment provided by engineer. Temporary file error - Ideiglenesfájl-hiba - No comment provided by engineer. + 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 @@ -7103,29 +8084,34 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. 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 Weblate-en! + 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 address will be short, and your profile will be shared via the address. + A cím rövid lesz és a profil meg lesz osztva a címen keresztül. + alert message + 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 kapcsolatkéréseket kap – beállítások megnyitása az engedélyezéshez. + Az alkalmazás értesíteni fogja, amikor üzeneteket vagy 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ésben más-más üzemeltetőket használ. + 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. @@ -7135,27 +8121,27 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. 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 QR-kód nem egy SimpleX-QR-kód-hivatkozás. + A beolvasott QR-kód nem egy SimpleX-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, az Ön ismerőse lehet, hogy offline állapotban van. + A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van. No comment provided by engineer. The connection you accepted will be cancelled! - Az Ön által elfogadott kérelem vissza lesz vonva! + Az Ön által elfogadott kapcsolat 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. @@ -7170,17 +8156,22 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The future of messaging - A privát üzenetküldés következő generációja + Az üzenetváltás jövője No comment provided by engineer. The hash of the previous message is different. - Az előző üzenet hasító értéke különbözik. + Az előző üzenet kivonata különbözik. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + A hivatkozás rövid lesz és a csoportprofil meg lesz osztva a hivatkozáson keresztül. + alert message + The message will be deleted for all members. - Az üzenet az összes 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. @@ -7190,7 +8181,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The messages will be deleted for all members. - Az üzenetek az összes tag számára törlésre kerülnek. + Az üzenetek az összes tag számára törölve lesznek. No comment provided by engineer. @@ -7200,22 +8191,12 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. 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éskor, így törölhető. - No comment provided by engineer. - - - The profile is only shared with your contacts. - A profilja csak az ismerőseivel kerül megosztásra. + 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 same conditions will apply to operator **%@**. - Ugyanezek a feltételek lesznek elfogadva a következő üzemeltetőre is: **%@**. - No comment provided by engineer. - - - The same conditions will apply to operator(s): **%@**. - Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**. + Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**. No comment provided by engineer. @@ -7230,17 +8211,17 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The sender will NOT be notified - A feladó NEM fog értesítést kapni - No comment provided by engineer. + A kérés küldője NEM fog értesítést kapni + alert message The servers for new connections of your current chat profile **%@**. - A jelenlegi csevegési profilhoz tartozó új kapcsolatok 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 **%@**. - Az Ön jelenlegi **%@** nevű csevegőprofiljához tartozó új fájlok kiszolgálói. + A jelenlegi **%@** nevű csevegési profiljához tartozó új fájlok kiszolgálói. No comment provided by engineer. @@ -7250,7 +8231,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The uploaded database archive will be permanently removed from the servers. - A feltöltött adatbázis-archívum véglegesen eltávolításra kerül a kiszolgálókról. + 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. @@ -7265,27 +8246,32 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. These settings are for your current profile **%@**. - Ezek a beállítások csak 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ók 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édiatartalmakkal együtt törlésre kerül. Az alacsony felbontású képek 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. @@ -7295,7 +8281,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 @@ -7305,12 +8291,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 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. @@ -7318,14 +8304,9 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Ez a csoport már nem létezik. No comment provided by engineer. - - This is your own SimpleX address! - Ez az Ön SimpleX-címe! - No comment provided by engineer. - - - This is your own one-time link! - Ez az Ön egyszer használható meghívó-hivatkozása! + + 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. @@ -7333,9 +8314,24 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. 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 a jelenlegi (**%@**) profiljában lévő üzenetekre vonatkozik. + Ez a beállítás csak az Ön jelenlegi **%@** nevű csevegési profiljában lévő üzenetekre vonatkozik. + No comment provided by engineer. + + + This setting is for your current profile **%@**. + Ez a beállítás csak a jelenlegi **%@** nevű csevegési profiljára vonatkozik. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + Az üzeneteltűnési idő csak az új partnerekre vonatkozik. No comment provided by engineer. @@ -7350,7 +8346,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. @@ -7365,7 +8361,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. 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 az ismerősével. + 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. @@ -7387,7 +8383,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To protect your privacy, SimpleX uses separate IDs for each of your contacts. - Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt. + Adatainak védelme érdekében a SimpleX külön azonosítókat használ minden egyes kapcsolatához. No comment provided by engineer. @@ -7412,7 +8408,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a **Csevegési profilok** menüben. + 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. @@ -7420,11 +8416,21 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll A küldéshez No comment provided by engineer. + + To send commands you must be connected. + A parancsok küldéséhez kapcsolódva kell lennie. + alert message + To support instant push notifications the chat database has to be migrated. Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + Másik profil használatához a kapcsolatfelvételi kísérlet után törölje a csevegést, és használja újra a hivatkozást. + alert message + To use the servers of **%@**, accept conditions of use. A(z) **%@** kiszolgálóinak használatához fogadja el a használati feltételeket. @@ -7432,19 +8438,24 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll 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) az ismerőse eszközén lévő kóddal. + 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őlista átváltása: + Csevegési lista ki/be: No comment provided by engineer. Toggle incognito when connecting. - Inkognitómód használata kapcsolódáskor. + Inkognitó profil használata kapcsolódáskor ki/be. No comment provided by engineer. + + Token status: %@. + Token állapota: %@. + token status + Toolbar opacity Eszköztár átlátszatlansága @@ -7457,7 +8468,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Transport isolation - Átvitel-izoláció módja + Átvitelelkülönítés No comment provided by engineer. @@ -7465,15 +8476,10 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll 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ősé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ősétől érkező üzenetek fogadására szolgál. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál. + subscription status explanation Turkish interface @@ -7502,7 +8508,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Unblock for all - Letiltás feloldása az összes tag számára + Feloldás No comment provided by engineer. @@ -7517,7 +8523,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Unblock member? - Tag feloldása? + Feloldja a tag letiltását? No comment provided by engineer. @@ -7577,24 +8583,24 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll 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. + Hacsak nem az iOS hívási felületét használja, engedélyezze a Ne zavarjanak módot a megszakadá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. 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 egyszer, lehet hogy ez egy hiba – jelentse a problémát. -A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapcsolattartási 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. Unlink - Szétkapcsolás + Leválasztás No comment provided by engineer. Unlink desktop? - Számítógép szétkapcsolása? + Leválasztja a számítógépet? No comment provided by engineer. @@ -7610,16 +8616,21 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Unmute Némítás megszüntetése - swipe action + 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. @@ -7629,17 +8640,22 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc 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 settings? - Beállítások frissítése? + Frissíti a beállításokat? + No comment provided by engineer. + + + Updated conditions + Frissített feltételek No comment provided by engineer. @@ -7647,11 +8663,41 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc 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 + Frissítés + alert button + + + Upgrade address + Cím frissítése + No comment provided by engineer. + + + Upgrade address? + Frissíti a címet? + alert message + Upgrade and open chat Fejlesztés és a csevegés megnyitása No comment provided by engineer. + + Upgrade group link? + Frissíti a csoporthivatkozást? + alert message + + + Upgrade link + Hivatkozás frissítése + No comment provided by engineer. + + + Upgrade your address + Cím frissítése + No comment provided by engineer. + Upload errors Feltöltési hibák @@ -7689,7 +8735,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use .onion hosts - Onion-kiszolgálók használata + Onion kiszolgálók használata No comment provided by engineer. @@ -7699,18 +8745,28 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc 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álókhoz. No comment provided by engineer. Use chat - Csevegés használata + SimpleX Chat használata No comment provided by engineer. Use current profile Jelenlegi profil használata - No comment provided by engineer. + new chat action Use for files @@ -7724,7 +8780,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use for new connections - Alkalmazás új kapcsolatokhoz + Használat új kapcsolatokhoz No comment provided by engineer. @@ -7734,13 +8790,18 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use iOS call interface - Az iOS hívási felületét használata + iOS hívási felület használata + No comment provided by engineer. + + + Use incognito profile + Inkognitóprofil használata No comment provided by engineer. Use new incognito profile Új inkognitóprofil használata - No comment provided by engineer. + new chat action Use only local notifications? @@ -7749,12 +8810,12 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc 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. + Privát útválasztás használata az 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. + Privát útválasztás használata az ismeretlen kiszolgálókhoz. No comment provided by engineer. @@ -7769,17 +8830,22 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use the app while in the call. - Használja az alkalmazást hívás közben. + Alkalmazás használata hívás közben. No comment provided by engineer. Use the app with one hand. - Használja az alkalmazást egy kézzel. + Alkalmazás egy kézzel való használata. + No comment provided by engineer. + + + Use web port + Webport használata No comment provided by engineer. User selection - Felhasználó kiválasztása + Felhasználó kijelölése No comment provided by engineer. @@ -7789,7 +8855,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Using SimpleX Chat servers. - SimpleX Chat-kiszolgálók használatban. + SimpleX Chat kiszolgálók használatban. No comment provided by engineer. @@ -7834,7 +8900,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Via secure quantum resistant protocol. - Biztonságos kvantumrezisztens-protokollon keresztül. + Biztonságos kvantumbiztos protokollon keresztül. No comment provided by engineer. @@ -7854,7 +8920,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc 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. @@ -7889,7 +8955,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Voice messages are prohibited. - A hangüzenetek küldése le van tiltva ebben a csoportban. + A hangüzenetek küldése le van tiltva. No comment provided by engineer. @@ -7899,7 +8965,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Voice messages prohibited! - A hangüzenetek le vannak tilva! + A hangüzenetek le vannak tiltva! No comment provided by engineer. @@ -7929,7 +8995,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Wallpaper accent - Háttérkép kiemelés + Háttérkép kiemelőszíne No comment provided by engineer. @@ -7939,7 +9005,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc 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. @@ -7954,7 +9020,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Welcome %@! - Üdvözöllek %@! + Üdvözöljük %@! No comment provided by engineer. @@ -7967,6 +9033,11 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Az üdvözlőüzenet túl hosszú No comment provided by engineer. + + Welcome your contacts 👋 + Üdvözölje a partnereit 👋 + No comment provided by engineer. + What's new Újdonságok @@ -7984,12 +9055,12 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc When more than one operator is enabled, none of them has metadata to learn who communicates with whom. - Amikor egynél több hálózati üzemeltető van engedélyezve, egyikük sem rendelkezik olyan metaadatokkal ahhoz, hogy felderítse, ki kommunikál kivel. + 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. + Ha egy inkognitóprofilt oszt meg valamelyik partnerével, a rendszer ezt az inkognitóprofilt fogja használni azokban a csoportokban, ahová az adott partnere meghívja Önt. No comment provided by engineer. @@ -8024,32 +9095,32 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Without Tor or VPN, your IP address will be visible to file servers. - Tor vagy VPN nélkül az IP-címe látható lesz a fájlkiszolgálók számára. + Tor vagy VPN nélkül az IP-címe láthatóvá válik 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 IP-címe látható lesz a következő XFTP-közvetítő-kiszolgálók számára: %@. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@. alert message Wrong database passphrase - Hibás adatbázis-jelmondat + Érvénytelen adatbázis-jelmondat No comment provided by engineer. Wrong key or unknown connection - most likely this connection is deleted. - Hibás kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött. + É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. - Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött. + É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! - Hibás jelmondat! + Érvénytelen jelmondat! No comment provided by engineer. @@ -8064,7 +9135,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You accepted connection - Kapcsolat létrehozása + Ön elfogadta a kapcsolatot No comment provided by engineer. @@ -8074,12 +9145,12 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc 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 %@. - Ön már kapcsolódva van ehhez: %@. + Ön már kapcsolódott a következőhöz: %@. No comment provided by engineer. @@ -8089,13 +9160,13 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You are already connecting to %@. - Már folyamatban van a kapcsolódás ehhez: %@. - No comment provided by engineer. + A kapcsolódás már folyamatban van a következőhöz: %@. + new chat sheet message You are already connecting via this one-time link! - A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül! - No comment provided by engineer. + A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül! + new chat sheet message You are already in group %@. @@ -8105,35 +9176,35 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You are already joining the group %@. A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz. - No comment provided by engineer. - - - You are already joining the group via this link! - A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül! - No comment provided by engineer. + new chat sheet message 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. + new chat sheet message You are already joining the group! Repeat join request? - Csatlakozás folyamatban! -Csatlakozáskérés megismétlése? - No comment provided by engineer. + A csatlakozás már folyamatban van a csoporthoz! +Megismétli a csatlakozási kérést? + new chat sheet title - - 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 ismerősétől érkező üzenetek fogadására szolgál. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + Ön kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál. + subscription status explanation 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 the server used to receive messages from this connection (no subscription). + Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs előfizetés). + subscription status explanation + 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. @@ -8141,7 +9212,7 @@ Csatlakozáskérés megismétlése? 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. + A lezárási képernyőről is fogadhat hívásokat, eszköz- és alkalmazáshitelesítés nélkül. No comment provided by engineer. @@ -8149,11 +9220,6 @@ Csatlakozáskérés megismétlése? Ezt a „Megjelenés” menüben módosíthatja. No comment provided by engineer. - - You can configure operators in Network & servers settings. - Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja. - 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. @@ -8166,12 +9232,12 @@ Csatlakozáské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üjében. + Később engedélyezheti őket az „Adatvédelem és biztonság” menüben. No comment provided by engineer. @@ -8181,12 +9247,12 @@ Csatlakozáskérés megismétlése? 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. + Elrejtheti vagy lenémíthatja a felhasználó -profiljait – csúsztassa jobbra a profilt. No comment provided by engineer. You can make it visible to your SimpleX contacts via Settings. - Láthatóvá teheti a SimpleXbeli ismerősei 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. @@ -8196,12 +9262,12 @@ Csatlakozáskérés megismétlése? You can send messages to %@ from Archived contacts. - Az „Archivált ismerősökből” továbbra is küldhet üzeneteket neki: %@. + 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 az ismerős nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást. + Beállíthatja a partner nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást. No comment provided by engineer. @@ -8211,12 +9277,12 @@ Csatlakozáskérés megismétlése? 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 csoporthivatkozást később törli, akkor nem fogja elveszíteni a csoport meglévő tagjait. No comment provided by engineer. You can share this address with your contacts to let them connect with **%@**. - Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek Önnel a(z) **%@** nevű profilján keresztül. + Megoszthatja ezt a SimpleX-címet a partnereivel, hogy kapcsolatba léphessenek vele: **%@**. No comment provided by engineer. @@ -8226,7 +9292,7 @@ Csatlakozáskérés megismétlése? You can still view conversation with %@ in the list of chats. - A(z) %@ nevű ismerősével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. + A(z) %@ nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. No comment provided by engineer. @@ -8241,13 +9307,18 @@ Csatlakozáské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. + A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. + alert message + + + You can view your reports in Chat with admins. + A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben. alert message You can't send messages! - Nem lehet üzeneteket küldeni! - No comment provided by engineer. + Ön nem tud üzeneteket küldeni! + alert title You could not be verified; please try again. @@ -8259,36 +9330,31 @@ Csatlakozáskérés megismétlése? Ön dönti el, hogy kivel beszélget. No comment provided by engineer. - - You have already requested connection via this address! - Már küldött egy kapcsolatkérést ezen a címen keresztül! - No comment provided by engineer. - You have already requested connection! Repeat connection request? - Már küldött egy kapcsolódási kérelmet! -Kapcsolatkérés megismétlése? - No comment provided by engineer. + Ön már küldött egy kapcsolódási kérést! +Megismétli a kapcsolódási kérést? + new chat sheet title 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. @@ -8298,22 +9364,22 @@ Kapcsolatkérés megismétlése? You may save the exported archive. - Az exportált archívumot elmentheti. + Mentheti az exportált archívumot. 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ősétő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 az ismerőse számára, hogy fel tudják hívni egymást. + 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 az ismerőse számára, hogy hangüzeneteket küldhessenek egymásnak. + 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. @@ -8326,6 +9392,16 @@ Kapcsolatkérés megismétlése? Csoportmeghívó elküldve No comment provided by engineer. + + You should receive notifications. + Ön megkapja az értesítéseket. + token info + + + You will be able to send messages **only after your request is accepted**. + Csak azután tud üzeneteket küldeni, **miután a kérését elfogadták**. + 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! @@ -8338,22 +9414,17 @@ Kapcsolatkérés megismétlése? You will be connected when your connection request is accepted, please wait or check later! - Akkor lesz kapcsolódva, ha a kapcsolatkérése elfogadásra kerül, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva, ha a kapcsolódási kérését elfogadják, várjon, vagy ellenőrizze később! No comment provided by engineer. You will be connected when your contact's device is online, please wait or check later! - Akkor lesz kapcsolódva, amikor az ismerősének 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. - No comment provided by engineer. - - - You will connect to all group members. - Kapcsolódni fog a csoport összes tagjához. + 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. @@ -8373,17 +9444,17 @@ Kapcsolatké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 a címé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ősét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a 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 - 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 + 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. @@ -8391,16 +9462,16 @@ Kapcsolatkérés megismétlése? Saját ICE-kiszolgálók No comment provided by engineer. - - Your SMP servers - Saját SMP-kiszolgálók - No comment provided by engineer. - Your SimpleX address Profil SimpleX-címe No comment provided by engineer. + + Your business contact + Üzleti partner + No comment provided by engineer. + Your calls Hívások @@ -8418,7 +9489,7 @@ Kapcsolatkérés megismétlése? Your chat preferences - Csevegési beállítások + Az Ön csevegési beállításai alert title @@ -8426,34 +9497,44 @@ Kapcsolatkérés megismétlése? Csevegési profilok No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + A csevegés át lett helyezve ide: %@, de egy váratlan hiba történt a profilra való átirányításkor. + alert message + + + Your connection was moved to %@ but an error happened when switching 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 + Partner + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). - Az 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. - Az ismerősei 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. + A hitelesítési 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. @@ -8461,6 +9542,11 @@ Kapcsolatkérés megismétlése? Jelenlegi profil No comment provided by engineer. + + Your group + Saját csoport + No comment provided by engineer. + Your preferences Beállítások @@ -8473,29 +9559,29 @@ Kapcsolatkérés megismétlése? Your profile - Profil + Saját profil No comment provided by engineer. 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 only shared with your contacts. + A profilja az eszközén van tárolva és csak a partnereivel van megosztva. No comment provided by engineer. Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. - A profilja az eszközön van tárolva és csak az ismerőseivel kerül megosztásra. A SimpleX-kiszolgálók nem láthatják a profilját. + 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 megváltozott. Ha elmenti, a frissített profil elküldésre kerül az összes ismerősének. + A profilja módosult. Ha menti, akkor a profilfrissítés el lesz küldve a partnerei számára. alert message - - Your profile, contacts and delivered messages are stored on your device. - A profilja, az ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra. - No comment provided by engineer. - Your random profile Véletlenszerű profil @@ -8508,7 +9594,7 @@ Kapcsolatkérés megismétlése? Your servers - Az Ön kiszolgálói + Saját kiszolgálók No comment provided by engineer. @@ -8518,7 +9604,7 @@ Kapcsolatkérés megismétlése? [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. @@ -8546,9 +9632,14 @@ Kapcsolatkérés megismétlése? gombra fent, majd válassza ki: No comment provided by engineer. + + accepted %@ + befogadta őt: %@ + rcv group event chat item + accepted call - elfogadott hívás + fogadott hívás call status @@ -8556,6 +9647,11 @@ Kapcsolatkérés megismétlése? elfogadott meghívó chat list item title + + accepted you + befogadta Önt + rcv group event chat item + admin adminisztrátor @@ -8576,6 +9672,11 @@ Kapcsolatkérés megismétlése? titkosítás elfogadása… chat item text + + all + összes + member criteria value + all members összes tag @@ -8591,9 +9692,14 @@ Kapcsolatkérés megismétlése? é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 + kísérletek No comment provided by engineer. @@ -8608,12 +9714,12 @@ Kapcsolatkérés megismétlése? bad message ID - téves üzenet ID + hibás az üzenet azonosítója integrity error chat item bad message hash - hibás az üzenet hasító értéke + hibás az üzenet kivonata integrity error chat item @@ -8623,13 +9729,14 @@ Kapcsolatkérés megismétlése? blocked %@ - letiltotta %@-t + letiltotta őt: %@ rcv group event chat item blocked by admin letiltva az adminisztrátor által - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8656,6 +9763,11 @@ Kapcsolatkérés megismétlése? hívás… call status + + can't send messages + nem lehet üzeneteket küldeni + No comment provided by engineer. + cancelled %@ %@ visszavonva @@ -8663,32 +9775,32 @@ Kapcsolatkérés megismétlése? 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 az Ön szerepkörét erre: %@ + a következőre módosította az Ön szerepkörét: „%@” rcv group event chat item changing address for %@… - cím megváltoztatása nála: %@… + cím módosítása %@ számára… chat item text changing address… - cím megváltoztatása… + cím módosítása… chat item text colored - színes + színezett No comment provided by engineer. @@ -8706,11 +9818,6 @@ Kapcsolatkérés megismétlése? kapcsolódott No comment provided by engineer. - - connected directly - közvetlenül kapcsolódott - rcv group event chat item - connecting kapcsolódás @@ -8728,12 +9835,12 @@ Kapcsolatkérés megismétlése? connecting (introduced) - kapcsolódás (bejelentve) + kapcsolódás (bemutatkozva) No comment provided by engineer. connecting (introduction invitation) - kapcsolódás (bemutatkozó-meghívó) + kapcsolódás (bemutatkozó meghívó) No comment provided by engineer. @@ -8758,17 +9865,37 @@ Kapcsolatkérés megismétlése? 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 deleted + partner törölve + No comment provided by engineer. + + + contact disabled + partner letiltva + No comment provided by engineer. + 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. + + + contact not ready + a partner nem áll készen + No comment provided by engineer. + + + contact should accept… + a partnernek el kell fogadnia… No comment provided by engineer. @@ -8778,12 +9905,12 @@ Kapcsolatkérés megismétlése? 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. @@ -8799,7 +9926,8 @@ Kapcsolatkérés megismétlése? default (%@) alapértelmezett (%@) - pref value + delete after time +pref value default (no) @@ -8818,17 +9946,17 @@ Kapcsolatkérés megismétlése? deleted contact - törölt ismerős + törölt partner rcv direct event chat item deleted group - törölt csoport + törölte a csoportot rcv group event chat item 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. @@ -8863,7 +9991,7 @@ Kapcsolatkérés megismétlése? enabled for contact - engedélyezve az ismerős számára + engedélyezve a partner számára enabled status @@ -8873,7 +10001,7 @@ Kapcsolatkérés megismétlése? encryption agreed - titkosítás elfogadva + titkosítása elfogadva chat item text @@ -8883,32 +10011,32 @@ Kapcsolatkérés megismétlése? encryption ok - titkosítás rendben + titkosítása rendben van chat item text 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 @@ -8926,31 +10054,31 @@ Kapcsolatkérés megismétlése? hiba No comment provided by engineer. - - event happened - esemény történt - No comment provided by engineer. - expired lejárt No comment provided by engineer. - - for better metadata privacy. - a metaadatok jobb védelme érdekében. - No comment provided by engineer. - forwarded továbbított No comment provided by engineer. + + group + csoport + shown on group welcome message + group deleted a csoport törölve No comment provided by engineer. + + group is deleted + csoport törölve + No comment provided by engineer. + group profile updated csoportprofil frissítve @@ -8963,12 +10091,12 @@ Kapcsolatkérés megismétlése? 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. + Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a push-értesítések fogadását. No comment provided by engineer. iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. - Az iOS kulcstartó 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. @@ -8988,7 +10116,7 @@ Kapcsolatkérés megismétlése? incognito via one-time link - inkognitó egy egyszer használható meghívó-hivatkozáson keresztül + inkognitó egy egyszer használható meghívón keresztül chat list item description @@ -9013,7 +10141,7 @@ Kapcsolatkérés megismétlése? invitation to group %@ - meghívás a(z) %@ csoportba + meghívás a(z) %@ nevű csoportba group name @@ -9033,12 +10161,12 @@ Kapcsolatkérés megismétlése? invited to connect - meghívta, hogy csatlakozzon + függőben lévő kapcsolat chat list item title invited via your group link - meghíva az Ön csoporthivatkozásán keresztül + meghíva a saját csoporthivatkozásán keresztül rcv group event chat item @@ -9046,11 +10174,6 @@ Kapcsolatkérés megismétlése? dőlt No comment provided by engineer. - - join as %@ - csatlakozás mint: %@ - No comment provided by engineer. - left elhagyta a csoportot @@ -9068,7 +10191,7 @@ Kapcsolatkérés megismétlése? 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 @@ -9076,6 +10199,11 @@ Kapcsolatkérés megismétlése? kapcsolódott rcv group event chat item + + member has old version + a tag régi verziót használ + No comment provided by engineer. + message üzenet @@ -9103,23 +10231,23 @@ Kapcsolatkérés megismétlése? moderated by %@ - moderálva lett %@ által + %@ moderálta ezt az üzenetet marked deleted chat item preview text + + moderator + moderátor + member role + months hónap time unit - - mute - némítás - No comment provided by engineer. - never soha - No comment provided by engineer. + delete after time new message @@ -9136,11 +10264,21 @@ Kapcsolatkérés megismétlése? nincs e2e titkosítás No comment provided by engineer. + + no subscription + nincs előfizetés + No comment provided by engineer. + no text nincs szöveg copied message info in history + + not synchronized + nincs szinkronizálva + No comment provided by engineer. + observer megfigyelő @@ -9150,17 +10288,18 @@ Kapcsolatkérés megismétlése? off kikapcsolva enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ - %@ ajánlotta + felajánlotta a következőt: %@ feature offered item offered %1$@: %2$@ - ajánlotta %1$@: %2$@-kor + felajánlotta a következőt: %1$@: %2$@ feature offered item @@ -9190,12 +10329,27 @@ Kapcsolatkérés megismétlése? 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. + + + pending review + függőben lévő áttekintés 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 @@ -9208,6 +10362,11 @@ Kapcsolatkérés megismétlése? visszaigazolás fogadása… No comment provided by engineer. + + rejected + elutasítva + No comment provided by engineer. + rejected call elutasított hívás @@ -9228,6 +10387,11 @@ Kapcsolatkérés megismétlése? eltávolította a kapcsolattartási címet profile update event chat item + + removed from group + eltávolítva a csoportból + No comment provided by engineer. + removed profile picture eltávolította a profilképét @@ -9238,11 +10402,41 @@ Kapcsolatkérés megismétlése? eltávolította Önt rcv group event chat item + + request is sent + kérés elküldve + No comment provided by engineer. + + + request to join rejected + csatlakozási kérés elutasítva + No comment provided by engineer. + + + requested connection + partneri kapcsolatot kért + rcv group event chat item + + + requested connection from group %@ + a(z) %@ nevű csoportból partneri kapcsolatot kért + rcv direct event chat item + requested to connect - kérelmezve a kapcsolódáshoz + függőben lévő kapcsolat chat list item title + + review + áttekintés + No comment provided by engineer. + + + reviewed by admins + áttekintve a moderátorok által + No comment provided by engineer. + saved mentett @@ -9250,7 +10444,7 @@ Kapcsolatkérés megismétlése? saved from %@ - elmentve innen: %@ + mentve innen: %@ No comment provided by engineer. @@ -9275,26 +10469,21 @@ Kapcsolatkérés megismétlése? security code changed - a biztonsági kód megváltozott + biztonsági kódja módosult chat item text - - send direct message - közvetlen üzenet küldése - No comment provided by engineer. - server queue info: %1$@ last received msg: %2$@ - a kiszolgáló üzenet-sorbaállítási információi: %1$@ + a kiszolgáló várólista információi: %1$@ utoljára fogadott üzenet: %2$@ queue info set new contact address - új kapcsolattartási cím beállítása + új kapcsolattartási címet állított be profile update event chat item @@ -9319,7 +10508,7 @@ utoljára fogadott üzenet: %2$@ this contact - ez az ismerős + ez a partner notification title @@ -9334,7 +10523,7 @@ utoljára fogadott üzenet: %2$@ unknown servers - ismeretlen átjátszók + ismeretlen kiszolgálók No comment provided by engineer. @@ -9342,11 +10531,6 @@ utoljára fogadott üzenet: %2$@ ismeretlen állapot No comment provided by engineer. - - unmute - némítás megszüntetése - No comment provided by engineer. - unprotected nem védett @@ -9354,12 +10538,12 @@ utoljára fogadott üzenet: %2$@ updated group profile - frissítette a csoport profilját + frissítette a csoportprofilt rcv group event chat item updated profile - frissített profil + frissítette a profilját profile update event chat item @@ -9374,7 +10558,7 @@ utoljára fogadott üzenet: %2$@ via contact address link - kapcsolattartási cím-hivatkozáson keresztül + a kapcsolattartási címhivatkozáson keresztül chat list item description @@ -9384,12 +10568,12 @@ utoljára fogadott üzenet: %2$@ via one-time link - egyszer használható meghívó-hivatkozáson keresztül + egy egyszer használható meghívón keresztül chat list item description via relay - közvetítő-kiszolgálón keresztül + továbbítókiszolgálón keresztül No comment provided by engineer. @@ -9437,10 +10621,10 @@ utoljára fogadott üzenet: %2$@ Ön No comment provided by engineer. - - you are invited to group - meghívást kapott a csoportba - No comment provided by engineer. + + you accepted this member + Ön befogadta ezt a tagot + snd group event chat item you are observer @@ -9454,22 +10638,22 @@ utoljára fogadott üzenet: %2$@ you changed address - cím megváltoztatva + Ön módosította a címet chat item text you changed address for %@ - cím megváltoztatva nála: %@ + Ön módosította a címet %@ számára chat item text you changed role for yourself to %@ - saját szerepköre megváltozott 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$@ - Ön megváltoztatta %1$@ szerepkörét erre: %@ + Ön a következőre módosította %1$@ szerepkörét: „%2$@” snd group event chat item @@ -9484,12 +10668,12 @@ utoljára fogadott üzenet: %2$@ you shared one-time link - Ön egy egyszer használható meghívó-hivatkozást osztott meg + Ön egy egyszer használható meghívót osztott meg chat list item description you shared one-time link incognito - Ön egy egyszer használható meghívó-hivatkozást osztott meg inkognitóban + Ön egy egyszer használható meghívót osztott meg inkognitóban chat list item description @@ -9511,7 +10695,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -9531,7 +10715,7 @@ utoljára fogadott üzenet: %2$@ 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 @@ -9541,14 +10725,14 @@ utoljára fogadott üzenet: %2$@ SimpleX needs access to Photo Library for saving captured and received media - A SimpleXnek galéria-hozzáférésre van szüksége a rögzített és fogadott média mentéséhez + A SimpleXnek hozzáférésre van szüksége a galériához a rögzített és fogadott média mentéséhez Privacy - Photo Library Additions Usage Description
- +
@@ -9570,7 +10754,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -9578,6 +10762,11 @@ utoljára fogadott üzenet: %2$@ %d új esemény notification body + + From %d chat(s) + %d csevegésből + notification body + From: %@ Tőle: %@ @@ -9593,16 +10782,11 @@ utoljára fogadott üzenet: %2$@ Új üzenetek notification - - New messages in %d chats - Új üzenetek %d csevegésben - notification body -
- +
@@ -9624,7 +10808,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -9634,7 +10818,7 @@ utoljára fogadott üzenet: %2$@ App is locked! - Az alkalmazás zárolva! + Az alkalmazás zárolva van! No comment provided by engineer. @@ -9659,7 +10843,7 @@ utoljára fogadott üzenet: %2$@ Currently maximum supported file size is %@. - Jelenleg a maximálisan támogatott fájlméret: %@. + Jelenleg támogatott legnagyobb fájl méret: %@. No comment provided by engineer. @@ -9679,12 +10863,12 @@ utoljára fogadott üzenet: %2$@ Database passphrase is different from saved in the keychain. - Az adatbázis jelmondata eltér a kulcstartóban lévőtő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. @@ -9694,12 +10878,12 @@ utoljára fogadott üzenet: %2$@ Error preparing file - Hiba a fájl előkészítésekor + Hiba történt a fájl előkészítésekor No comment provided by engineer. Error preparing message - Hiba az üzenet előkészítésekor + Hiba történt az üzenet előkészítésekor No comment provided by engineer. @@ -9764,7 +10948,7 @@ utoljára fogadott üzenet: %2$@ Selected chat preferences prohibit this message. - A kiválasztott csevegési beállítások tiltják ezt az üzenetet. + A kijelölt csevegési beállítások tiltják ezt az üzenetet. No comment provided by engineer. @@ -9784,7 +10968,7 @@ utoljára fogadott üzenet: %2$@ Slow network? - Lassú internetkapcsolat? + Lassú a hálózata? No comment provided by engineer. @@ -9804,7 +10988,7 @@ utoljára fogadott üzenet: %2$@ Wrong database passphrase - Hibás adatbázis-jelmondat + Érvénytelen adatbázis-jelmondat No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json index 0b16198498..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 d785acda81..5f057cd8bb 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (può essere copiato) @@ -202,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 @@ -272,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 @@ -327,11 +315,6 @@ %u messaggi saltati. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nuovo) @@ -342,11 +325,6 @@ (questo dispositivo v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Aggiungi contatto**: per creare un nuovo link di invito. @@ -412,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). @@ -453,11 +426,6 @@ - cronologia delle modifiche. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -471,7 +439,8 @@ 1 day 1 giorno - time interval + delete after time +time interval 1 hour @@ -486,12 +455,19 @@ 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 @@ -518,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> @@ -579,6 +550,7 @@ About operators + Info sugli operatori No comment provided by engineer. @@ -590,8 +562,19 @@ Accept Accetta accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Accetta come membro + alert action + + + Accept as observer + Accetta come osservatore + alert action Accept conditions @@ -603,6 +586,11 @@ Accettare la richiesta di connessione? No comment provided by engineer. + + Accept contact request + Accetta la richiesta di contatto + alert title + Accept contact request from %@? Accettare la richiesta di contatto da %@? @@ -611,8 +599,13 @@ Accept incognito Accetta in incognito - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Accetta membro + alert title Accepted conditions @@ -629,6 +622,11 @@ Errori di riconoscimento No comment provided by engineer. + + Active + Attivo + token status text + Active connections Connessioni attive @@ -644,6 +642,16 @@ Aggiungi amici No comment provided by engineer. + + Add list + Aggiungi elenco + No comment provided by engineer. + + + Add message + Aggiungi un messaggio + placeholder for sending contact request + Add profile Aggiungi profilo @@ -669,6 +677,11 @@ 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 @@ -744,6 +757,11 @@ 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. @@ -754,6 +772,11 @@ 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. @@ -794,6 +817,16 @@ 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. @@ -834,6 +867,11 @@ Consenti downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + Consenti file e contenuti multimediali solo se il tuo contatto li consente. + 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) @@ -869,6 +907,11 @@ 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. @@ -914,6 +957,11 @@ Permetti ai tuoi contatti di inviare messaggi a tempo. No comment provided by engineer. + + Allow your contacts to send files and media. + Consenti ai tuoi contatti di inviare file e contenuti multimediali. + No comment provided by engineer. + Allow your contacts to send voice messages. Permetti ai tuoi contatti di inviare messaggi vocali. @@ -927,12 +975,12 @@ Already connecting! Già in connessione! - No comment provided by engineer. + new chat sheet title Already joining the group! Già in ingresso nel gruppo! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -949,6 +997,11 @@ 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 @@ -974,6 +1027,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 @@ -1019,6 +1077,21 @@ 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 @@ -1029,6 +1102,21 @@ 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 @@ -1099,11 +1187,6 @@ Auto-accetta le immagini No comment provided by engineer. - - Auto-accept settings - Accetta automaticamente le impostazioni - alert title - Back Indietro @@ -1139,6 +1222,11 @@ 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. @@ -1159,6 +1247,11 @@ Notifiche migliorate No comment provided by engineer. + + Better privacy and security + Privacy e sicurezza migliori + No comment provided by engineer. + Better security ✅ Sicurezza migliorata ✅ @@ -1169,6 +1262,16 @@ Esperienza utente migliorata No comment provided by engineer. + + Bio + Bio + No comment provided by engineer. + + + Bio too large + Bio troppo lunga + alert title + Black Nero @@ -1219,6 +1322,11 @@ Sfocatura dei file multimediali No comment provided by engineer. + + Bot + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi. @@ -1239,6 +1347,11 @@ Sia tu che il tuo contatto potete inviare messaggi a tempo. No comment provided by engineer. + + Both you and your contact can send files and media. + Sia tu che il tuo contatto potete inviare file e contenuti multimediali. + No comment provided by engineer. + Both you and your contact can send voice messages. Sia tu che il tuo contatto potete inviare messaggi vocali. @@ -1259,11 +1372,30 @@ Chat di lavoro No comment provided by engineer. + + Business connection + Connessione lavorativa + 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! @@ -1294,6 +1426,11 @@ Impossibile chiamare il membro No comment provided by engineer. + + Can't change profile + Impossibile cambiare profilo + alert title + Can't invite contact! Impossibile invitare il contatto! @@ -1313,7 +1450,8 @@ Cancel Annulla alert action - alert button +alert button +new chat action Cancel migration @@ -1350,6 +1488,11 @@ Cambia No comment provided by engineer. + + Change automatic message deletion? + Cambiare l'eliminazione automatica dei messaggi? + alert title + Change chat profiles Modifica profili utente @@ -1399,7 +1542,7 @@ Change self-destruct passcode Cambia codice di autodistruzione authentication reason - set passcode view +set passcode view Chat @@ -1414,7 +1557,7 @@ Chat already exists! La chat esiste già! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1501,11 +1644,31 @@ La chat verrà eliminata solo per te, non è reversibile! No comment provided by engineer. + + Chat with admins + Chat con amministratori + chat toolbar + + + Chat with member + Chatta con il membro + No comment provided by engineer. + + + Chat with members before they join. + Chatta con i membri prima che si uniscano. + No comment provided by engineer. + Chats Chat No comment provided by engineer. + + Chats with members + Chat con membri + No comment provided by engineer. + Check messages every 20 min. Controlla i messaggi ogni 20 min. @@ -1571,6 +1734,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? @@ -1591,6 +1764,11 @@ Modalità di colore No comment provided by engineer. + + Community guidelines violation + Violazione delle linee guida della comunità + report reason + Compare file Confronta file @@ -1624,17 +1802,7 @@ Conditions of use Condizioni d'uso - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Le condizioni verranno accettate per gli operatori: **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1656,6 +1824,11 @@ Configura server ICE No comment provided by engineer. + + Configure server operators + Configura gli operatori dei server + No comment provided by engineer. + Confirm Conferma @@ -1706,6 +1879,11 @@ Conferma caricamento No comment provided by engineer. + + Confirmed + Confermato + token status text + Connect Connetti @@ -1716,9 +1894,9 @@ Connetti automaticamente No comment provided by engineer. - - Connect incognito - Connetti in incognito + + Connect faster! 🚀 + Connettiti più velocemente! 🚀 No comment provided by engineer. @@ -1731,44 +1909,39 @@ Connettiti più velocemente ai tuoi amici. No comment provided by engineer. - - Connect to yourself? - Connettersi a te stesso? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Connettersi a te stesso? Questo è il tuo indirizzo SimpleX! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Connettersi a te stesso? Questo è il tuo link una tantum! - No comment provided by engineer. + new chat sheet title Connect via contact address Connettere via indirizzo del contatto - No comment provided by engineer. + new chat sheet title Connect via link Connetti via link - No comment provided by engineer. + new chat sheet title Connect via one-time link Connetti via link una tantum - No comment provided by engineer. + new chat sheet title Connect with %@ Connettersi con %@ - No comment provided by engineer. + new chat action Connected @@ -1825,16 +1998,33 @@ Questo è il tuo link una tantum! 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 - No comment provided by engineer. + alert title Connection error (AUTH) 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 @@ -1845,6 +2035,11 @@ Questo è il tuo link una tantum! 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 @@ -1858,7 +2053,7 @@ Questo è il tuo link una tantum! Connection timeout Connessione scaduta - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1910,6 +2105,11 @@ Questo è il tuo link una tantum! Preferenze del contatto No comment provided by engineer. + + Contact requests from groups + Richieste di contatto dai gruppi + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Il contatto verrà eliminato - non è reversibile! @@ -1925,6 +2125,11 @@ 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 @@ -2000,6 +2205,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/). 💻 @@ -2015,9 +2225,9 @@ Questo è il tuo link una tantum! Crea coda server test step - - Create secret group - Crea gruppo segreto + + Create your address + Crea il tuo indirizzo No comment provided by engineer. @@ -2217,8 +2427,7 @@ Questo è il tuo link una tantum! Delete Elimina alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2260,6 +2469,11 @@ Questo è il tuo link una tantum! Elimina chat No comment provided by engineer. + + 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 @@ -2270,6 +2484,11 @@ Questo è il tuo link una tantum! Eliminare il profilo di chat? No comment provided by engineer. + + Delete chat with member? + Eliminare la chat con il membro? + alert title + Delete chat? Eliminare la chat? @@ -2350,6 +2569,11 @@ Questo è il tuo link una tantum! Eliminare il link? No comment provided by engineer. + + Delete list? + Eliminare l'elenco? + alert title + Delete member message? Eliminare il messaggio del membro? @@ -2363,7 +2587,7 @@ Questo è il tuo link una tantum! Delete messages Elimina messaggi - No comment provided by engineer. + alert button Delete messages after @@ -2400,6 +2624,11 @@ Questo è il tuo link una tantum! 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. @@ -2455,11 +2684,21 @@ Questo è il tuo link una tantum! Ricevute di consegna! No comment provided by engineer. + + Deprecated options + Opzioni deprecate + No comment provided by engineer. + Description Descrizione No comment provided by engineer. + + Description too large + Descrizione troppo lunga + alert title + Desktop address Indirizzo desktop @@ -2560,6 +2799,16 @@ Questo è il tuo link una tantum! 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 @@ -2650,6 +2899,11 @@ Questo è il tuo link una tantum! 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 @@ -2660,9 +2914,19 @@ Questo è il tuo link una tantum! 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ù + alert action + + + Done + Fatto No comment provided by engineer. @@ -2674,7 +2938,7 @@ Questo è il tuo link una tantum! Download Scarica alert button - chat item action +chat item action Download errors @@ -2741,6 +3005,11 @@ Questo è il tuo link una tantum! Modifica il profilo del gruppo No comment provided by engineer. + + Empty message! + Messaggio vuoto! + No comment provided by engineer. + Enable Attiva @@ -2751,9 +3020,9 @@ Questo è il tuo link una tantum! Attiva (mantieni sostituzioni) No comment provided by engineer. - - Enable Flux - Attiva Flux + + 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. @@ -2769,13 +3038,18 @@ Questo è il tuo link una tantum! Enable automatic message deletion? Attivare l'eliminazione automatica dei messaggi? - No comment provided by engineer. + alert title Enable camera access Attiva l'accesso alla fotocamera No comment provided by engineer. + + Enable disappearing messages by default. + Attiva i messaggi a tempo in modo predefinito. + No comment provided by engineer. + Enable for all Attiva per tutti @@ -2896,6 +3170,11 @@ Questo è il tuo link una tantum! 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 @@ -2971,6 +3250,11 @@ Questo è il tuo link una tantum! Errore nell'accettazione della richiesta di contatto No comment provided by engineer. + + Error accepting member + Errore di accettazione del membro + alert title + Error adding member(s) Errore di aggiunta membro/i @@ -2981,11 +3265,21 @@ Questo è il tuo link una tantum! Errore di aggiunta del server alert title + + Error adding short link + Errore di aggiunta link breve + No comment provided by engineer. + Error changing address Errore nella modifica dell'indirizzo No comment provided by engineer. + + Error changing chat profile + Errore cambiando il profilo di chat + alert title + Error changing connection profile Errore nel cambio di profilo di connessione @@ -2999,17 +3293,27 @@ Questo è il tuo link una tantum! Error changing setting Errore nella modifica dell'impostazione - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + Errore di connessione al server usato per ricevere messaggi da questa connessione: %@ + subscription status explanation Error creating address @@ -3026,6 +3330,11 @@ Questo è il tuo link una tantum! 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 @@ -3041,20 +3350,30 @@ Questo è il tuo link una tantum! 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 No comment provided by engineer. + + Error deleting chat + Errore di eliminazione della chat con il membro + alert title + Error deleting chat database Errore nell'eliminazione del database della chat - No comment provided by engineer. + alert title Error deleting chat! Errore nell'eliminazione della chat! - No comment provided by engineer. + alert title Error deleting connection @@ -3064,12 +3383,12 @@ Questo è il tuo link una tantum! Error deleting database Errore nell'eliminazione del database - No comment provided by engineer. + alert title Error deleting old database Errore nell'eliminazione del database vecchio - No comment provided by engineer. + alert title Error deleting token @@ -3104,7 +3423,7 @@ Questo è il tuo link una tantum! Error exporting chat database Errore nell'esportazione del database della chat - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3114,7 +3433,7 @@ Questo è il tuo link una tantum! Error importing chat database Errore nell'importazione del database della chat - No comment provided by engineer. + alert title Error joining group @@ -3136,6 +3455,11 @@ Questo è il tuo link una tantum! Errore di apertura della chat No comment provided by engineer. + + Error opening group + Errore di preparazione del gruppo + No comment provided by engineer. + Error receiving file Errore nella ricezione del file @@ -3151,10 +3475,25 @@ Questo è il tuo link una tantum! Errore di riconnessione ai server No comment provided by engineer. + + Error registering for notifications + Errore di registrazione per le notifiche + alert title + + + Error rejecting contact request + Errore nel rifiuto della richiesta di contatto + alert title + Error removing member Errore nella rimozione del membro - No comment provided by engineer. + alert title + + + Error reordering lists + Errore riordinando gli elenchi + alert title Error resetting statistics @@ -3166,6 +3505,11 @@ Questo è il tuo link una tantum! 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 @@ -3216,6 +3560,11 @@ Questo è il tuo link una tantum! Errore nell'invio del messaggio No comment provided by engineer. + + Error setting auto-accept + Errore impostando l'accettazione automatica + No comment provided by engineer. + Error setting delivery receipts! Errore nell'impostazione delle ricevute di consegna! @@ -3234,7 +3583,7 @@ Questo è il tuo link una tantum! Error switching profile Errore nel cambio di profilo - No comment provided by engineer. + alert title Error switching profile! @@ -3246,6 +3595,11 @@ Questo è il tuo link una tantum! 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 @@ -3289,7 +3643,14 @@ Questo è il tuo link una tantum! Error: %@ Errore: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + Errore: %@. + server test error Error: URL is invalid @@ -3326,6 +3687,11 @@ Questo è il tuo link una tantum! Espandi chat item action + + Expired + Scaduto + token status text + Export database Esporta database @@ -3366,20 +3732,35 @@ Questo è il tuo link una tantum! 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 - No comment provided by engineer. + file error alert title File errors: @@ -3388,6 +3769,13 @@ Questo è il tuo link una tantum! %@ 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. @@ -3443,6 +3831,11 @@ Questo è il tuo link una tantum! File e multimediali chat feature + + Files and media are prohibited in this chat. + File e contenuti multimediali sono vietati in questa chat. + No comment provided by engineer. + Files and media are prohibited. File e contenuti multimediali sono vietati in questo gruppo. @@ -3483,6 +3876,26 @@ Questo è il tuo link una tantum! Trova le chat più velocemente No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + L'impronta digitale nell'indirizzo del server di destinazione non corrisponde al certificato: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + L'impronta digitale nell'indirizzo del server di inoltro non corrisponde al certificato: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + L'impronta digitale nell'indirizzo del server non corrisponde al certificato. + server test error + + + Fingerprint in server address does not match certificate: %@. + L'impronta digitale nell'indirizzo del server non corrisponde al certificato: %@. + No comment provided by engineer. + Fix Correggi @@ -3513,6 +3926,11 @@ Questo è il tuo link una tantum! 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 %@: @@ -3528,6 +3946,11 @@ Questo è il tuo link una tantum! 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 @@ -3584,9 +4007,9 @@ Questo è il tuo link una tantum! 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 %1$@ failed to connect to destination server %2$@. Please try later. + Il server di inoltro %1$@ non è riuscito a connettersi al server di destinazione %2$@. Riprova più tardi. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3652,6 +4075,11 @@ Errore: %2$@ 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! @@ -3675,7 +4103,7 @@ Errore: %2$@ Group already exists! Il gruppo esiste già! - No comment provided by engineer. + new chat sheet title Group display name @@ -3742,6 +4170,11 @@ Errore: %2$@ Il profilo del gruppo è memorizzato sui dispositivi dei membri, non sui server. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + Il profilo del gruppo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato ai membri del gruppo. + alert message + Group welcome message Messaggio di benvenuto del gruppo @@ -3757,11 +4190,21 @@ Errore: %2$@ 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 @@ -3822,6 +4265,11 @@ Errore: %2$@ Come aiuta la privacy No comment provided by engineer. + + How it works + Come funziona + alert button + How to Come si fa @@ -3964,6 +4412,16 @@ Altri miglioramenti sono in arrivo! Suoni nelle chiamate No comment provided by engineer. + + Inappropriate content + Contenuto inappropriato + report reason + + + Inappropriate profile + Profilo inappropriato + report reason + Incognito Incognito @@ -4056,6 +4514,31 @@ Altri miglioramenti sono in arrivo! 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 @@ -4074,7 +4557,7 @@ Altri miglioramenti sono in arrivo! Invalid link Link non valido - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4187,37 +4670,32 @@ Altri miglioramenti sono in arrivo! Entra swipe action + + Join as %@ + entra come %@ + No comment provided by engineer. + Join group Entra nel gruppo - No comment provided by engineer. + new chat sheet title Join group conversations Entra in conversazioni di gruppo No comment provided by engineer. - - Join group? - Entrare nel gruppo? - No comment provided by engineer. - Join incognito Entra in incognito No comment provided by engineer. - - Join with current profile - Entra con il profilo attuale - No comment provided by engineer. - Join your group? This is your link for group %@! Entrare nel tuo gruppo? Questo è il tuo link per il gruppo %@! - No comment provided by engineer. + new chat action Joining group @@ -4244,6 +4722,11 @@ Questo è il tuo link per il gruppo %@! Tenere l'invito inutilizzato? alert title + + Keep your chats clean + Mantieni le chat pulite + No comment provided by engineer. + Keep your connections Mantieni le tue connessioni @@ -4299,6 +4782,11 @@ Questo è il tuo link per il gruppo %@! Uscire dal gruppo? No comment provided by engineer. + + Less traffic on mobile networks. + Meno traffico sulle reti mobili. + No comment provided by engineer. + Let's talk in SimpleX Chat Parliamo in SimpleX Chat @@ -4329,6 +4817,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! @@ -4339,6 +4842,11 @@ Questo è il tuo link per il gruppo %@! Messaggi in diretta No comment provided by engineer. + + Loading profile… + Caricamento del profilo… + in progress text + Local name Nome locale @@ -4414,11 +4922,31 @@ Questo è il tuo link per il gruppo %@! Membro No comment provided by engineer. + + Member %@ + Membro %@ + past/unknown group member + + + Member admission + Ammissione dei membri + No comment provided by engineer. + Member inactive Membro inattivo item status text + + Member is deleted - can't accept request + Il membro è eliminato - impossibile accettare la richiesta + No comment provided by engineer. + + + 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. @@ -4444,6 +4972,11 @@ Questo è il tuo link per il gruppo %@! Il membro verrà rimosso dal gruppo, non è reversibile! No comment provided by engineer. + + Member will join the group, accept member? + Il membro entrerà nel gruppo, accettarlo? + alert message + Members can add message reactions. I membri del gruppo possono aggiungere reazioni ai messaggi. @@ -4454,6 +4987,11 @@ Questo è il tuo link per il gruppo %@! 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. @@ -4479,6 +5017,11 @@ Questo è il tuo link per il gruppo %@! 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 @@ -4509,6 +5052,11 @@ Questo è il tuo link per il gruppo %@! Messaggio inoltrato item status text + + Message instantly once you tap Connect. + Parla immediatamente appena tocchi Connetti. + No comment provided by engineer. + Message may be delivered later if member becomes active. Il messaggio può essere consegnato più tardi se il membro diventa attivo. @@ -4584,11 +5132,21 @@ Questo è il tuo link per il gruppo %@! Messaggi No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + I messaggi sono protetti da **crittografia end-to-end**. + No comment provided by engineer. + Messages from %@ will be shown! 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 @@ -4689,6 +5247,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! @@ -4717,7 +5280,12 @@ Questo è il tuo link per il gruppo %@! Mute Silenzia - swipe action + notification label action + + + Mute all + Silenzia tutto + notification label action Muted when inactive! @@ -4767,7 +5335,12 @@ Questo è il tuo link per il gruppo %@! Network status Stato della rete - No comment provided by engineer. + alert title + + + New + Nuovo + token status text New Passcode @@ -4819,6 +5392,11 @@ Questo è il tuo link per il gruppo %@! Nuovi eventi notification + + New group role: Moderator + Nuovo ruolo nei gruppi: Moderatore + No comment provided by engineer. + New in %@ Novità nella %@ @@ -4834,6 +5412,11 @@ Questo è il tuo link per il gruppo %@! Nuovo ruolo del membro No comment provided by engineer. + + New member wants to join the group. + Un nuovo membro vuole entrare nel gruppo. + rcv group event chat item + New message Nuovo messaggio @@ -4859,6 +5442,26 @@ 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 chats with members + Nessuna chat con membri + No comment provided by engineer. + No contacts selected Nessun contatto selezionato @@ -4909,6 +5512,11 @@ Questo è il tuo link per il gruppo %@! Nessun server di multimediali e file. servers error + + No message + Nessun messaggio + No comment provided by engineer. + No message servers. Nessun server dei messaggi. @@ -4934,6 +5542,11 @@ Questo è il tuo link per il gruppo %@! Nessuna autorizzazione per registrare messaggi vocali No comment provided by engineer. + + No private routing session + Nessuna sessione di instradamento privato + alert title + No push server Locale @@ -4964,6 +5577,16 @@ Questo è il tuo link per il gruppo %@! 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. @@ -4974,6 +5597,11 @@ Questo è il tuo link per il gruppo %@! Non compatibile! No comment provided by engineer. + + Notes + Note + No comment provided by engineer. + Nothing selected Nessuna selezione @@ -4994,11 +5622,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. @@ -5021,7 +5659,9 @@ Questo è il tuo link per il gruppo %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5082,6 +5722,16 @@ Richiede l'attivazione della VPN. 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. @@ -5102,6 +5752,11 @@ Richiede l'attivazione della VPN. Solo tu puoi inviare messaggi a tempo. No comment provided by engineer. + + Only you can send files and media. + Solo tu puoi inviare file e contenuti multimediali. + No comment provided by engineer. + Only you can send voice messages. Solo tu puoi inviare messaggi vocali. @@ -5127,6 +5782,11 @@ Richiede l'attivazione della VPN. Solo il tuo contatto può inviare messaggi a tempo. No comment provided by engineer. + + Only your contact can send files and media. + Solo il tuo contatto può inviare file e contenuti multimediali. + No comment provided by engineer. + Only your contact can send voice messages. Solo il tuo contatto può inviare messaggi vocali. @@ -5135,7 +5795,7 @@ Richiede l'attivazione della VPN. Open Apri - No comment provided by engineer. + alert action Open Settings @@ -5150,28 +5810,73 @@ Richiede l'attivazione della VPN. Open chat Apri chat - No comment provided by engineer. + new chat action Open chat console Apri la console della chat authentication reason + + Open clean link + Apri link pulito + alert action + Open conditions Apri le condizioni No comment provided by engineer. + + Open full link + Apri link completo + alert action + Open group Apri gruppo - No comment provided by engineer. + new chat action + + + Open link? + Aprire il link? + alert title Open migration to another device Apri migrazione ad un altro dispositivo authentication reason + + Open new chat + Apri una chat nuova + new chat action + + + Open new group + Apri un gruppo nuovo + new chat action + + + Open to accept + Apri per accettare + No comment provided by engineer. + + + Open to connect + Apri per connettere + No comment provided by engineer. + + + Open to join + Apri per entrare + No comment provided by engineer. + + + Open to use bot + Apri per usare il bot + No comment provided by engineer. + Opening app… Apertura dell'app… @@ -5217,6 +5922,11 @@ Richiede l'attivazione della VPN. 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 @@ -5274,11 +5984,6 @@ Richiede l'attivazione della VPN. Password per mostrare No comment provided by engineer. - - Past member %@ - Membro passato %@ - past/unknown group member - Paste desktop address Incolla l'indirizzo desktop @@ -5349,7 +6054,7 @@ Si prega di condividere qualsiasi altro problema con gli sviluppatori. Please check your network connection with %@ and try again. Controlla la tua connessione di rete con %@ e riprova. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5408,6 +6113,26 @@ 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 group moderators to review your request to join the group. + Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo. + snd group event chat item + + + 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 @@ -5418,11 +6143,6 @@ Errore: %@ Porta No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata - server test error - Preserve the last message draft, with attachments. Conserva la bozza dell'ultimo messaggio, con gli allegati. @@ -5458,16 +6178,31 @@ Errore: %@ 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 @@ -5491,7 +6226,12 @@ Errore: %@ Private routing error Errore di instradamento privato - No comment provided by engineer. + alert title + + + Private routing timeout + Scadenza dell'instradamento privato + alert title Profile and server connections @@ -5543,6 +6283,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. @@ -5590,6 +6335,11 @@ Attivalo nelle impostazioni *Rete e server*. Proteggi i tuoi profili di chat con una password! No comment provided by engineer. + + Protocol background timeout + Scadenza del protocollo in sec. piano + No comment provided by engineer. + Protocol timeout Scadenza del protocollo @@ -5695,11 +6445,6 @@ Attivalo nelle impostazioni *Rete e server*. Ricevuto il: %@ copied message info - - Received file event - Evento file ricevuto - notification - Received message Messaggio ricevuto @@ -5800,11 +6545,27 @@ Attivalo nelle impostazioni *Rete e server*. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5814,7 +6575,12 @@ Attivalo nelle impostazioni *Rete e server*. Reject contact request Rifiuta la richiesta di contatto - No comment provided by engineer. + alert title + + + Reject member? + Rifiutare il membro? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5841,6 +6607,11 @@ Attivalo nelle impostazioni *Rete e server*. Rimuovi immagine No comment provided by engineer. + + Remove link tracking + Rimuovi il tracciamento del link + No comment provided by engineer. + Remove member Rimuovi membro @@ -5856,6 +6627,11 @@ Attivalo nelle impostazioni *Rete e server*. Rimuovere la password dal portachiavi? No comment provided by engineer. + + Removes messages and blocks members. + Rimuove i messaggi e blocca i membri. + No comment provided by engineer. + Renegotiate Rinegoziare @@ -5871,11 +6647,6 @@ Attivalo nelle impostazioni *Rete e server*. Rinegoziare la crittografia? No comment provided by engineer. - - Repeat connection request? - Ripetere la richiesta di connessione? - No comment provided by engineer. - Repeat download Ripeti scaricamento @@ -5886,11 +6657,6 @@ Attivalo nelle impostazioni *Rete e server*. Ripeti importazione No comment provided by engineer. - - Repeat join request? - Ripetere la richiesta di ingresso? - No comment provided by engineer. - Repeat upload Ripeti caricamento @@ -5901,6 +6667,61 @@ Attivalo nelle impostazioni *Rete e server*. 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 sent to moderators + Segnalazione inviata ai moderatori + alert title + + + 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 @@ -5979,7 +6800,7 @@ Attivalo nelle impostazioni *Rete e server*. Retry Riprova - No comment provided by engineer. + alert action Reveal @@ -5991,11 +6812,21 @@ Attivalo nelle impostazioni *Rete e server*. Leggi le condizioni No comment provided by engineer. - - Review later - Leggi più tardi + + Review group members + Revisiona i membri del gruppo No comment provided by engineer. + + Review members + Revisiona i membri + admission stage + + + Review members before admitting ("knocking"). + Revisiona i membri prima di ammetterli ("bussare"). + admission stage description + Revoke Revoca @@ -6045,13 +6876,23 @@ Attivalo nelle impostazioni *Rete e server*. Save Salva alert button - chat item action +chat item action Save (and notify contacts) Salva (e avvisa i contatti) alert button + + Save (and notify members) + Salva (e informa i membri) + alert button + + + Save admission settings? + Salvare le impostazioni di ammissione? + alert title + Save and notify contact Salva e avvisa il contatto @@ -6077,6 +6918,16 @@ Attivalo nelle impostazioni *Rete e server*. Salva il profilo del gruppo No comment provided by engineer. + + Save group profile? + Salvare il profilo del gruppo? + alert title + + + Save list + Salva elenco + No comment provided by engineer. + Save passphrase and open chat Salva la password e apri la chat @@ -6267,6 +7118,11 @@ Attivalo nelle impostazioni *Rete e server*. Invia un messaggio in diretta: si aggiornerà per i destinatari mentre lo digiti No comment provided by engineer. + + Send contact request? + Inviare una richiesta di contatto? + No comment provided by engineer. + Send delivery receipts to Invia ricevute di consegna a @@ -6317,6 +7173,11 @@ Attivalo nelle impostazioni *Rete e server*. Invia notifiche No comment provided by engineer. + + Send private reports + Invia segnalazioni private + No comment provided by engineer. + Send questions and ideas Invia domande e idee @@ -6327,6 +7188,16 @@ Attivalo nelle impostazioni *Rete e server*. Invia ricevute No comment provided by engineer. + + Send request + Invia richiesta + No comment provided by engineer. + + + Send request without message + Invia richiesta senza messaggio + No comment provided by engineer. + Send them from gallery or custom keyboards. Inviali dalla galleria o dalle tastiere personalizzate. @@ -6337,6 +7208,11 @@ Attivalo nelle impostazioni *Rete e server*. Invia fino a 100 ultimi messaggi ai nuovi membri. No comment provided by engineer. + + Send your private feedback to groups. + Invia i tuoi commenti privati ai gruppi. + No comment provided by engineer. + Sender cancelled file transfer. Il mittente ha annullato il trasferimento del file. @@ -6402,11 +7278,6 @@ Attivalo nelle impostazioni *Rete e server*. Inviato direttamente No comment provided by engineer. - - Sent file event - Evento file inviato - notification - Sent message Messaggio inviato @@ -6477,14 +7348,14 @@ Attivalo nelle impostazioni *Rete e server*. 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 + + Server requires authorization to create queues, check password. + Il server richiede l'autorizzazione di creare code, controlla la password. server test error - - Server requires authorization to upload, check password - Il server richiede l'autorizzazione per il caricamento, controllare la password + + Server requires authorization to upload, check password. + Il server richiede l'autorizzazione per l'invio, controlla la password. server test error @@ -6532,6 +7403,11 @@ Attivalo nelle impostazioni *Rete e server*. 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… @@ -6552,6 +7428,16 @@ Attivalo nelle impostazioni *Rete e server*. Impostalo al posto dell'autenticazione di sistema. No comment provided by engineer. + + Set member admission + Imposta l'ammissione dei membri + 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 @@ -6567,6 +7453,11 @@ Attivalo nelle impostazioni *Rete e server*. Imposta la password per esportare No comment provided by engineer. + + Set profile bio and welcome message. + Imposta la bio del profilo e il messaggio di benvenuto. + No comment provided by engineer. + Set the message shown to new members! Imposta il messaggio mostrato ai nuovi membri! @@ -6596,7 +7487,7 @@ Attivalo nelle impostazioni *Rete e server*. Share Condividi alert action - chat item action +chat item action Share 1-time link @@ -6638,6 +7529,16 @@ Attivalo nelle impostazioni *Rete e server*. Condividi link No comment provided by engineer. + + Share old address + Condividi l'indirizzo vecchio + alert button + + + Share old link + Condividi il link completo + alert button + Share profile Condividi il profilo @@ -6658,6 +7559,26 @@ Attivalo nelle impostazioni *Rete e server*. Condividi con i contatti No comment provided by engineer. + + Share your address + Condividi il tuo indirizzo + No comment provided by engineer. + + + Short SimpleX address + Indirizzo breve di SimpleX + No comment provided by engineer. + + + Short description + Descrizione breve + No comment provided by engineer. + + + Short link + Link breve + No comment provided by engineer. + Show QR code Mostra codice QR @@ -6715,7 +7636,7 @@ Attivalo nelle impostazioni *Rete e server*. 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 + SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell'app. No comment provided by engineer. @@ -6758,6 +7679,16 @@ Attivalo nelle impostazioni *Rete e server*. Indirizzo SimpleX o link una tantum? No comment provided by engineer. + + SimpleX address settings + Accetta automaticamente le impostazioni + alert title + + + SimpleX channel link + Link del canale SimpleX + simplex link type + SimpleX contact address Indirizzo di contatto SimpleX @@ -6798,6 +7729,11 @@ Attivalo nelle impostazioni *Rete e server*. Protocolli di SimpleX esaminati da Trail of Bits. No comment provided by engineer. + + SimpleX relay link + Link del relay SimpleX + simplex link type + Simplified incognito mode Modalità incognito semplificata @@ -6860,6 +7796,12 @@ Attivalo nelle impostazioni *Rete e server*. Qualcuno notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Quadrata, circolare o qualsiasi forma tra le due. @@ -6945,6 +7887,11 @@ Attivalo nelle impostazioni *Rete e server*. Arresto della chat No comment provided by engineer. + + Storage + Archiviazione + No comment provided by engineer. + Strong Forte @@ -7000,11 +7947,21 @@ Attivalo nelle impostazioni *Rete e server*. Connessione TCP No comment provided by engineer. + + TCP connection bg timeout + Scadenza conness. TCP in sec. piano + 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 @@ -7030,11 +7987,31 @@ Attivalo nelle impostazioni *Rete e server*. Scatta foto No comment provided by engineer. + + Tap Connect to chat + Tocca Connetti per chattare + No comment provided by engineer. + + + Tap Connect to send request + Tocca Connetti per inviare la richiesta + No comment provided by engineer. + + + Tap Connect to use bot + Tocca Connetti per usare il bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi. No comment provided by engineer. + + Tap Join group + Tocca Entra nel gruppo + No comment provided by engineer. + Tap button Tocca il pulsante @@ -7073,13 +8050,18 @@ Attivalo nelle impostazioni *Rete e server*. Temporary file error Errore del file temporaneo - No comment provided by engineer. + 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 @@ -7117,6 +8099,11 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + L'indirizzo sarà breve e il tuo profilo verrà condiviso attraverso l'indirizzo. + alert message + 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. @@ -7177,6 +8164,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.L'hash del messaggio precedente è diverso. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + Il link sarà breve e il profilo del gruppo verrà condiviso attraverso il link. + alert message + The message will be deleted for all members. Il messaggio verrà eliminato per tutti i membri. @@ -7202,21 +8194,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato. No comment provided by engineer. - - The profile is 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 same conditions will apply to operator(s): **%@**. - Le stesse condizioni si applicheranno agli operatori **%@**. - No comment provided by engineer. - The second preset operator in the app! Il secondo operatore preimpostato nell'app! @@ -7230,7 +8212,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. The sender will NOT be notified Il mittente NON verrà avvisato - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7282,6 +8264,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. @@ -7317,14 +8304,9 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questo gruppo non esiste più. No comment provided by engineer. - - This is your own SimpleX address! - Questo è il tuo indirizzo SimpleX! - No comment provided by engineer. - - - This is your own one-time link! - Questo è il tuo link una tantum! + + 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. @@ -7332,11 +8314,26 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.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. + + This setting is for your current profile **%@**. + Questa impostazione è per il tuo profilo attuale **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + Il tempo di scomparsa è impostato solo per i contatti nuovi. + No comment provided by engineer. + Title Titoli @@ -7419,11 +8416,21 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per inviare No comment provided by engineer. + + To send commands you must be connected. + Per inviare comandi devi essere connesso/a. + alert message + To support instant push notifications the chat database has to be migrated. Per supportare le notifiche push istantanee, il database della chat deve essere migrato. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + Per usare un altro profilo dopo il tentativo di connessione, elimina la chat e usa di nuovo il link. + alert message + To use the servers of **%@**, accept conditions of use. Per usare i server di **%@**, accetta le condizioni d'uso. @@ -7444,6 +8451,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio 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 @@ -7464,15 +8476,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio 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: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Tentativo di connessione al server usato per ricevere messaggi da questo contatto. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + Tentativo di connessione al server usato per ricevere messaggi da questa connessione. + subscription status explanation Turkish interface @@ -7609,13 +8616,18 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Unmute Riattiva notifiche - swipe action + notification label action Unread Non letto swipe action + + 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. @@ -7641,16 +8653,51 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Aggiornare le impostazioni? No comment provided by engineer. + + Updated conditions + Condizioni aggiornate + No comment provided by engineer. + Updating settings will re-connect the client to all servers. L'aggiornamento delle impostazioni riconnetterà il client a tutti i server. No comment provided by engineer. + + Upgrade + Aggiorna + alert button + + + Upgrade address + Aggiorna l'indirizzo + No comment provided by engineer. + + + Upgrade address? + Aggiornare l'indirizzo? + alert message + Upgrade and open chat Aggiorna e apri chat No comment provided by engineer. + + Upgrade group link? + Aggiornare il link del gruppo? + alert message + + + Upgrade link + Aggiungi link + No comment provided by engineer. + + + Upgrade your address + Aggiorna il tuo indirizzo + No comment provided by engineer. + Upload errors Errori di invio @@ -7701,6 +8748,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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 @@ -7709,7 +8766,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use current profile Usa il profilo attuale - No comment provided by engineer. + new chat action Use for files @@ -7736,10 +8793,15 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa interfaccia di chiamata iOS No comment provided by engineer. + + Use incognito profile + Usa profilo in incognito + No comment provided by engineer. + Use new incognito profile Usa nuovo profilo in incognito - No comment provided by engineer. + new chat action Use only local notifications? @@ -7776,6 +8838,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa l'app con una mano sola. No comment provided by engineer. + + Use web port + Usa porta web + No comment provided by engineer. + User selection Selezione utente @@ -7966,6 +9033,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Il messaggio di benvenuto è troppo lungo No comment provided by engineer. + + Welcome your contacts 👋 + Dai il benvenuto ai tuoi contatti 👋 + No comment provided by engineer. + What's new Novità @@ -8089,12 +9161,12 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e You are already connecting to %@. Ti stai già connettendo a %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Ti stai già connettendo tramite questo link una tantum! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8104,35 +9176,35 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e You are already joining the group %@. Stai già entrando nel gruppo %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Stai già entrando nel gruppo tramite questo link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Stai già entrando nel gruppo tramite questo link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Stai già entrando nel gruppo! Ripetere la richiesta di ingresso? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Sei connesso/a al server usato per ricevere messaggi da questo contatto. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + Sei connesso/a al server usato per ricevere messaggi da questa connessione. + subscription status explanation You are invited to group Sei stato/a invitato/a al gruppo No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + Non sei connesso/a al server usato per ricevere messaggi da questa connessione (nessuna iscrizione). + subscription status explanation + 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. @@ -8148,11 +9220,6 @@ Ripetere la richiesta di ingresso? Puoi cambiarlo nelle impostazioni dell'aspetto. No comment provided by engineer. - - You can configure operators in Network & servers settings. - Puoi configurare gli operatori nelle impostazioni di rete e server. - No comment provided by engineer. - You can configure servers via settings. Puoi configurare i server nelle impostazioni. @@ -8243,10 +9310,15 @@ Ripetere la richiesta di ingresso? Puoi vedere di nuovo il link di invito nei dettagli di connessione. alert message + + You can view your reports in Chat with admins. + Puoi vedere le tue segnalazioni nella chat con gli amministratori. + alert message + You can't send messages! Non puoi inviare messaggi! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8258,17 +9330,12 @@ Ripetere la richiesta di ingresso? 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! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Hai già richiesto la connessione! Ripetere la richiesta di connessione? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8325,6 +9392,16 @@ 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 able to send messages **only after your request is accepted**. + Potrai inviare messaggi **solo dopo che la tua richiesta verrà accettata**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi! @@ -8350,11 +9427,6 @@ Ripetere la richiesta di connessione? Dovrai autenticarti quando avvii o riapri l'app dopo 30 secondi in secondo piano. No comment provided by engineer. - - You will connect to all group members. - Ti connetterai a tutti i membri del gruppo. - 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. @@ -8390,16 +9462,16 @@ Ripetere la richiesta di connessione? 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 business contact + Il tuo contatto lavorativo + No comment provided by engineer. + Your calls Le tue chiamate @@ -8425,11 +9497,21 @@ Ripetere la richiesta di connessione? I tuoi profili di chat No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + La tua chat è stata spostata su %@ , ma si è verificato un errore imprevisto mentre venivi reindirizzato/a al profilo. + alert message + + + Your connection was moved to %@ but an error happened when switching 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 + Il tuo contatto + 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 (%@). @@ -8460,6 +9542,11 @@ Ripetere la richiesta di connessione? Il tuo profilo attuale No comment provided by engineer. + + Your group + Il tuo gruppo + No comment provided by engineer. + Your preferences Le tue preferenze @@ -8480,6 +9567,11 @@ Ripetere la richiesta di connessione? Verrà condiviso il tuo profilo **%@**. No comment provided by engineer. + + 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 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. @@ -8490,11 +9582,6 @@ Ripetere la richiesta di connessione? Il tuo profilo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato a tutti i tuoi contatti. alert message - - Your profile, contacts and delivered messages are stored on your device. - Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo. - No comment provided by engineer. - Your random profile Il tuo profilo casuale @@ -8545,6 +9632,11 @@ Ripetere la richiesta di connessione? sopra, quindi scegli: No comment provided by engineer. + + accepted %@ + %@ accettato + rcv group event chat item + accepted call chiamata accettata @@ -8552,8 +9644,14 @@ Ripetere la richiesta di connessione? accepted invitation + invito accettato chat list item title + + accepted you + ti ha accettato/a + rcv group event chat item + admin amministratore @@ -8574,6 +9672,11 @@ Ripetere la richiesta di connessione? concordando la crittografia… chat item text + + all + tutti + member criteria value + all members tutti i membri @@ -8589,6 +9692,11 @@ Ripetere la richiesta di connessione? e altri %lld eventi No comment provided by engineer. + + archived report + segnalazione archiviata + No comment provided by engineer. + attempts tentativi @@ -8627,7 +9735,8 @@ Ripetere la richiesta di connessione? blocked by admin bloccato dall'amministratore - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8654,6 +9763,11 @@ Ripetere la richiesta di connessione? chiamata… call status + + can't send messages + impossibile inviare messaggi + No comment provided by engineer. + cancelled %@ annullato %@ @@ -8704,11 +9818,6 @@ Ripetere la richiesta di connessione? connesso/a No comment provided by engineer. - - connected directly - si è connesso/a direttamente - rcv group event chat item - connecting in connessione @@ -8759,6 +9868,16 @@ Ripetere la richiesta di connessione? contatto %1$@ cambiato in %2$@ profile update event chat item + + contact deleted + contatto eliminato + No comment provided by engineer. + + + contact disabled + contatto disattivato + No comment provided by engineer. + contact has e2e encryption il contatto ha la crittografia e2e @@ -8769,6 +9888,16 @@ Ripetere la richiesta di connessione? il contatto non ha la crittografia e2e No comment provided by engineer. + + contact not ready + contatto non pronto + No comment provided by engineer. + + + contact should accept… + il contatto dovrebbe accettare… + No comment provided by engineer. + creator creatore @@ -8797,7 +9926,8 @@ Ripetere la richiesta di connessione? default (%@) predefinito (%@) - pref value + delete after time +pref value default (no) @@ -8924,31 +10054,31 @@ Ripetere la richiesta di connessione? errore No comment provided by engineer. - - event happened - evento accaduto - No comment provided by engineer. - expired scaduto No comment provided by engineer. - - for better metadata privacy. - per una migliore privacy dei metadati. - No comment provided by engineer. - forwarded inoltrato No comment provided by engineer. + + group + gruppo + shown on group welcome message + group deleted gruppo eliminato No comment provided by engineer. + + group is deleted + il gruppo è eliminato + No comment provided by engineer. + group profile updated profilo del gruppo aggiornato @@ -9044,11 +10174,6 @@ Ripetere la richiesta di connessione? corsivo No comment provided by engineer. - - join as %@ - entra come %@ - No comment provided by engineer. - left è uscito/a @@ -9066,7 +10191,7 @@ Ripetere la richiesta di connessione? member %1$@ changed to %2$@ - membro %1$@ cambiato in %2$@ + il membro %1$@ è diventato %2$@ profile update event chat item @@ -9074,6 +10199,11 @@ Ripetere la richiesta di connessione? si è connesso/a rcv group event chat item + + member has old version + il membro ha una versione vecchia + No comment provided by engineer. + message messaggio @@ -9104,20 +10234,20 @@ Ripetere la richiesta di connessione? moderato da %@ marked deleted chat item preview text + + moderator + moderatore + member role + months mesi time unit - - mute - silenzia - No comment provided by engineer. - never mai - No comment provided by engineer. + delete after time new message @@ -9134,11 +10264,21 @@ Ripetere la richiesta di connessione? nessuna crittografia e2e No comment provided by engineer. + + no subscription + nessuna iscrizione + No comment provided by engineer. + no text nessun testo copied message info in history + + not synchronized + non sincronizzato + No comment provided by engineer. + observer osservatore @@ -9148,8 +10288,9 @@ Ripetere la richiesta di connessione? off off enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9191,6 +10332,21 @@ Ripetere la richiesta di connessione? 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. + + + pending review + in attesa di revisione + No comment provided by engineer. + quantum resistant e2e encryption crittografia e2e resistente alla quantistica @@ -9206,6 +10362,11 @@ Ripetere la richiesta di connessione? conferma ricevuta… No comment provided by engineer. + + rejected + rifiutato + No comment provided by engineer. + rejected call chiamata rifiutata @@ -9226,6 +10387,11 @@ Ripetere la richiesta di connessione? indirizzo di contatto rimosso profile update event chat item + + removed from group + rimosso dal gruppo + No comment provided by engineer. + removed profile picture immagine del profilo rimossa @@ -9236,10 +10402,41 @@ Ripetere la richiesta di connessione? ti ha rimosso/a rcv group event chat item + + request is sent + richiesta inviata + No comment provided by engineer. + + + request to join rejected + richiesta di entrare rifiutata + No comment provided by engineer. + + + requested connection + connessione richiesta + rcv group event chat item + + + requested connection from group %@ + connessione richiesta dal gruppo %@ + rcv direct event chat item + requested to connect + richiesto di connettersi chat list item title + + review + revisiona + No comment provided by engineer. + + + reviewed by admins + revisionato dagli amministratori + No comment provided by engineer. + saved salvato @@ -9275,11 +10472,6 @@ Ripetere la richiesta di connessione? codice di sicurezza modificato chat item text - - send direct message - invia messaggio diretto - No comment provided by engineer. - server queue info: %1$@ @@ -9339,11 +10531,6 @@ ultimo msg ricevuto: %2$@ stato sconosciuto No comment provided by engineer. - - unmute - riattiva notifiche - No comment provided by engineer. - unprotected non protetto @@ -9434,10 +10621,10 @@ ultimo msg ricevuto: %2$@ tu No comment provided by engineer. - - you are invited to group - sei stato/a invitato/a al gruppo - No comment provided by engineer. + + you accepted this member + hai accettato questo membro + snd group event chat item you are observer @@ -9508,7 +10695,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -9545,7 +10732,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -9567,7 +10754,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -9575,6 +10762,11 @@ ultimo msg ricevuto: %2$@ %d nuovi eventi notification body + + From %d chat(s) + Da %d chat + notification body + From: %@ Da: %@ @@ -9590,16 +10782,11 @@ ultimo msg ricevuto: %2$@ Nuovi messaggi notification - - New messages in %d chats - Nuovi messaggi in %d chat - notification body -
- +
@@ -9621,7 +10808,7 @@ ultimo msg ricevuto: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/it.xcloc/contents.json b/apps/ios/SimpleX Localizations/it.xcloc/contents.json index 13870ab8dd..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 72e68cff48..9a42ab3f7e 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (コピー可能) @@ -59,7 +47,7 @@ %@ (current) - %@ (現在) + %@ (現在) No comment provided by engineer. @@ -114,10 +102,12 @@ %@ server + %@ サーバー No comment provided by engineer. %@ servers + %@ サーバー No comment provided by engineer. @@ -132,6 +122,7 @@ %1$@, %2$@ + %1$@, %2$@ format for date separator in chat @@ -156,18 +147,22 @@ %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 @@ -177,6 +172,7 @@ %d messages not forwarded + %d 個のメッセージが未転送 alert title @@ -194,6 +190,11 @@ %d 秒 time interval + + %d seconds(s) + %d 秒 + delete after time + %d skipped message(s) %d 件のスキップされたメッセージ @@ -206,7 +207,6 @@ %lld - %lld No comment provided by engineer. @@ -264,11 +264,6 @@ %lldつの新しいインターフェース言語 No comment provided by engineer. - - %lld second(s) - %lld 秒 - No comment provided by engineer. - %lld seconds %lld 秒 @@ -319,11 +314,6 @@ %u 件のメッセージがスキップされました。 No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (新規) @@ -334,11 +324,6 @@ (このデバイス v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。 @@ -376,6 +361,7 @@ **Scan / Paste link**: to connect via a link you received. + **QRスキャン / リンクの貼り付け**: 受け取ったリンクで接続する。 No comment provided by engineer. @@ -403,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. @@ -427,6 +411,9 @@ - optionally notify deleted contacts. - profile names with spaces. - and more! + - 任意で削除された連絡先へ通知します。 +- プロフィール名に空白を含めることができます。 +- and more! No comment provided by engineer. @@ -438,11 +425,6 @@ - 編集履歴。 No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 秒 @@ -456,7 +438,8 @@ 1 day 1日 - time interval + delete after time +time interval 1 hour @@ -471,19 +454,28 @@ 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. @@ -501,11 +493,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> @@ -562,6 +549,7 @@ About operators + オペレーターについて No comment provided by engineer. @@ -572,11 +560,23 @@ Accept 承諾 accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + メンバーとして承認する + alert action + + + Accept as observer + オブザーバーとして承認する + alert action Accept conditions + 条件に同意する No comment provided by engineer. @@ -584,6 +584,11 @@ 接続要求を承認? No comment provided by engineer. + + Accept contact request + 連絡先リクエストを受け入れる + alert title + Accept contact request from %@? %@ からの連絡要求を受け入れますか? @@ -592,21 +597,32 @@ Accept incognito シークレットモードで承諾 - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + メンバーを承認する + alert title 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. @@ -620,6 +636,14 @@ Add friends No comment provided by engineer. + + Add list + No comment provided by engineer. + + + Add message + placeholder for sending contact request + Add profile プロフィールを追加 @@ -644,6 +668,10 @@ 別の端末に追加 No comment provided by engineer. + + Add to list + No comment provided by engineer. + Add welcome message ウェルカムメッセージを追加 @@ -655,10 +683,12 @@ Added media & file servers + 追加されたメディア & ファイルサーバー No comment provided by engineer. Added message servers + 追加されたメッセージサーバー No comment provided by engineer. @@ -707,6 +737,11 @@ Advanced settings + 詳細設定 + No comment provided by engineer. + + + All No comment provided by engineer. @@ -719,6 +754,10 @@ 全チャットとメッセージが削除されます(※元に戻せません※)! 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. 入力するとすべてのデータが消去されます。 @@ -752,8 +791,17 @@ 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. あなたの連絡先が繋がったまま継続します。 @@ -792,9 +840,13 @@ Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) - 送信相手も永久メッセージ削除を許可する時のみに許可する。 + 送信相手も永久メッセージ削除を許可する時のみに許可する。(24時間) No comment provided by engineer. @@ -819,6 +871,7 @@ Allow sharing + 共有を許可 No comment provided by engineer. @@ -826,6 +879,10 @@ 送信済みメッセージの永久削除を許可する。(24時間) No comment provided by engineer. + + Allow to report messsages to moderators. + No comment provided by engineer. + Allow to send SimpleX links. SimpleXリンクの送信を許可。 @@ -871,6 +928,10 @@ 送信相手が消えるメッセージを送るのを許可する。 No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. 送信相手からの音声メッセージを許可する。 @@ -884,15 +945,16 @@ Already connecting! 既に接続中です! - No comment provided by engineer. + new chat sheet title Already joining the group! すでにグループに参加しています! - No comment provided by engineer. + new chat sheet title Always use private routing. + プライベートルーティングを常に使用する。 No comment provided by engineer. @@ -905,6 +967,10 @@ 指定された名前の空のチャット プロファイルが作成され、アプリが通常どおり開きます。 No comment provided by engineer. + + Another reason + report reason + Answer call 通話に応答 @@ -930,6 +996,10 @@ アプリは新しいローカルファイル(ビデオを除く)を暗号化します。 No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon アプリのアイコン @@ -961,25 +1031,52 @@ 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. @@ -1048,10 +1145,6 @@ 画像を自動的に受信 No comment provided by engineer. - - Auto-accept settings - alert title - Back 戻る @@ -1083,6 +1176,10 @@ Better groups No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. No comment provided by engineer. @@ -1100,6 +1197,10 @@ Better notifications No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ No comment provided by engineer. @@ -1108,6 +1209,14 @@ Better user experience No comment provided by engineer. + + Bio + No comment provided by engineer. + + + Bio too large + alert title + Black No comment provided by engineer. @@ -1148,6 +1257,10 @@ Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. 自分も相手もメッセージへのリアクションを追加できます。 @@ -1168,6 +1281,10 @@ あなたと連絡相手が消えるメッセージを送信できます。 No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. あなたと連絡相手が音声メッセージを送信できます。 @@ -1186,11 +1303,25 @@ Business chats No comment provided by engineer. + + Business connection + 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! 通話は既に終了してます! @@ -1217,6 +1348,10 @@ Can't call member No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! 連絡先を招待できません! @@ -1235,7 +1370,8 @@ Cancel 中止 alert action - alert button +alert button +new chat action Cancel migration @@ -1268,6 +1404,10 @@ 変更 No comment provided by engineer. + + Change automatic message deletion? + alert title + Change chat profiles authentication reason @@ -1316,7 +1456,7 @@ Change self-destruct passcode 自己破壊パスコードを変更する authentication reason - set passcode view +set passcode view Chat @@ -1328,7 +1468,7 @@ Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1407,11 +1547,27 @@ Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + + + Chat with members before they join. + No comment provided by engineer. + Chats チャット No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1473,6 +1629,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? プライベートノートを消しますか? @@ -1492,6 +1656,10 @@ 色設定 No comment provided by engineer. + + Community guidelines violation + report reason + Compare file ファイルを比較 @@ -1521,15 +1689,7 @@ Conditions of use - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1548,6 +1708,10 @@ ICEサーバを設定 No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm 確認 @@ -1593,6 +1757,10 @@ Confirm upload No comment provided by engineer. + + Confirmed + token status text + Connect 接続 @@ -1602,9 +1770,8 @@ Connect automatically No comment provided by engineer. - - Connect incognito - シークレットモードで接続 + + Connect faster! 🚀 No comment provided by engineer. @@ -1617,37 +1784,33 @@ 友達ともっと速くつながりましょう。 No comment provided by engineer. - - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link リンク経由で接続 - No comment provided by engineer. + new chat sheet title Connect via one-time link ワンタイムリンクで接続 - No comment provided by engineer. + new chat sheet title Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1701,19 +1864,32 @@ This is your own one-time link! Connection and servers status. - 接続とサーバーのステータス + 接続とサーバーのステータス。 + No comment provided by engineer. + + + Connection blocked No comment provided by engineer. Connection error 接続エラー - No comment provided by engineer. + alert title Connection error (AUTH) 接続エラー (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. @@ -1723,6 +1899,10 @@ This is your own one-time link! 接続リクエストを送信しました! No comment provided by engineer. + + Connection requires encryption renegotiation. + No comment provided by engineer. + Connection security No comment provided by engineer. @@ -1735,7 +1915,7 @@ This is your own one-time link! Connection timeout 接続タイムアウト - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1783,6 +1963,10 @@ This is your own one-time link! 連絡先の設定 No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -1797,6 +1981,10 @@ This is your own one-time link! 連絡先はメッセージを削除対象とすることができます。あなたには閲覧可能です。 No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue 続ける @@ -1865,6 +2053,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/)で新しいプロファイルを作成します。 💻 @@ -1872,6 +2064,7 @@ This is your own one-time link! Create profile + プロフィールを作成する No comment provided by engineer. @@ -1879,9 +2072,8 @@ This is your own one-time link! キューの作成 server test step - - Create secret group - シークレットグループを作成する + + Create your address No comment provided by engineer. @@ -2073,8 +2265,7 @@ This is your own one-time link! Delete 削除 alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2112,6 +2303,10 @@ This is your own one-time link! Delete chat No comment provided by engineer. + + Delete chat messages from your device. + No comment provided by engineer. + Delete chat profile チャットのプロフィールを削除する @@ -2122,6 +2317,10 @@ This is your own one-time link! チャットのプロフィールを削除しますか? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2199,6 +2398,10 @@ This is your own one-time link! リンクを削除しますか? No comment provided by engineer. + + Delete list? + alert title + Delete member message? メンバーのメッセージを削除しますか? @@ -2212,7 +2415,7 @@ This is your own one-time link! Delete messages メッセージを削除 - No comment provided by engineer. + alert button Delete messages after @@ -2248,6 +2451,10 @@ This is your own one-time link! 待ち行列を削除 server test step + + Delete report + No comment provided by engineer. + Delete up to 20 messages at once. No comment provided by engineer. @@ -2298,11 +2505,19 @@ This is your own one-time link! 配信通知! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description 説明 No comment provided by engineer. + + Description too large + alert title + Desktop address No comment provided by engineer. @@ -2395,6 +2610,14 @@ This is your own one-time link! SimpleXロックを無効にする authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all すべて無効 @@ -2478,9 +2701,13 @@ This is your own one-time link! 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. @@ -2488,9 +2715,17 @@ This is your own one-time link! 有効にしない No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again 次から表示しない + alert action + + + Done No comment provided by engineer. @@ -2501,7 +2736,7 @@ This is your own one-time link! Download alert button - chat item action +chat item action Download errors @@ -2560,6 +2795,10 @@ This is your own one-time link! グループのプロフィールを編集 No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable 有効 @@ -2570,8 +2809,8 @@ This is your own one-time link! 有効にする(設定の優先を維持) No comment provided by engineer. - - Enable Flux + + Enable Flux in Network & servers settings for better metadata privacy. No comment provided by engineer. @@ -2587,12 +2826,16 @@ This is your own one-time link! Enable automatic message deletion? 自動メッセージ削除を有効にしますか? - No comment provided by engineer. + alert title Enable camera access No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all すべて有効 @@ -2707,6 +2950,10 @@ This is your own one-time link! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode パスコードを入力 @@ -2777,6 +3024,10 @@ This is your own one-time link! 連絡先リクエストの承諾にエラー発生 No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) メンバー追加にエラー発生 @@ -2786,11 +3037,19 @@ This is your own one-time link! Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address アドレス変更にエラー発生 No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile No comment provided by engineer. @@ -2803,15 +3062,23 @@ This is your own one-time link! Error changing setting 設定変更にエラー発生 - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2828,6 +3095,10 @@ This is your own one-time link! グループリンク生成にエラー発生 No comment provided by engineer. + + Error creating list + alert title + Error creating member contact メンバー連絡先の作成中にエラーが発生 @@ -2842,20 +3113,28 @@ This is your own one-time link! プロフィール作成にエラー発生! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file ファイルの復号エラー No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database チャットデータベース削除にエラー発生 - No comment provided by engineer. + alert title Error deleting chat! チャット削除にエラー発生! - No comment provided by engineer. + alert title Error deleting connection @@ -2865,12 +3144,12 @@ This is your own one-time link! Error deleting database データベースの削除にエラー発生 - No comment provided by engineer. + alert title Error deleting old database 古いデータベースを削除にエラー発生 - No comment provided by engineer. + alert title Error deleting token @@ -2903,7 +3182,7 @@ This is your own one-time link! Error exporting chat database チャットデータベースのエキスポートにエラー発生 - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -2912,7 +3191,7 @@ This is your own one-time link! Error importing chat database チャットデータベースのインポートにエラー発生 - No comment provided by engineer. + alert title Error joining group @@ -2931,6 +3210,10 @@ This is your own one-time link! Error opening chat No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file ファイル受信にエラー発生 @@ -2944,10 +3227,22 @@ This is your own one-time link! Error reconnecting servers No comment provided by engineer. + + Error registering for notifications + alert title + + + Error rejecting contact request + alert title + Error removing member メンバー除名にエラー発生 - No comment provided by engineer. + alert title + + + Error reordering lists + alert title Error resetting statistics @@ -2958,6 +3253,10 @@ This is your own one-time link! ICEサーバ保存にエラー発生 No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile グループのプロフィール保存にエラー発生 @@ -3005,6 +3304,10 @@ This is your own one-time link! メッセージ送信にエラー発生 No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! No comment provided by engineer. @@ -3021,7 +3324,7 @@ This is your own one-time link! Error switching profile - No comment provided by engineer. + alert title Error switching profile! @@ -3033,6 +3336,10 @@ This is your own one-time link! 接続の同期エラー No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link グループのリンクのアップデートにエラー発生 @@ -3073,7 +3380,13 @@ This is your own one-time link! Error: %@ エラー : %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3107,6 +3420,10 @@ This is your own one-time link! Expand chat item action + + Expired + token status text + Export database データベースをエキスポート @@ -3145,24 +3462,41 @@ This is your own one-time link! 送信者がオンラインになるまでの待ち時間がなく、速い! 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 - No comment provided by engineer. + 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 @@ -3213,6 +3547,10 @@ This is your own one-time link! ファイルとメディア chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. このグループでは、ファイルとメディアは禁止されています。 @@ -3250,6 +3588,23 @@ This is your own one-time link! チャットを素早く検索 No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + サーバアドレスの証明証IDが正しくないかもしれません + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix 修正 @@ -3280,6 +3635,10 @@ This is your own one-time link! グループメンバーによる修正はサポートされていません No comment provided by engineer. + + For all moderators + No comment provided by engineer. + For chat profile %@: servers error @@ -3293,6 +3652,10 @@ This is your own one-time link! 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. @@ -3338,8 +3701,8 @@ This is your own one-time link! No comment provided by engineer. - Forwarding server %@ failed to connect to destination server %@. Please try later. - No comment provided by engineer. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3397,6 +3760,10 @@ Error: %2$@ GIFとステッカー No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3416,7 +3783,7 @@ Error: %2$@ Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3483,6 +3850,10 @@ Error: %2$@ グループのプロフィールはサーバではなく、メンバーの端末に保存されます。 No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message グループのウェルカムメッセージ @@ -3498,11 +3869,19 @@ Error: %2$@ あなたにとってグループが削除されます (※元に戻せません※)! 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 プライベート @@ -3560,6 +3939,10 @@ Error: %2$@ How it helps privacy No comment provided by engineer. + + How it works + alert button + How to 使い方 @@ -3692,6 +4075,14 @@ More improvements are coming soon! In-call sounds No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito シークレットモード @@ -3762,7 +4153,7 @@ More improvements are coming soon! Instant - すぐに + 即時 No comment provided by engineer. @@ -3781,6 +4172,26 @@ More improvements are coming soon! 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. @@ -3796,7 +4207,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -3904,32 +4315,29 @@ More improvements are coming soon! 参加 swipe action + + Join as %@ + %@ として参加 + No comment provided by engineer. + Join group グループに参加 - No comment provided by engineer. + new chat sheet title Join group conversations No comment provided by engineer. - - Join group? - No comment provided by engineer. - Join incognito シークレットモードで参加 No comment provided by engineer. - - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -3952,6 +4360,10 @@ This is your link for group %@! Keep unused invitation? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections 接続を維持 @@ -4005,6 +4417,10 @@ This is your link for group %@! グループを脱退しますか? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat SimpleXチャットで会話しよう @@ -4032,6 +4448,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! ライブメッセージ! @@ -4042,6 +4470,10 @@ This is your link for group %@! ライブメッセージ No comment provided by engineer. + + Loading profile… + in progress text + Local name ローカルネーム @@ -4115,10 +4547,26 @@ This is your link for group %@! メンバー No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + No comment provided by engineer. + Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + Member reports + chat feature + Member role will be changed to "%@". All chat members will be notified. No comment provided by engineer. @@ -4142,6 +4590,10 @@ This is your link for group %@! メンバーをグループから除名する (※元に戻せません※)! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. グループメンバーはメッセージへのリアクションを追加できます。 @@ -4152,6 +4604,10 @@ This is your link for group %@! グループのメンバーがメッセージを完全削除することができます。(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. @@ -4176,6 +4632,10 @@ This is your link for group %@! グループのメンバーが音声メッセージを送信できます。 No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4202,6 +4662,10 @@ This is your link for group %@! Message forwarded item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. item status description @@ -4268,10 +4732,18 @@ This is your link for group %@! メッセージ & ファイル No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + Messages received No comment provided by engineer. @@ -4300,6 +4772,7 @@ This is your link for group %@! Migrate from another device + 別の端末から移行 No comment provided by engineer. @@ -4344,7 +4817,7 @@ This is your link for group %@! Migrations: - 移行 + 移行: No comment provided by engineer. @@ -4362,6 +4835,10 @@ This is your link for group %@! モデレーターによって介入済み: %@ copied message info + + More + swipe action + More improvements are coming soon! まだまだ改善してまいります! @@ -4388,7 +4865,11 @@ This is your link for group %@! Mute ミュート - swipe action + notification label action + + + Mute all + notification label action Muted when inactive! @@ -4433,7 +4914,11 @@ This is your link for group %@! Network status ネットワーク状況 - No comment provided by engineer. + alert title + + + New + token status text New Passcode @@ -4480,6 +4965,10 @@ This is your link for group %@! New events notification + + New group role: Moderator + No comment provided by engineer. + New in %@ %@ の新機能 @@ -4494,6 +4983,10 @@ This is your link for group %@! 新しいメンバーの役割 No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message 新しいメッセージ @@ -4518,6 +5011,22 @@ 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 chats with members + No comment provided by engineer. + No contacts selected 連絡先が選択されてません @@ -4565,6 +5074,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -4586,6 +5099,10 @@ This is your link for group %@! 音声メッセージを録音する権限がありません No comment provided by engineer. + + No private routing session + alert title + No push server 自分のみ @@ -4612,6 +5129,14 @@ This is your link for group %@! No servers to send files. servers error + + No token! + alert title + + + No unread chats + No comment provided by engineer. + No user identifiers. 世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。 @@ -4621,6 +5146,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Notes + No comment provided by engineer. + Nothing selected No comment provided by engineer. @@ -4639,10 +5168,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. @@ -4664,7 +5201,9 @@ This is your link for group %@! Ok OK - alert button + alert action +alert button +new chat action Old database @@ -4723,6 +5262,14 @@ VPN を有効にする必要があります。 音声メッセージを利用可能に設定できるのはグループのオーナーだけです。 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. メッセージへのリアクションを追加できるのは、あなただけです。 @@ -4743,6 +5290,10 @@ VPN を有効にする必要があります。 消えるメッセージを送れるのはあなただけです。 No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. 音声メッセージを送れるのはあなただけです。 @@ -4768,6 +5319,10 @@ VPN を有効にする必要があります。 消えるメッセージを送れるのはあなたの連絡相手だけです。 No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. 音声メッセージを送れるのはあなたの連絡相手だけです。 @@ -4776,7 +5331,7 @@ VPN を有効にする必要があります。 Open 開く - No comment provided by engineer. + alert action Open Settings @@ -4790,25 +5345,61 @@ VPN を有効にする必要があります。 Open chat チャットを開く - No comment provided by engineer. + new chat action Open chat console チャットのコンソールを開く authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group - No comment provided by engineer. + new chat action + + + Open link? + alert title Open migration to another device authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -4845,6 +5436,10 @@ VPN を有効にする必要があります。 Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other No comment provided by engineer. @@ -4898,10 +5493,6 @@ VPN を有効にする必要があります。 パスワードを表示する No comment provided by engineer. - - Past member %@ - past/unknown group member - Paste desktop address No comment provided by engineer. @@ -4963,7 +5554,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. %@ を使用してネットワーク接続を確認し、再試行してください。 - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5019,6 +5610,22 @@ Error: %@ パスフレーズを失くさないように保管してください。失くすと変更できなくなります。 No comment provided by engineer. + + Please try to disable and re-enable notfications. + token info + + + Please wait for group moderators to review your request to join the group. + snd group event chat item + + + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + token info + Polish interface ポーランド語UI @@ -5028,11 +5635,6 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - サーバアドレスの証明証IDが正しくないかもしれません - server test error - Preserve the last message draft, with attachments. 添付を含めて、下書きを保存する。 @@ -5065,16 +5667,28 @@ Error: %@ 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. @@ -5085,6 +5699,7 @@ Error: %@ Private notes + プライベートノート name of notes to self @@ -5093,7 +5708,11 @@ Error: %@ Private routing error - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5143,6 +5762,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. @@ -5186,6 +5809,10 @@ Enable in *Network & servers* settings. チャットのプロフィールをパスワードで保護します! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout プロトコル・タイムアウト @@ -5282,11 +5909,6 @@ Enable in *Network & servers* settings. 受信: %@ copied message info - - Received file event - ファイル受信イベント - notification - Received message 受信したメッセージ @@ -5377,11 +5999,24 @@ Enable in *Network & servers* settings. 電池使用量低減 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5391,7 +6026,11 @@ Enable in *Network & servers* settings. Reject contact request 連絡要求を拒否する - No comment provided by engineer. + alert title + + + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5416,6 +6055,10 @@ Enable in *Network & servers* settings. Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member メンバーを除名する @@ -5431,6 +6074,10 @@ Enable in *Network & servers* settings. キーチェーンからパスフレーズを削除しますか? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate 再ネゴシエート @@ -5446,10 +6093,6 @@ Enable in *Network & servers* settings. 暗号化を再ネゴシエートしますか? No comment provided by engineer. - - Repeat connection request? - No comment provided by engineer. - Repeat download No comment provided by engineer. @@ -5458,10 +6101,6 @@ Enable in *Network & servers* settings. Repeat import No comment provided by engineer. - - Repeat join request? - No comment provided by engineer. - Repeat upload No comment provided by engineer. @@ -5471,6 +6110,50 @@ Enable in *Network & servers* settings. 返信 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 sent to moderators + alert title + + + 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 必須 @@ -5543,7 +6226,7 @@ Enable in *Network & servers* settings. Retry - No comment provided by engineer. + alert action Reveal @@ -5554,10 +6237,18 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later + + Review group members No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke 取り消す @@ -5603,13 +6294,21 @@ Enable in *Network & servers* settings. Save 保存 alert button - chat item action +chat item action Save (and notify contacts) 保存(連絡先に通知) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact 保存して、連絡先にに知らせる @@ -5634,6 +6333,14 @@ Enable in *Network & servers* settings. グループプロフィールの保存 No comment provided by engineer. + + Save group profile? + alert title + + + Save list + No comment provided by engineer. + Save passphrase and open chat パスフレーズをを保存して、チャットを開始 @@ -5809,6 +6516,10 @@ Enable in *Network & servers* settings. ライブメッセージを送信 (入力しながら宛先の画面で更新される) No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to No comment provided by engineer. @@ -5854,6 +6565,10 @@ Enable in *Network & servers* settings. 通知を送信する No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas 質問やアイデアを送る @@ -5863,6 +6578,14 @@ Enable in *Network & servers* settings. Send receipts No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. ギャラリーまたはカスタム キーボードから送信します。 @@ -5872,6 +6595,10 @@ Enable in *Network & servers* settings. Send up to 100 last messages to new members. No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. 送信者がファイル転送をキャンセルしました。 @@ -5930,11 +6657,6 @@ Enable in *Network & servers* settings. Sent directly No comment provided by engineer. - - Sent file event - 送信済みファイルイベント - notification - Sent message 送信 @@ -5993,13 +6715,13 @@ Enable in *Network & servers* settings. Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. キューを作成するにはサーバーの認証が必要です。パスワードを確認してください server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. アップロードにはサーバーの認証が必要です。パスワードを確認してください server test error @@ -6042,6 +6764,10 @@ Enable in *Network & servers* settings. 1日に設定 No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… 連絡先の名前を設定… @@ -6061,6 +6787,14 @@ Enable in *Network & servers* settings. システム認証の代わりに設定します。 No comment provided by engineer. + + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + No comment provided by engineer. + Set passcode パスコードを設定する @@ -6075,6 +6809,10 @@ Enable in *Network & servers* settings. 暗証フレーズを設定してからエクスポート No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! 新しいメンバーに表示されるメッセージを設定してください! @@ -6102,7 +6840,7 @@ Enable in *Network & servers* settings. Share 共有する alert action - chat item action +chat item action Share 1-time link @@ -6140,6 +6878,14 @@ Enable in *Network & servers* settings. リンクを送る No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile No comment provided by engineer. @@ -6157,6 +6903,22 @@ Enable in *Network & servers* settings. 連絡先と共有する No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -6249,6 +7011,14 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX address settings + alert title + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX連絡先アドレス @@ -6286,6 +7056,10 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode シークレットモードの簡素化 @@ -6341,6 +7115,11 @@ Enable in *Network & servers* settings. 誰か notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. No comment provided by engineer. @@ -6420,6 +7199,10 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Storage + No comment provided by engineer. + Strong blur media @@ -6468,11 +7251,19 @@ Enable in *Network & servers* settings. TCP connection No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -6497,10 +7288,26 @@ Enable in *Network & servers* settings. 写真を撮影 No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join group + No comment provided by engineer. + Tap button ボタンをタップ @@ -6535,13 +7342,17 @@ Enable in *Network & servers* settings. Temporary file error - No comment provided by engineer. + file error alert title Test failed at step %@. テストはステップ %@ で失敗しました。 server test failure + + Test notifications + No comment provided by engineer. + Test server テストサーバ @@ -6579,6 +7390,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + The app can notify you when you receive messages or contact requests - please open settings to enable. アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。 @@ -6635,6 +7450,10 @@ It can happen because of some bug or when the connection is compromised.以前のメッセージとハッシュ値が異なります。 No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. メッセージはすべてのメンバーに対して削除されます。 @@ -6658,19 +7477,10 @@ 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. - プロフィールは連絡先にしか共有されません。 - No comment provided by engineer. - The same conditions will apply to operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! No comment provided by engineer. @@ -6683,7 +7493,7 @@ It can happen because of some bug or when the connection is compromised. The sender will NOT be notified 送信者には通知されません - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -6730,6 +7540,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. あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。 @@ -6760,23 +7574,31 @@ It can happen because of some bug or when the connection is compromised.このグループはもう存在しません。 No comment provided by engineer. - - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time 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. 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title No comment provided by engineer. @@ -6851,11 +7673,19 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. インスタント プッシュ通知をサポートするには、チャット データベースを移行する必要があります。 No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. @@ -6873,6 +7703,10 @@ You will be prompted to complete authentication before this feature is enabled.< Toggle incognito when connecting. No comment provided by engineer. + + Token status: %@. + token status + Toolbar opacity No comment provided by engineer. @@ -6890,15 +7724,9 @@ You will be prompted to complete authentication before this feature is enabled.< 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. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7025,13 +7853,17 @@ To connect, please ask your contact to create another connection link and check Unmute ミュート解除 - swipe action + notification label action Unread 未読 swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. No comment provided by engineer. @@ -7055,16 +7887,44 @@ To connect, please ask your contact to create another connection link and check Update settings? No comment provided by engineer. + + Updated conditions + No comment provided by engineer. + Updating settings will re-connect the client to all servers. 設定を更新すると、全サーバにクライントの再接続が行われます。 No comment provided by engineer. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat アップグレードしてチャットを開く No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors No comment provided by engineer. @@ -7108,6 +7968,14 @@ To connect, please ask your contact to create another connection link and check 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 チャット @@ -7116,7 +7984,7 @@ To connect, please ask your contact to create another connection link and check Use current profile 現在のプロファイルを使用する - No comment provided by engineer. + new chat action Use for files @@ -7140,10 +8008,14 @@ To connect, please ask your contact to create another connection link and check iOS通話インターフェースを使用する No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile 新しいシークレットプロファイルを使用する - No comment provided by engineer. + new chat action Use only local notifications? @@ -7174,6 +8046,10 @@ To connect, please ask your contact to create another connection link and check Use the app with one hand. No comment provided by engineer. + + Use web port + No comment provided by engineer. + User selection No comment provided by engineer. @@ -7347,6 +8223,10 @@ To connect, please ask your contact to create another connection link and check Welcome message is too long No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new 新着情報 @@ -7455,11 +8335,11 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7467,31 +8347,30 @@ To connect, please ask your contact to create another connection link and check 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. + new chat sheet message You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - この連絡先から受信するメッセージのサーバに既に接続してます。 - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group グループ招待が届きました No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. No comment provided by engineer. @@ -7505,10 +8384,6 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. - - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. No comment provided by engineer. @@ -7539,6 +8414,7 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. + 設定でSimpleXの連絡先に表示させることができます。 No comment provided by engineer. @@ -7592,10 +8468,14 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! メッセージを送信できませんでした! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -7607,14 +8487,10 @@ Repeat join request? あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 No comment provided by engineer. - - You have already requested connection via this address! - No comment provided by engineer. - You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -7668,6 +8544,14 @@ Repeat connection request? グループの招待を送りました No comment provided by engineer. + + You should receive notifications. + token info + + + You will be able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! グループのホスト端末がオンラインになったら、接続されます。後でチェックするか、しばらくお待ちください! @@ -7692,10 +8576,6 @@ Repeat connection request? 起動時、または非アクティブ状態で30秒が経った後に戻ると、認証する必要となります。 No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. ミュートされたプロフィールがアクティブな場合でも、そのプロフィールからの通話や通知は引き続き受信します。 @@ -7730,16 +8610,15 @@ Repeat connection request? あなたの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 business contact + No comment provided by engineer. + Your calls あなたの通話 @@ -7764,8 +8643,16 @@ Repeat connection request? あなたのチャットプロフィール No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. + No comment provided by engineer. + + + Your contact No comment provided by engineer. @@ -7797,6 +8684,10 @@ Repeat connection request? 現在のプロフィール No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences あなたの設定 @@ -7816,6 +8707,11 @@ Repeat connection request? あなたのプロファイル **%@** が共有されます。 No comment provided by engineer. + + Your profile is stored on your device and only shared with your contacts. + プロフィールは連絡先にしか共有されません。 + No comment provided by engineer. + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。 @@ -7825,11 +8721,6 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message - - Your profile, contacts and delivered messages are stored on your device. - あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。 - No comment provided by engineer. - Your random profile あなたのランダム・プロフィール @@ -7879,6 +8770,10 @@ Repeat connection request? 上で選んでください: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call 受けた通話 @@ -7888,6 +8783,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin 管理者 @@ -7907,6 +8806,10 @@ Repeat connection request? 暗号化に同意しています… chat item text + + all + member criteria value + all members feature role @@ -7920,6 +8823,10 @@ Repeat connection request? and %lld other events No comment provided by engineer. + + archived report + No comment provided by engineer. + attempts No comment provided by engineer. @@ -7953,7 +8860,8 @@ Repeat connection request? blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -7979,6 +8887,10 @@ Repeat connection request? 発信中… call status + + can't send messages + No comment provided by engineer. + cancelled %@ キャンセルされました %@ @@ -8029,10 +8941,6 @@ Repeat connection request? 接続中 No comment provided by engineer. - - connected directly - rcv group event chat item - connecting 接続待ち @@ -8082,6 +8990,14 @@ Repeat connection request? contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption 連絡先はエンドツーエンド暗号化があります @@ -8092,6 +9008,14 @@ Repeat connection request? 連絡先はエンドツーエンド暗号化がありません No comment provided by engineer. + + contact not ready + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator 作成者 @@ -8119,7 +9043,8 @@ Repeat connection request? default (%@) デフォルト (%@) - pref value + delete after time +pref value default (no) @@ -8244,28 +9169,27 @@ Repeat connection request? エラー No comment provided by engineer. - - event happened - イベント発生 - No comment provided by engineer. - expired No comment provided by engineer. - - for better metadata privacy. - No comment provided by engineer. - forwarded No comment provided by engineer. + + group + shown on group welcome message + group deleted グループ削除済み No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated グループのプロフィールが更新されました @@ -8359,11 +9283,6 @@ Repeat connection request? 斜体 No comment provided by engineer. - - join as %@ - %@ として参加 - No comment provided by engineer. - left 脱退 @@ -8388,6 +9307,10 @@ Repeat connection request? 接続中 rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8417,19 +9340,19 @@ Repeat connection request? %@ によってモデレートされた marked deleted chat item preview text + + moderator + member role + months time unit - - mute - No comment provided by engineer. - never 一度も - No comment provided by engineer. + delete after time new message @@ -8446,11 +9369,19 @@ Repeat connection request? エンドツーエンド暗号化がありません No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text テキストなし copied message info in history + + not synchronized + No comment provided by engineer. + observer オブザーバー @@ -8460,8 +9391,9 @@ Repeat connection request? off オフ enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -8500,6 +9432,18 @@ Repeat connection request? P2P No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -8514,6 +9458,10 @@ Repeat connection request? 確認を受け取りました… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call 拒否した通話 @@ -8533,6 +9481,10 @@ Repeat connection request? removed contact address profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profile update event chat item @@ -8542,10 +9494,34 @@ Repeat connection request? あなたを除名しました rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved No comment provided by engineer. @@ -8578,10 +9554,6 @@ Repeat connection request? セキュリティコードが変更されました chat item text - - send direct message - No comment provided by engineer. - server queue info: %1$@ @@ -8632,10 +9604,6 @@ last received msg: %2$@ unknown status No comment provided by engineer. - - unmute - No comment provided by engineer. - unprotected No comment provided by engineer. @@ -8720,10 +9688,9 @@ last received msg: %2$@ you No comment provided by engineer. - - you are invited to group - グループ招待が届きました - No comment provided by engineer. + + you accepted this member + snd group event chat item you are observer @@ -8792,7 +9759,7 @@ last received msg: %2$@
- +
@@ -8828,7 +9795,7 @@ last received msg: %2$@
- +
@@ -8850,13 +9817,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -8869,15 +9840,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -8896,7 +9863,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json index 604a21be97..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 9aaa83afc3..ca51a875c7 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -24,216 +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. **Create link / QR code** for your contact to use. No comment provided by engineer. - + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. + **비공개**: 20분마다 새로운 메시지를 확인합니다. 푸시 서버에는 장치 토큰만 공유됩니다. 연락처 수나 메세지 메타데이터가 표시되지 않습니다. No comment provided by engineer. - + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. + **비공개**: 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 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 @@ -244,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 @@ -274,12 +327,14 @@ Accept contact No comment provided by engineer. - + Accept contact request from %@? + %@의 연락 요청을 수락하시겠습니까? notification body - + Accept incognito + 인정하지 않음 No comment provided by engineer. @@ -290,192 +345,233 @@ 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 + 서버 추가하기 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. @@ -487,8 +583,9 @@ 주소를 초대할 수 없습니다. No comment provided by engineer. - + Can't invite contacts! + 연락처를 초대할 수 없습니다! No comment provided by engineer. @@ -511,8 +608,9 @@ 변경 No comment provided by engineer. - + Change database passphrase? + 데이터베이스 암호 변경? No comment provided by engineer. @@ -540,16 +638,19 @@ 채팅 기록 보관함 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. @@ -557,80 +658,98 @@ 채팅 데이터베이스를 가져옴 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 @@ -641,8 +760,9 @@ Connect via group link? No comment provided by engineer. - + Connect via link + 링크를 통해 연결 No comment provided by engineer. @@ -653,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. @@ -2009,8 +2165,9 @@ We will be adding server redundancy to prevent lost messages. Open user profiles authentication reason - + Anybody can host servers. + 누구나 서버를 호스팅할 수 있습니다. No comment provided by engineer. @@ -2093,8 +2250,8 @@ We will be adding server redundancy to prevent lost messages. 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 + + Fingerprint in server address does not match certificate. server test error @@ -2446,8 +2603,8 @@ 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 requires authorization to create queues, check password. server test error @@ -2710,8 +2867,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. No comment provided by engineer. @@ -3539,7 +3696,7 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -3716,8 +3873,8 @@ SimpleX servers cannot see your profile. yes pref value - - you are invited to group + + You are invited to group No comment provided by engineer. @@ -3784,6 +3941,1186 @@ SimpleX servers cannot see your profile. 새로운 멤버에게 최대 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! + %@로부터의 모든 새 메세지가 숨겨집니다! + + + SimpleX address 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 + 다크 모드 색상들 +
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 54a713478f..4b51d66a34 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -2047,8 +2047,8 @@ We will be adding server redundancy to prevent lost messages. 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 + + Fingerprint in server address does not match certificate. server test error @@ -2375,8 +2375,8 @@ 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 requires authorization to create queues, check password. server test error @@ -2631,8 +2631,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. No comment provided by engineer. @@ -3439,7 +3439,7 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -3616,8 +3616,8 @@ SimpleX servers cannot see your profile. yes pref value - - you are invited to group + + You are invited to group No comment provided by engineer. 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 ab3499a4dc..8e0cdee3ca 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (kan gekopieerd worden) @@ -202,6 +190,11 @@ %d sec time interval + + %d seconds(s) + %d seconden + delete after time + %d skipped message(s) %d overgeslagen bericht(en) @@ -272,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 @@ -327,11 +315,6 @@ %u berichten zijn overgeslagen. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nieuw) @@ -342,11 +325,6 @@ (dit apparaat v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Contact toevoegen**: om een nieuwe uitnodigingslink aan te maken, of verbinding te maken via een link die u heeft ontvangen. @@ -412,17 +390,12 @@ \*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). - faster and more stable. - - verbinding maken met [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! -- ontvangst bevestiging(tot 20 leden). + - verbinding maken met [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- ontvangst bevestiging(tot 20 leden). - sneller en stabieler. No comment provided by engineer. @@ -453,11 +426,6 @@ - bewerkingsgeschiedenis. No comment provided by engineer.
- - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -471,7 +439,8 @@ 1 day 1 dag - time interval + delete after time +time interval 1 hour @@ -486,12 +455,19 @@ 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 @@ -518,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> @@ -591,8 +562,19 @@ Accept Accepteer accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Accepteren als lid + alert action + + + Accept as observer + Accepteren als waarnemer + alert action Accept conditions @@ -604,6 +586,10 @@ Accepteer contact No comment provided by engineer. + + Accept contact request + alert title + Accept contact request from %@? Accepteer contactverzoek van %@? @@ -612,8 +598,13 @@ Accept incognito Accepteer incognito - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Lid accepteren + alert title Accepted conditions @@ -630,6 +621,11 @@ Bevestigingsfouten No comment provided by engineer. + + Active + actief + token status text + Active connections Actieve verbindingen @@ -645,6 +641,15 @@ Vrienden toevoegen No comment provided by engineer. + + Add list + Lijst toevoegen + No comment provided by engineer. + + + Add message + placeholder for sending contact request + Add profile Profiel toevoegen @@ -670,6 +675,11 @@ Toevoegen aan een ander apparaat No comment provided by engineer. + + Add to list + Toevoegen aan lijst + No comment provided by engineer. + Add welcome message Welkom bericht toevoegen @@ -745,6 +755,11 @@ Geavanceerde instellingen No comment provided by engineer. + + All + alle + No comment provided by engineer. + All app data is deleted. Alle app-gegevens worden verwijderd. @@ -755,6 +770,11 @@ 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. @@ -795,6 +815,16 @@ Alle profielen profile dropdown + + All reports will be archived for you. + Alle rapporten worden voor u gearchiveerd. + No comment provided by engineer. + + + All servers + Alle servers + No comment provided by engineer. + All your contacts will remain connected. Al uw contacten blijven verbonden. @@ -835,6 +865,10 @@ Downgraden toestaan No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) @@ -870,6 +904,11 @@ 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 SimpleX links. Sta toe dat SimpleX-links worden verzonden. @@ -915,6 +954,10 @@ Sta toe dat uw contacten verdwijnende berichten verzenden. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Sta toe dat uw contacten spraak berichten verzenden. @@ -928,12 +971,12 @@ Already connecting! Al bezig met verbinden! - No comment provided by engineer. + new chat sheet title Already joining the group! Al lid van de groep! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -950,6 +993,11 @@ 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 @@ -975,6 +1023,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 @@ -1020,6 +1073,21 @@ 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 @@ -1030,6 +1098,21 @@ 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 @@ -1100,11 +1183,6 @@ Afbeeldingen automatisch accepteren No comment provided by engineer. - - Auto-accept settings - Instellingen automatisch accepteren - alert title - Back Terug @@ -1140,6 +1218,11 @@ 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. @@ -1160,6 +1243,11 @@ Betere meldingen No comment provided by engineer. + + Better privacy and security + Betere privacy en veiligheid + No comment provided by engineer. + Better security ✅ Betere beveiliging ✅ @@ -1170,6 +1258,14 @@ Betere gebruikerservaring No comment provided by engineer. + + Bio + No comment provided by engineer. + + + Bio too large + alert title + Black Zwart @@ -1220,6 +1316,10 @@ Vervaag media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Zowel u als uw contact kunnen bericht reacties toevoegen. @@ -1240,6 +1340,10 @@ Zowel jij als je contact kunnen verdwijnende berichten sturen. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Zowel jij als je contact kunnen spraak berichten verzenden. @@ -1260,11 +1364,29 @@ Zakelijke chats No comment provided by engineer. + + Business connection + 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: +- send only legal content in public groups. +- respect other users – no spam. + Door SimpleX Chat te gebruiken, gaat u ermee akkoord: +- alleen legale content te versturen in openbare groepen. +- andere gebruikers te respecteren – geen spam. + No comment provided by engineer. + Call already ended! Oproep al beëindigd! @@ -1295,6 +1417,10 @@ Kan lid niet bellen No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Kan contact niet uitnodigen! @@ -1314,7 +1440,8 @@ Cancel Annuleren alert action - alert button +alert button +new chat action Cancel migration @@ -1351,6 +1478,11 @@ Veranderen No comment provided by engineer. + + Change automatic message deletion? + Automatisch verwijderen van berichten wijzigen? + alert title + Change chat profiles Gebruikersprofielen wijzigen @@ -1400,7 +1532,7 @@ Change self-destruct passcode Zelfvernietigings code wijzigen authentication reason - set passcode view +set passcode view Chat @@ -1415,7 +1547,7 @@ Chat already exists! Chat bestaat al! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1502,11 +1634,30 @@ De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt! No comment provided by engineer. + + Chat with admins + Chat met beheerders + chat toolbar + + + Chat with member + Chat met lid + No comment provided by engineer. + + + Chat with members before they join. + No comment provided by engineer. + Chats Chats No comment provided by engineer. + + Chats with members + Chats met leden + No comment provided by engineer. + Check messages every 20 min. Controleer uw berichten elke 20 minuten. @@ -1572,6 +1723,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? @@ -1592,6 +1753,11 @@ Kleur mode No comment provided by engineer. + + Community guidelines violation + Schending van de communityrichtlijnen + report reason + Compare file Bestand vergelijken @@ -1625,17 +1791,7 @@ Conditions of use Gebruiksvoorwaarden - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Voorwaarden worden geaccepteerd voor operator(s): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1657,6 +1813,11 @@ ICE servers configureren No comment provided by engineer. + + Configure server operators + Serveroperators configureren + No comment provided by engineer. + Confirm Bevestigen @@ -1707,6 +1868,11 @@ Bevestig het uploaden No comment provided by engineer. + + Confirmed + Bevestigd + token status text + Connect Verbind @@ -1717,9 +1883,8 @@ Automatisch verbinden No comment provided by engineer. - - Connect incognito - Verbind incognito + + Connect faster! 🚀 No comment provided by engineer. @@ -1732,44 +1897,39 @@ Maak sneller verbinding met je vrienden. No comment provided by engineer. - - Connect to yourself? - Verbinding maken met jezelf? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Verbinding maken met jezelf? Dit is uw eigen SimpleX adres! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Verbinding maken met jezelf? Dit is uw eigen eenmalige link! - No comment provided by engineer. + new chat sheet title Connect via contact address Verbinding maken via contactadres - No comment provided by engineer. + new chat sheet title Connect via link Maak verbinding via link - No comment provided by engineer. + new chat sheet title Connect via one-time link Verbinden via een eenmalige link? - No comment provided by engineer. + new chat sheet title Connect with %@ Verbonden met %@ - No comment provided by engineer. + new chat action Connected @@ -1826,16 +1986,33 @@ Dit is uw eigen eenmalige link! Verbindings- en serverstatus. No comment provided by engineer. + + Connection blocked + Verbinding geblokkeerd + No comment provided by engineer. + Connection error Verbindingsfout - No comment provided by engineer. + alert title Connection error (AUTH) 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 @@ -1846,6 +2023,11 @@ Dit is uw eigen eenmalige link! 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 @@ -1859,7 +2041,7 @@ Dit is uw eigen eenmalige link! Connection timeout Timeout verbinding - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1911,6 +2093,10 @@ Dit is uw eigen eenmalige link! Contact voorkeuren No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt! @@ -1926,6 +2112,11 @@ 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 @@ -2001,6 +2192,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/). 💻 @@ -2016,9 +2212,8 @@ Dit is uw eigen eenmalige link! Maak een wachtrij server test step - - Create secret group - Maak een geheime groep aan + + Create your address No comment provided by engineer. @@ -2218,8 +2413,7 @@ Dit is uw eigen eenmalige link! Delete Verwijderen alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2261,6 +2455,11 @@ Dit is uw eigen eenmalige link! Chat verwijderen No comment provided by engineer. + + Delete chat messages from your device. + Verwijder chatberichten van uw apparaat. + No comment provided by engineer. + Delete chat profile Chatprofiel verwijderen @@ -2271,6 +2470,11 @@ Dit is uw eigen eenmalige link! Chatprofiel verwijderen? No comment provided by engineer. + + Delete chat with member? + Chat met lid verwijderen? + alert title + Delete chat? Chat verwijderen? @@ -2351,6 +2555,11 @@ Dit is uw eigen eenmalige link! Link verwijderen? No comment provided by engineer. + + Delete list? + Lijst verwijderen? + alert title + Delete member message? Bericht van lid verwijderen? @@ -2364,7 +2573,7 @@ Dit is uw eigen eenmalige link! Delete messages Verwijder berichten - No comment provided by engineer. + alert button Delete messages after @@ -2401,6 +2610,11 @@ Dit is uw eigen eenmalige link! 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. @@ -2456,11 +2670,19 @@ Dit is uw eigen eenmalige link! Ontvangstbewijzen! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Beschrijving No comment provided by engineer. + + Description too large + alert title + Desktop address Desktop adres @@ -2561,6 +2783,16 @@ Dit is uw eigen eenmalige link! 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 @@ -2651,6 +2883,11 @@ Dit is uw eigen eenmalige link! 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 @@ -2661,9 +2898,19 @@ Dit is uw eigen eenmalige link! 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 + alert action + + + Done + Klaar No comment provided by engineer. @@ -2675,7 +2922,7 @@ Dit is uw eigen eenmalige link! Download Downloaden alert button - chat item action +chat item action Download errors @@ -2742,6 +2989,10 @@ Dit is uw eigen eenmalige link! Groep profiel bewerken No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable Inschakelen @@ -2752,9 +3003,9 @@ Dit is uw eigen eenmalige link! Inschakelen (overschrijvingen behouden) No comment provided by engineer. - - Enable Flux - Flux inschakelen + + 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. @@ -2770,13 +3021,17 @@ Dit is uw eigen eenmalige link! Enable automatic message deletion? Automatisch verwijderen van berichten aanzetten? - No comment provided by engineer. + alert title Enable camera access Schakel cameratoegang in No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all Inschakelen voor iedereen @@ -2897,6 +3152,11 @@ Dit is uw eigen eenmalige link! 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 @@ -2972,6 +3232,11 @@ Dit is uw eigen eenmalige link! Fout bij het accepteren van een contactverzoek No comment provided by engineer. + + Error accepting member + Fout bij het accepteren van lid + alert title + Error adding member(s) Fout bij het toevoegen van leden @@ -2982,11 +3247,19 @@ Dit is uw eigen eenmalige link! Fout bij toevoegen server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Fout bij wijzigen van adres No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile Fout bij wijzigen van verbindingsprofiel @@ -3000,17 +3273,26 @@ Dit is uw eigen eenmalige link! Error changing setting Fout bij wijzigen van instelling - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -3027,6 +3309,11 @@ Dit is uw eigen eenmalige link! 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 @@ -3042,20 +3329,30 @@ Dit is uw eigen eenmalige link! 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 No comment provided by engineer. + + Error deleting chat + Fout bij het verwijderen van chat met lid + alert title + Error deleting chat database Fout bij het verwijderen van de chat database - No comment provided by engineer. + alert title Error deleting chat! Fout bij verwijderen gesprek! - No comment provided by engineer. + alert title Error deleting connection @@ -3065,12 +3362,12 @@ Dit is uw eigen eenmalige link! Error deleting database Fout bij het verwijderen van de database - No comment provided by engineer. + alert title Error deleting old database Fout bij het verwijderen van de oude database - No comment provided by engineer. + alert title Error deleting token @@ -3105,7 +3402,7 @@ Dit is uw eigen eenmalige link! Error exporting chat database Fout bij het exporteren van de chat database - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3115,7 +3412,7 @@ Dit is uw eigen eenmalige link! Error importing chat database Fout bij het importeren van de chat database - No comment provided by engineer. + alert title Error joining group @@ -3137,6 +3434,10 @@ Dit is uw eigen eenmalige link! Fout bij het openen van de chat No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file Fout bij ontvangen van bestand @@ -3152,10 +3453,24 @@ Dit is uw eigen eenmalige link! Fout bij opnieuw verbinden van servers No comment provided by engineer. + + Error registering for notifications + Fout bij registreren voor meldingen + alert title + + + Error rejecting contact request + alert title + Error removing member Fout bij verwijderen van lid - No comment provided by engineer. + alert title + + + Error reordering lists + Fout bij het opnieuw ordenen van lijsten + alert title Error resetting statistics @@ -3167,6 +3482,11 @@ Dit is uw eigen eenmalige link! 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 @@ -3217,6 +3537,10 @@ Dit is uw eigen eenmalige link! Fout bij verzenden van bericht No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Fout bij het instellen van ontvangst bevestiging! @@ -3235,7 +3559,7 @@ Dit is uw eigen eenmalige link! Error switching profile Fout bij wisselen van profiel - No comment provided by engineer. + alert title Error switching profile! @@ -3247,6 +3571,11 @@ Dit is uw eigen eenmalige link! 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 @@ -3290,7 +3619,13 @@ Dit is uw eigen eenmalige link! Error: %@ Fout: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3327,6 +3662,11 @@ Dit is uw eigen eenmalige link! Uitklappen chat item action + + Expired + Verlopen + token status text + Export database Database exporteren @@ -3367,20 +3707,35 @@ Dit is uw eigen eenmalige link! 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 - No comment provided by engineer. + file error alert title File errors: @@ -3389,6 +3744,13 @@ Dit is uw eigen eenmalige link! %@ 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. @@ -3444,6 +3806,10 @@ Dit is uw eigen eenmalige link! Bestanden en media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Bestanden en media zijn niet toegestaan. @@ -3484,6 +3850,23 @@ Dit is uw eigen eenmalige link! Vind chats sneller No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Mogelijk is de certificaat vingerafdruk in het server adres onjuist + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Herstel @@ -3514,6 +3897,11 @@ Dit is uw eigen eenmalige link! 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 %@: @@ -3529,6 +3917,11 @@ Dit is uw eigen eenmalige link! 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 @@ -3585,9 +3978,9 @@ Dit is uw eigen eenmalige link! 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 %1$@ failed to connect to destination server %2$@. Please try later. + De doorstuurserver %1$@ kon geen verbinding maken met de bestemmingsserver %2$@. Probeer het later opnieuw. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3653,6 +4046,11 @@ Fout: %2$@ 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! @@ -3676,7 +4074,7 @@ Fout: %2$@ Group already exists! Groep bestaat al! - No comment provided by engineer. + new chat sheet title Group display name @@ -3743,6 +4141,10 @@ Fout: %2$@ Groep profiel wordt opgeslagen op de apparaten van de leden, niet op de servers. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Groep welkom bericht @@ -3758,11 +4160,21 @@ Fout: %2$@ 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 @@ -3823,6 +4235,11 @@ Fout: %2$@ Hoe het de privacy helpt No comment provided by engineer. + + How it works + Hoe het werkt + alert button + How to Hoe @@ -3965,6 +4382,16 @@ Binnenkort meer verbeteringen! Geluiden tijdens het bellen No comment provided by engineer. + + Inappropriate content + Ongepaste inhoud + report reason + + + Inappropriate profile + Ongepast profiel + report reason + Incognito Incognito @@ -4057,6 +4484,31 @@ Binnenkort meer verbeteringen! 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 @@ -4075,7 +4527,7 @@ Binnenkort meer verbeteringen! Invalid link Ongeldige link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4185,40 +4637,35 @@ Binnenkort meer verbeteringen! Join - Word lid van + Word lid swipe action + + Join as %@ + deelnemen als %@ + No comment provided by engineer. + Join group Word lid van groep - No comment provided by engineer. + new chat sheet title Join group conversations Neem deel aan groepsgesprekken No comment provided by engineer. - - Join group? - Deelnemen aan groep? - No comment provided by engineer. - Join incognito Doe incognito mee No comment provided by engineer. - - Join with current profile - Word lid met huidig profiel - No comment provided by engineer. - Join your group? This is your link for group %@! Sluit u aan bij uw groep? Dit is jouw link voor groep %@! - No comment provided by engineer. + new chat action Joining group @@ -4245,6 +4692,10 @@ Dit is jouw link voor groep %@! Ongebruikte uitnodiging bewaren? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections Behoud uw verbindingen @@ -4300,6 +4751,10 @@ Dit is jouw link voor groep %@! Groep verlaten? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat Laten we praten in SimpleX Chat @@ -4330,6 +4785,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! @@ -4340,6 +4810,10 @@ Dit is jouw link voor groep %@! Live berichten No comment provided by engineer. + + Loading profile… + in progress text + Local name Lokale naam @@ -4415,11 +4889,29 @@ Dit is jouw link voor groep %@! Lid No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + Toelating van leden + No comment provided by engineer. + Member inactive Lid inactief item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + 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. @@ -4445,6 +4937,11 @@ Dit is jouw link voor groep %@! Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt! No comment provided by engineer. + + Member will join the group, accept member? + Lid zal toetreden tot de groep, lid accepteren? + alert message + Members can add message reactions. Groepsleden kunnen bericht reacties toevoegen. @@ -4455,6 +4952,11 @@ Dit is jouw link voor groep %@! 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. @@ -4480,6 +4982,11 @@ Dit is jouw link voor groep %@! Groepsleden kunnen spraak berichten verzenden. No comment provided by engineer. + + Mention members 👋 + Vermeld leden 👋 + No comment provided by engineer. + Menus Menu's @@ -4510,6 +5017,10 @@ Dit is jouw link voor groep %@! Bericht doorgestuurd item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. Het bericht kan later worden bezorgd als het lid actief wordt. @@ -4585,11 +5096,20 @@ Dit is jouw link voor groep %@! Berichten No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! 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 @@ -4690,6 +5210,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! @@ -4718,7 +5243,12 @@ Dit is jouw link voor groep %@! Mute Dempen - swipe action + notification label action + + + Mute all + Alles dempen + notification label action Muted when inactive! @@ -4768,7 +5298,12 @@ Dit is jouw link voor groep %@! Network status Netwerk status - No comment provided by engineer. + alert title + + + New + Nieuw + token status text New Passcode @@ -4820,6 +5355,10 @@ Dit is jouw link voor groep %@! Nieuwe gebeurtenissen notification + + New group role: Moderator + No comment provided by engineer. + New in %@ Nieuw in %@ @@ -4835,6 +5374,11 @@ Dit is jouw link voor groep %@! Nieuwe leden rol No comment provided by engineer. + + New member wants to join the group. + Nieuw lid wil zich bij de groep aansluiten. + rcv group event chat item + New message nieuw bericht @@ -4860,6 +5404,26 @@ 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 chats with members + Geen chats met leden + No comment provided by engineer. + No contacts selected Geen contacten geselecteerd @@ -4910,6 +5474,11 @@ Dit is jouw link voor groep %@! Geen media- en bestandsservers. servers error + + No message + Geen bericht + No comment provided by engineer. + No message servers. Geen berichtenservers. @@ -4935,6 +5504,10 @@ Dit is jouw link voor groep %@! Geen toestemming om spraakbericht op te nemen No comment provided by engineer. + + No private routing session + alert title + No push server Lokaal @@ -4965,6 +5538,16 @@ Dit is jouw link voor groep %@! 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. @@ -4975,6 +5558,11 @@ Dit is jouw link voor groep %@! Niet compatibel! No comment provided by engineer. + + Notes + Notities + No comment provided by engineer. + Nothing selected Niets geselecteerd @@ -4995,16 +5583,26 @@ 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. - disable members ("observer" role) - Nu kunnen beheerders: + Nu kunnen beheerders: - berichten van leden verwijderen. - schakel leden uit ("waarnemer" rol) No comment provided by engineer. @@ -5022,7 +5620,9 @@ Dit is jouw link voor groep %@! Ok OK - alert button + alert action +alert button +new chat action Old database @@ -5083,6 +5683,16 @@ Vereist het inschakelen van VPN. 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 bericht reacties toevoegen. @@ -5103,6 +5713,10 @@ Vereist het inschakelen van VPN. Alleen jij kunt verdwijnende berichten verzenden. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Alleen jij kunt spraak berichten verzenden. @@ -5128,6 +5742,10 @@ Vereist het inschakelen van VPN. Alleen uw contact kan verdwijnende berichten verzenden. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Alleen uw contact kan spraak berichten verzenden. @@ -5136,7 +5754,7 @@ Vereist het inschakelen van VPN. Open Open - No comment provided by engineer. + alert action Open Settings @@ -5151,28 +5769,65 @@ Vereist het inschakelen van VPN. Open chat Chat openen - No comment provided by engineer. + new chat action Open chat console Chat console openen authentication reason + + Open clean link + alert action + Open conditions Open voorwaarden No comment provided by engineer. + + Open full link + alert action + Open group Open groep - No comment provided by engineer. + new chat action + + + Open link? + Link openen? + alert title Open migration to another device Open de migratie naar een ander apparaat authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… App openen… @@ -5218,6 +5873,11 @@ Vereist het inschakelen van VPN. 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 @@ -5275,11 +5935,6 @@ Vereist het inschakelen van VPN. Wachtwoord om weer te geven No comment provided by engineer. - - Past member %@ - Voormalig lid %@ - past/unknown group member - Paste desktop address Desktopadres plakken @@ -5350,7 +6005,7 @@ Deel eventuele andere problemen met de ontwikkelaars. Please check your network connection with %@ and try again. Controleer uw netwerkverbinding met %@ en probeer het opnieuw. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5409,6 +6064,26 @@ 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 group moderators to review your request to join the group. + Wacht totdat de moderators van de groep uw verzoek tot lidmaatschap van de groep hebben beoordeeld. + snd group event chat item + + + 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 @@ -5419,11 +6094,6 @@ Fout: %@ Poort No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Mogelijk is de certificaat vingerafdruk in het server adres onjuist - server test error - Preserve the last message draft, with attachments. Bewaar het laatste berichtconcept, met bijlagen. @@ -5459,16 +6129,31 @@ Fout: %@ 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 @@ -5492,7 +6177,11 @@ Fout: %@ Private routing error Fout in privéroutering - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5544,6 +6233,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 @@ -5591,6 +6285,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Bescherm je chatprofielen met een wachtwoord! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout Protocol timeout @@ -5696,11 +6394,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Ontvangen op: %@ copied message info - - Received file event - Ontvangen bestandsgebeurtenis - notification - Received message Ontvangen bericht @@ -5801,11 +6494,27 @@ Schakel dit in in *Netwerk en servers*-instellingen. 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5815,7 +6524,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Reject contact request Contactverzoek afwijzen - No comment provided by engineer. + alert title + + + Reject member? + Lid afwijzen? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5842,6 +6556,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Verwijder afbeelding No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Lid verwijderen @@ -5857,6 +6575,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Wachtwoord van de keychain verwijderen? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate Opnieuw onderhandelen @@ -5872,11 +6594,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Heronderhandelen over versleuteling? No comment provided by engineer. - - Repeat connection request? - Verbindingsverzoek herhalen? - No comment provided by engineer. - Repeat download Herhaal het downloaden @@ -5887,11 +6604,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Herhaal import No comment provided by engineer. - - Repeat join request? - Deelnameverzoek herhalen? - No comment provided by engineer. - Repeat upload Herhaal het uploaden @@ -5902,6 +6614,61 @@ Schakel dit in in *Netwerk en servers*-instellingen. 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 sent to moderators + Rapport verzonden naar moderators + alert title + + + 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 @@ -5980,7 +6747,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Retry Opnieuw proberen - No comment provided by engineer. + alert action Reveal @@ -5992,11 +6759,20 @@ Schakel dit in in *Netwerk en servers*-instellingen. Voorwaarden bekijken No comment provided by engineer. - - Review later - Later beoordelen + + Review group members No comment provided by engineer. + + Review members + Leden beoordelen + admission stage + + + Review members before admitting ("knocking"). + Controleer de leden voordat u ze toelaat ('knocking'). + admission stage description + Revoke Intrekken @@ -6046,13 +6822,22 @@ Schakel dit in in *Netwerk en servers*-instellingen. Save Opslaan alert button - chat item action +chat item action Save (and notify contacts) Bewaar (en informeer contacten) alert button + + Save (and notify members) + alert button + + + Save admission settings? + Toegangsinstellingen opslaan? + alert title + Save and notify contact Opslaan en Contact melden @@ -6078,9 +6863,18 @@ Schakel dit in in *Netwerk en servers*-instellingen. Groep profiel opslaan No comment provided by engineer. + + Save group profile? + alert title + + + Save list + Lijst opslaan + No comment provided by engineer. + Save passphrase and open chat - Bewaar het wachtwoord en open je chats + Wachtwoord opslaan en open je chats No comment provided by engineer. @@ -6268,6 +7062,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Stuur een live bericht, het wordt bijgewerkt voor de ontvanger(s) terwijl u het typt No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to Stuur ontvangstbewijzen naar @@ -6318,6 +7116,11 @@ Schakel dit in in *Netwerk en servers*-instellingen. Meldingen verzenden No comment provided by engineer. + + Send private reports + Rapporteer privé + No comment provided by engineer. + Send questions and ideas Stuur vragen en ideeën @@ -6328,6 +7131,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. Ontvangstbewijzen verzenden No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. Stuur ze vanuit de galerij of aangepaste toetsenborden. @@ -6338,6 +7149,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Stuur tot 100 laatste berichten naar nieuwe leden. No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. Afzender heeft bestandsoverdracht geannuleerd. @@ -6403,11 +7218,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Direct verzonden No comment provided by engineer. - - Sent file event - Verzonden bestandsgebeurtenis - notification - Sent message Verzonden bericht @@ -6478,13 +7288,13 @@ Schakel dit in in *Netwerk en servers*-instellingen. Serverprotocol gewijzigd. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Server vereist autorisatie om te uploaden, wachtwoord controleren server test error @@ -6533,6 +7343,11 @@ Schakel dit in in *Netwerk en servers*-instellingen. 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… @@ -6553,6 +7368,16 @@ Schakel dit in in *Netwerk en servers*-instellingen. Stel het in in plaats van systeemverificatie. No comment provided by engineer. + + Set member admission + Toegang voor leden instellen + 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 @@ -6568,6 +7393,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Wachtwoord instellen om te exporteren No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! Stel het getoonde bericht in voor nieuwe leden! @@ -6597,7 +7426,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Share Deel alert action - chat item action +chat item action Share 1-time link @@ -6639,6 +7468,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. Deel link No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile Profiel delen @@ -6659,6 +7496,23 @@ Schakel dit in in *Netwerk en servers*-instellingen. Delen met contacten No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + Korte link + No comment provided by engineer. + Show QR code Toon QR-code @@ -6759,6 +7613,16 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX adres of eenmalige link? No comment provided by engineer. + + SimpleX address settings + Instellingen automatisch accepteren + alert title + + + SimpleX channel link + SimpleX channel link + simplex link type + SimpleX contact address SimpleX contact adres @@ -6799,6 +7663,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX-protocollen beoordeeld door Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Vereenvoudigde incognitomodus @@ -6861,6 +7729,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Iemand notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Vierkant, cirkel of iets daartussenin. @@ -6946,6 +7820,11 @@ Schakel dit in in *Netwerk en servers*-instellingen. Chat stoppen No comment provided by engineer. + + Storage + Opslag + No comment provided by engineer. + Strong Krachtig @@ -6953,7 +7832,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Submit - Indienen + Bevestigen No comment provided by engineer. @@ -7001,11 +7880,20 @@ Schakel dit in in *Netwerk en servers*-instellingen. TCP verbinding No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -7031,11 +7919,27 @@ Schakel dit in in *Netwerk en servers*-instellingen. Foto nemen No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tik op SimpleX-adres maken in het menu om het later te maken. No comment provided by engineer. + + Tap Join group + No comment provided by engineer. + Tap button Tik op de knop @@ -7074,13 +7978,18 @@ Schakel dit in in *Netwerk en servers*-instellingen. Temporary file error Tijdelijke bestandsfout - No comment provided by engineer. + 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 @@ -7118,6 +8027,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + 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. @@ -7178,6 +8091,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De hash van het vorige bericht is anders. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. Het bericht wordt verwijderd voor alle leden. @@ -7203,21 +8120,11 @@ 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. - No comment provided by engineer. - The same conditions will apply to operator **%@**. Dezelfde voorwaarden gelden voor operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - Dezelfde voorwaarden gelden voor operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! De tweede vooraf ingestelde operator in de app! @@ -7231,7 +8138,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. The sender will NOT be notified De afzender wordt NIET op de hoogte gebracht - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7283,6 +8190,11 @@ 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 definitief verloren. @@ -7300,7 +8212,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. @@ -7318,14 +8230,9 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Deze groep bestaat niet meer. No comment provided by engineer. - - This is your own SimpleX address! - Dit is uw eigen SimpleX adres! - No comment provided by engineer. - - - This is your own one-time link! - Dit is uw eigen eenmalige link! + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen. No comment provided by engineer. @@ -7333,11 +8240,24 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title Titel @@ -7420,11 +8340,19 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Om te verzenden No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. Om de servers van **%@** te gebruiken, moet u de gebruiksvoorwaarden accepteren. @@ -7445,6 +8373,11 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Schakel incognito in tijdens het verbinden. No comment provided by engineer. + + Token status: %@. + Tokenstatus: %@. + token status + Toolbar opacity De transparantie van de werkbalk @@ -7465,15 +8398,9 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc 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: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7610,13 +8537,18 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Unmute Dempen opheffen - swipe action + notification label action Unread Ongelezen swipe action + + Unsupported connection link + Niet-ondersteunde verbindingslink + No comment provided by engineer. + Up to 100 last messages are sent to new members. Er worden maximaal 100 laatste berichten naar nieuwe leden verzonden. @@ -7642,16 +8574,45 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Instellingen actualiseren? No comment provided by engineer. + + Updated conditions + Bijgewerkte voorwaarden + No comment provided by engineer. + Updating settings will re-connect the client to all servers. Door de instellingen bij te werken, wordt de client opnieuw verbonden met alle servers. No comment provided by engineer. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat Upgrade en open chat No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors Upload fouten @@ -7702,6 +8663,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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. + Gebruik TCP-poort 443 alleen voor vooraf ingestelde servers. + No comment provided by engineer. + Use chat Gebruik chat @@ -7710,7 +8681,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use current profile Gebruik het huidige profiel - No comment provided by engineer. + new chat action Use for files @@ -7737,10 +8708,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak De iOS-oproepinterface gebruiken No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile Gebruik een nieuw incognitoprofiel - No comment provided by engineer. + new chat action Use only local notifications? @@ -7777,6 +8752,11 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik de app met één hand. No comment provided by engineer. + + Use web port + Gebruik een webpoort + No comment provided by engineer. + User selection Gebruikersselectie @@ -7789,7 +8769,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Using SimpleX Chat servers. - SimpleX Chat servers gebruiken. + Gebruik SimpleX Chat servers. No comment provided by engineer. @@ -7967,6 +8947,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Welkom bericht is te lang No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new Wat is er nieuw @@ -8090,12 +9074,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak You are already connecting to %@. U maakt al verbinding met %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Je maakt al verbinding via deze eenmalige link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8105,35 +9089,33 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak You are already joining the group %@. Je bent al lid van de groep %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Je wordt al lid van de groep via deze link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Je wordt al lid van de groep via deze link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Je sluit je al aan bij de groep! Deelnameverzoek herhalen? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - U bent verbonden met de server die wordt gebruikt om berichten van dit contact te ontvangen. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Je bent uitgenodigd voor de groep No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + 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. @@ -8149,11 +9131,6 @@ Deelnameverzoek herhalen? U kunt dit wijzigen in de instellingen onder uiterlijk. No comment provided by engineer. - - You can configure operators in Network & servers settings. - U kunt operators configureren in Netwerk- en serverinstellingen. - No comment provided by engineer. - You can configure servers via settings. U kunt servers configureren via instellingen. @@ -8244,10 +9221,15 @@ Deelnameverzoek herhalen? U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails. alert message + + You can view your reports in Chat with admins. + U kunt uw rapporten bekijken in Chat met beheerders. + alert message + You can't send messages! Je kunt geen berichten versturen! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8259,17 +9241,12 @@ Deelnameverzoek herhalen? 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! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Je hebt al verbinding aangevraagd! Verbindingsverzoek herhalen? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8326,6 +9303,15 @@ 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 able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later! @@ -8351,11 +9337,6 @@ Verbindingsverzoek herhalen? U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat. No comment provided by engineer. - - You will connect to all group members. - Je maakt verbinding met alle leden. - 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. @@ -8391,16 +9372,15 @@ Verbindingsverzoek herhalen? 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 business contact + No comment provided by engineer. + Your calls Uw oproepen @@ -8426,11 +9406,19 @@ Verbindingsverzoek herhalen? Uw chat profielen No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching 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 + 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 (%@). @@ -8461,6 +9449,10 @@ Verbindingsverzoek herhalen? Je huidige profiel No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences Jouw voorkeuren @@ -8481,6 +9473,11 @@ Verbindingsverzoek herhalen? Uw profiel **%@** wordt gedeeld. No comment provided by engineer. + + 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 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. @@ -8491,11 +9488,6 @@ Verbindingsverzoek herhalen? Je profiel is gewijzigd. Als je het opslaat, wordt het bijgewerkte profiel naar al je contacten verzonden. alert message - - Your profile, contacts and delivered messages are stored on your device. - Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen. - No comment provided by engineer. - Your random profile Je willekeurige profiel @@ -8546,6 +9538,11 @@ Verbindingsverzoek herhalen? hier boven, kies dan: No comment provided by engineer. + + accepted %@ + geaccepteerd %@ + rcv group event chat item + accepted call geaccepteerde oproep @@ -8556,6 +9553,11 @@ Verbindingsverzoek herhalen? geaccepteerde uitnodiging chat list item title + + accepted you + heb je geaccepteerd + rcv group event chat item + admin Beheerder @@ -8576,6 +9578,11 @@ Verbindingsverzoek herhalen? versleuteling overeenkomen… chat item text + + all + alle + member criteria value + all members alle leden @@ -8591,6 +9598,11 @@ Verbindingsverzoek herhalen? en %lld andere gebeurtenissen No comment provided by engineer. + + archived report + gearchiveerd rapport + No comment provided by engineer. + attempts pogingen @@ -8623,13 +9635,14 @@ Verbindingsverzoek herhalen? 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 @@ -8656,6 +9669,11 @@ Verbindingsverzoek herhalen? bellen… call status + + can't send messages + kan geen berichten versturen + No comment provided by engineer. + cancelled %@ geannuleerd %@ @@ -8706,11 +9724,6 @@ Verbindingsverzoek herhalen? verbonden No comment provided by engineer. - - connected directly - direct verbonden - rcv group event chat item - connecting Verbinden @@ -8761,6 +9774,16 @@ Verbindingsverzoek herhalen? contactpersoon %1$@ gewijzigd in %2$@ profile update event chat item + + contact deleted + contact verwijderd + No comment provided by engineer. + + + contact disabled + contact uitgeschakeld + No comment provided by engineer. + contact has e2e encryption contact heeft e2e-codering @@ -8771,6 +9794,15 @@ Verbindingsverzoek herhalen? contact heeft geen e2e versleuteling No comment provided by engineer. + + contact not ready + contact niet klaar + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator creator @@ -8799,7 +9831,8 @@ Verbindingsverzoek herhalen? default (%@) standaard (%@) - pref value + delete after time +pref value default (no) @@ -8926,31 +9959,30 @@ Verbindingsverzoek herhalen? fout No comment provided by engineer. - - event happened - gebeurtenis gebeurd - No comment provided by engineer. - expired verlopen No comment provided by engineer. - - for better metadata privacy. - voor betere privacy van metagegevens. - No comment provided by engineer. - forwarded doorgestuurd No comment provided by engineer. + + group + shown on group welcome message + group deleted groep verwijderd No comment provided by engineer. + + group is deleted + groep is verwijderd + No comment provided by engineer. + group profile updated groep profiel bijgewerkt @@ -9046,11 +10078,6 @@ Verbindingsverzoek herhalen? cursief No comment provided by engineer. - - join as %@ - deelnemen als %@ - No comment provided by engineer. - left is vertrokken @@ -9076,6 +10103,11 @@ Verbindingsverzoek herhalen? is toegetreden rcv group event chat item + + member has old version + lid heeft oude versie + No comment provided by engineer. + message bericht @@ -9106,20 +10138,20 @@ Verbindingsverzoek herhalen? gemodereerd door %@ marked deleted chat item preview text + + moderator + moderator + member role + months maanden time unit - - mute - dempen - No comment provided by engineer. - never nooit - No comment provided by engineer. + delete after time new message @@ -9136,11 +10168,20 @@ Verbindingsverzoek herhalen? geen e2e versleuteling No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text geen tekst copied message info in history + + not synchronized + niet gesynchroniseerd + No comment provided by engineer. + observer Waarnemer @@ -9150,8 +10191,9 @@ Verbindingsverzoek herhalen? off uit enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9193,6 +10235,21 @@ Verbindingsverzoek herhalen? 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. + + + pending review + in afwachting van beoordeling + No comment provided by engineer. + quantum resistant e2e encryption quantum bestendige e2e-codering @@ -9208,6 +10265,11 @@ Verbindingsverzoek herhalen? bevestiging ontvangen… No comment provided by engineer. + + rejected + afgewezen + No comment provided by engineer. + rejected call geweigerde oproep @@ -9228,6 +10290,11 @@ Verbindingsverzoek herhalen? contactadres verwijderd profile update event chat item + + removed from group + verwijderd uit de groep + No comment provided by engineer. + removed profile picture profielfoto verwijderd @@ -9238,11 +10305,38 @@ Verbindingsverzoek herhalen? heeft je verwijderd rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + verzoek tot toetreding afgewezen + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect - gevraagd om verbinding te maken + verzocht om verbinding te maken chat list item title + + review + beoordeling + No comment provided by engineer. + + + reviewed by admins + beoordeeld door beheerders + No comment provided by engineer. + saved opgeslagen @@ -9278,11 +10372,6 @@ Verbindingsverzoek herhalen? beveiligingscode gewijzigd chat item text - - send direct message - stuur een direct bericht - No comment provided by engineer. - server queue info: %1$@ @@ -9299,7 +10388,7 @@ laatst ontvangen bericht: %2$@ set new profile picture - nieuwe profielfoto instellen + nieuwe profielfoto profile update event chat item @@ -9342,11 +10431,6 @@ laatst ontvangen bericht: %2$@ onbekende status No comment provided by engineer. - - unmute - dempen opheffen - No comment provided by engineer. - unprotected onbeschermd @@ -9437,10 +10521,10 @@ laatst ontvangen bericht: %2$@ jij No comment provided by engineer. - - you are invited to group - je bent uitgenodigd voor de groep - No comment provided by engineer. + + you accepted this member + je hebt dit lid geaccepteerd + snd group event chat item you are observer @@ -9511,7 +10595,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -9548,7 +10632,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -9570,7 +10654,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -9578,6 +10662,11 @@ laatst ontvangen bericht: %2$@ ‐%d nieuwe gebeurtenissen notification body + + From %d chat(s) + Van %d chat(s) + notification body + From: %@ Van: %@ @@ -9593,16 +10682,11 @@ laatst ontvangen bericht: %2$@ Nieuwe berichten notification - - New messages in %d chats - Nieuwe berichten in %d chats - notification body -
- +
@@ -9624,7 +10708,7 @@ laatst ontvangen bericht: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json index 4c631c367e..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 8cfdf56f66..fb46bcd1d9 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (można skopiować) @@ -114,10 +102,12 @@ %@ server + %@ serwer No comment provided by engineer. %@ servers + %@ serwery/ów No comment provided by engineer. @@ -200,6 +190,11 @@ %d sek time interval + + %d seconds(s) + %d sekundach + delete after time + %d skipped message(s) %d pominięte wiadomość(i) @@ -270,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 @@ -325,11 +315,6 @@ %u pominiętych wiadomości. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nowy) @@ -340,11 +325,6 @@ (to urządzenie v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Dodaj kontakt**: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku. @@ -382,6 +362,7 @@ **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. @@ -409,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). @@ -450,11 +426,6 @@ - historia edycji. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sek @@ -468,7 +439,8 @@ 1 day 1 dzień - time interval + delete after time +time interval 1 hour @@ -483,19 +455,28 @@ 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. @@ -513,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> @@ -574,6 +550,7 @@ About operators + O operatorach No comment provided by engineer. @@ -585,11 +562,21 @@ Accept Akceptuj accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + alert action + + + Accept as observer + alert action Accept conditions + Zaakceptuj warunki No comment provided by engineer. @@ -597,6 +584,10 @@ Zaakceptować prośbę o połączenie? No comment provided by engineer. + + Accept contact request + alert title + Accept contact request from %@? Zaakceptuj prośbę o kontakt od %@? @@ -605,11 +596,16 @@ Accept incognito Akceptuj incognito - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + alert title Accepted conditions + Zaakceptowano warunki No comment provided by engineer. @@ -622,6 +618,11 @@ Błędy potwierdzenia No comment provided by engineer. + + Active + Aktywne + token status text + Active connections Aktywne połączenia @@ -634,8 +635,18 @@ Add friends + Dodaj znajomych No comment provided by engineer. + + Add list + Dodaj listę + No comment provided by engineer. + + + Add message + placeholder for sending contact request + Add profile Dodaj profil @@ -653,6 +664,7 @@ Add team members + Dodaj członków zespołu No comment provided by engineer. @@ -660,6 +672,11 @@ 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ą @@ -667,14 +684,17 @@ 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. @@ -704,10 +724,12 @@ Address or 1-time link? + Adres czy jednorazowy link? No comment provided by engineer. Address settings + Ustawienia adresu No comment provided by engineer. @@ -730,6 +752,11 @@ 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. @@ -740,6 +767,11 @@ 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. @@ -757,6 +789,7 @@ 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. @@ -779,6 +812,15 @@ 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. @@ -819,6 +861,10 @@ Zezwól na obniżenie wersji No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny) @@ -854,6 +900,11 @@ 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. @@ -899,6 +950,10 @@ Zezwól swoim kontaktom na wysyłanie znikających wiadomości. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Zezwól swoim kontaktom na wysyłanie wiadomości głosowych. @@ -912,12 +967,12 @@ Already connecting! Już połączony! - No comment provided by engineer. + new chat sheet title Already joining the group! Już dołączono do grupy! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -934,6 +989,11 @@ 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 @@ -959,6 +1019,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 @@ -1004,6 +1069,21 @@ 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 @@ -1014,6 +1094,21 @@ 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 @@ -1084,11 +1179,6 @@ Automatyczne akceptowanie obrazów No comment provided by engineer. - - Auto-accept settings - Ustawienia automatycznej akceptacji - alert title - Back Wstecz @@ -1116,6 +1206,7 @@ Better calls + Lepsze połączenia No comment provided by engineer. @@ -1123,8 +1214,13 @@ 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. @@ -1139,16 +1235,31 @@ 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. + + Bio + No comment provided by engineer. + + + Bio too large + alert title + Black Czarny @@ -1199,6 +1310,10 @@ Rozmycie mediów No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości. @@ -1219,6 +1334,10 @@ Zarówno Ty, jak i Twój kontakt możecie wysyłać znikające wiadomości. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe. @@ -1231,10 +1350,21 @@ Business address + Adres firmowy No comment provided by engineer. Business chats + Czaty biznesowe + No comment provided by engineer. + + + Business connection + No comment provided by engineer. + + + Businesses + Firmy No comment provided by engineer. @@ -1242,6 +1372,12 @@ 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! @@ -1272,6 +1408,10 @@ Nie można zadzwonić do członka No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Nie można zaprosić kontaktu! @@ -1291,7 +1431,8 @@ Cancel Anuluj alert action - alert button +alert button +new chat action Cancel migration @@ -1328,8 +1469,13 @@ Zmień No comment provided by engineer. + + Change automatic message deletion? + alert title + Change chat profiles + Zmień profil czatu authentication reason @@ -1376,19 +1522,22 @@ Change self-destruct passcode Zmień pin samozniszczenia authentication reason - set passcode view +set passcode view Chat + Czat No comment provided by engineer. Chat already exists + Czat już istnieje No comment provided by engineer. Chat already exists! - No comment provided by engineer. + Czat już istnieje! + new chat sheet title Chat colors @@ -1412,12 +1561,12 @@ Chat database exported - Wyeksportowano bazę danych czatu + 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. @@ -1467,10 +1616,24 @@ 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. + + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + + + Chat with members before they join. No comment provided by engineer. @@ -1478,12 +1641,18 @@ Czaty No comment provided by engineer. + + Chats with members + 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. @@ -1541,6 +1710,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? @@ -1561,6 +1738,10 @@ Tryb koloru No comment provided by engineer. + + Community guidelines violation + report reason + Compare file Porównaj plik @@ -1578,27 +1759,23 @@ 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 - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + Warunki użytkowania + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1617,6 +1794,10 @@ Skonfiguruj serwery ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Potwierdź @@ -1667,6 +1848,10 @@ Potwierdź wgranie No comment provided by engineer. + + Confirmed + token status text + Connect Połącz @@ -1677,9 +1862,8 @@ Łącz automatycznie No comment provided by engineer. - - Connect incognito - Połącz incognito + + Connect faster! 🚀 No comment provided by engineer. @@ -1692,44 +1876,39 @@ Szybciej łącz się ze znajomymi. No comment provided by engineer. - - Connect to yourself? - Połączyć się ze sobą? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Połączyć się ze sobą? To jest twój własny adres SimpleX! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Połączyć się ze sobą? To jest twój jednorazowy link! - No comment provided by engineer. + new chat sheet title Connect via contact address Połącz przez adres kontaktowy - No comment provided by engineer. + new chat sheet title Connect via link Połącz się przez link - No comment provided by engineer. + new chat sheet title Connect via one-time link Połącz przez jednorazowy link - No comment provided by engineer. + new chat sheet title Connect with %@ Połącz z %@ - No comment provided by engineer. + new chat action Connected @@ -1786,16 +1965,29 @@ To jest twój jednorazowy link! Stan połączenia i serwerów. No comment provided by engineer. + + Connection blocked + No comment provided by engineer. + Connection error Błąd połączenia - No comment provided by engineer. + alert title Connection error (AUTH) 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 @@ -1806,6 +1998,10 @@ To jest twój jednorazowy link! 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. @@ -1818,7 +2014,7 @@ To jest twój jednorazowy link! Connection timeout Czas połączenia minął - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1870,6 +2066,10 @@ To jest twój jednorazowy link! Preferencje kontaktu No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Kontakt zostanie usunięty – nie można tego cofnąć! @@ -1885,6 +2085,10 @@ 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 @@ -1959,6 +2163,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/). 💻 @@ -1974,9 +2182,8 @@ To jest twój jednorazowy link! Utwórz kolejkę server test step - - Create secret group - Utwórz tajną grupę + + Create your address No comment provided by engineer. @@ -2174,8 +2381,7 @@ To jest twój jednorazowy link! Delete Usuń alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2216,6 +2422,10 @@ To jest twój jednorazowy link! Delete chat No comment provided by engineer. + + Delete chat messages from your device. + No comment provided by engineer. + Delete chat profile Usuń profil czatu @@ -2226,6 +2436,10 @@ To jest twój jednorazowy link! Usunąć profil czatu? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2305,6 +2519,10 @@ To jest twój jednorazowy link! Usunąć link? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Usunąć wiadomość członka? @@ -2318,7 +2536,7 @@ To jest twój jednorazowy link! Delete messages Usuń wiadomości - No comment provided by engineer. + alert button Delete messages after @@ -2354,6 +2572,10 @@ To jest twój jednorazowy link! 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. @@ -2408,11 +2630,19 @@ To jest twój jednorazowy link! Potwierdzenia dostawy! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Opis No comment provided by engineer. + + Description too large + alert title + Desktop address Adres komputera @@ -2512,6 +2742,14 @@ To jest twój jednorazowy link! Wyłącz blokadę SimpleX authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Wyłącz dla wszystkich @@ -2602,6 +2840,10 @@ To jest twój jednorazowy link! 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 @@ -2612,9 +2854,17 @@ To jest twój jednorazowy link! 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 + alert action + + + Done No comment provided by engineer. @@ -2626,7 +2876,7 @@ To jest twój jednorazowy link! Download Pobierz alert button - chat item action +chat item action Download errors @@ -2692,6 +2942,10 @@ To jest twój jednorazowy link! Edytuj profil grupy No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable Włącz @@ -2702,8 +2956,8 @@ To jest twój jednorazowy link! Włącz (zachowaj nadpisania) No comment provided by engineer. - - Enable Flux + + Enable Flux in Network & servers settings for better metadata privacy. No comment provided by engineer. @@ -2719,13 +2973,17 @@ To jest twój jednorazowy link! Enable automatic message deletion? Czy włączyć automatyczne usuwanie wiadomości? - No comment provided by engineer. + alert title Enable camera access Włącz dostęp do kamery No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all Włącz dla wszystkich @@ -2846,6 +3104,10 @@ To jest twój jednorazowy link! Renegocjacja szyfrowania nie powiodła się. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Wprowadź Pin @@ -2920,6 +3182,10 @@ To jest twój jednorazowy link! Błąd przyjmowania prośby o kontakt No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Błąd dodawania członka(ów) @@ -2929,11 +3195,19 @@ To jest twój jednorazowy link! Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Błąd zmiany adresu No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile Błąd zmiany połączenia profilu @@ -2947,17 +3221,25 @@ To jest twój jednorazowy link! Error changing setting Błąd zmiany ustawienia - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2974,6 +3256,10 @@ To jest twój jednorazowy link! 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 @@ -2989,20 +3275,28 @@ To jest twój jednorazowy link! Błąd tworzenia profilu! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Błąd odszyfrowania pliku No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Błąd usuwania bazy danych czatu - No comment provided by engineer. + alert title Error deleting chat! Błąd usuwania czatu! - No comment provided by engineer. + alert title Error deleting connection @@ -3012,12 +3306,12 @@ To jest twój jednorazowy link! Error deleting database Błąd usuwania bazy danych - No comment provided by engineer. + alert title Error deleting old database Błąd usuwania starej bazy danych - No comment provided by engineer. + alert title Error deleting token @@ -3052,7 +3346,7 @@ To jest twój jednorazowy link! Error exporting chat database Błąd eksportu bazy danych czatu - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3062,7 +3356,7 @@ To jest twój jednorazowy link! Error importing chat database Błąd importu bazy danych czatu - No comment provided by engineer. + alert title Error joining group @@ -3083,6 +3377,10 @@ To jest twój jednorazowy link! Błąd otwierania czatu No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file Błąd odbioru pliku @@ -3098,10 +3396,22 @@ To jest twój jednorazowy link! Błąd ponownego łączenia serwerów No comment provided by engineer. + + Error registering for notifications + alert title + + + Error rejecting contact request + alert title + Error removing member Błąd usuwania członka - No comment provided by engineer. + alert title + + + Error reordering lists + alert title Error resetting statistics @@ -3113,6 +3423,10 @@ To jest twój jednorazowy link! 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 @@ -3162,6 +3476,10 @@ To jest twój jednorazowy link! Błąd wysyłania wiadomości No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Błąd ustawiania potwierdzeń dostawy! @@ -3180,7 +3498,7 @@ To jest twój jednorazowy link! Error switching profile Błąd zmiany profilu - No comment provided by engineer. + alert title Error switching profile! @@ -3192,6 +3510,10 @@ To jest twój jednorazowy link! 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 @@ -3234,7 +3556,13 @@ To jest twój jednorazowy link! Error: %@ Błąd: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3270,6 +3598,10 @@ To jest twój jednorazowy link! Rozszerz chat item action + + Expired + token status text + Export database Eksportuj bazę danych @@ -3310,20 +3642,32 @@ To jest twój jednorazowy link! 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 - No comment provided by engineer. + file error alert title File errors: @@ -3332,6 +3676,11 @@ To jest twój jednorazowy link! %@ 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. @@ -3387,6 +3736,10 @@ To jest twój jednorazowy link! Pliki i media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Pliki i media są zabronione w tej grupie. @@ -3427,6 +3780,23 @@ To jest twój jednorazowy link! Szybciej znajduj czaty No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Napraw @@ -3457,6 +3827,10 @@ To jest twój jednorazowy link! 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 @@ -3470,6 +3844,10 @@ To jest twój jednorazowy link! 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. @@ -3523,9 +3901,9 @@ To jest twój jednorazowy link! 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 %1$@ failed to connect to destination server %2$@. Please try later. + Serwer przekazujący %1$@ nie mógł połączyć się z serwerem docelowym %2$@. Spróbuj ponownie później. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3591,6 +3969,10 @@ Błąd: %2$@ GIF-y i naklejki No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! Dzień dobry! @@ -3614,7 +3996,7 @@ Błąd: %2$@ Group already exists! Grupa już istnieje! - No comment provided by engineer. + new chat sheet title Group display name @@ -3681,6 +4063,10 @@ Błąd: %2$@ Profil grupy jest przechowywany na urządzeniach członków, a nie na serwerach. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Wiadomość powitalna grupy @@ -3696,11 +4082,19 @@ Błąd: %2$@ 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 @@ -3759,6 +4153,10 @@ Błąd: %2$@ How it helps privacy No comment provided by engineer. + + How it works + alert button + How to Jak @@ -3899,6 +4297,14 @@ More improvements are coming soon! Dźwięki w rozmowie No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Incognito @@ -3991,6 +4397,26 @@ More improvements are coming soon! 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 @@ -4009,7 +4435,7 @@ More improvements are coming soon! Invalid link Nieprawidłowy link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4121,37 +4547,32 @@ More improvements are coming soon! Dołącz swipe action + + Join as %@ + dołącz jako %@ + No comment provided by engineer. + Join group Dołącz do grupy - No comment provided by engineer. + new chat sheet title Join group conversations Dołącz do grupowej rozmowy No comment provided by engineer. - - Join group? - Dołączyć do grupy? - No comment provided by engineer. - Join incognito Dołącz incognito No comment provided by engineer. - - Join with current profile - Dołącz z obecnym profilem - No comment provided by engineer. - Join your group? This is your link for group %@! Dołączyć do twojej grupy? To jest twój link do grupy %@! - No comment provided by engineer. + new chat action Joining group @@ -4178,6 +4599,10 @@ To jest twój link do grupy %@! Zachować nieużyte zaproszenie? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections Zachowaj swoje połączenia @@ -4231,6 +4656,10 @@ To jest twój link do grupy %@! Opuścić grupę? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat Porozmawiajmy w SimpleX Chat @@ -4261,6 +4690,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! @@ -4271,6 +4712,10 @@ To jest twój link do grupy %@! Wiadomości na żywo No comment provided by engineer. + + Loading profile… + in progress text + Local name Nazwa lokalna @@ -4346,11 +4791,27 @@ To jest twój link do grupy %@! Członek No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + No comment provided by engineer. + Member inactive Członek nieaktywny item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + Member reports + chat feature + Member role will be changed to "%@". All chat members will be notified. No comment provided by engineer. @@ -4374,6 +4835,10 @@ To jest twój link do grupy %@! Członek zostanie usunięty z grupy - nie można tego cofnąć! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Członkowie grupy mogą dodawać reakcje wiadomości. @@ -4384,6 +4849,10 @@ To jest twój link do grupy %@! 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. @@ -4409,6 +4878,10 @@ To jest twój link do grupy %@! Członkowie grupy mogą wysyłać wiadomości głosowe. No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus Menu @@ -4439,6 +4912,10 @@ To jest twój link do grupy %@! Wiadomość przekazana item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny. @@ -4514,11 +4991,19 @@ To jest twój link do grupy %@! Wiadomości i pliki No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! 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 @@ -4619,6 +5104,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! @@ -4646,7 +5135,11 @@ To jest twój link do grupy %@! Mute Wycisz - swipe action + notification label action + + + Mute all + notification label action Muted when inactive! @@ -4694,7 +5187,11 @@ To jest twój link do grupy %@! Network status Status sieci - No comment provided by engineer. + alert title + + + New + token status text New Passcode @@ -4745,6 +5242,10 @@ To jest twój link do grupy %@! New events notification + + New group role: Moderator + No comment provided by engineer. + New in %@ Nowość w %@ @@ -4760,6 +5261,10 @@ To jest twój link do grupy %@! Nowa rola członka No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Nowa wiadomość @@ -4784,6 +5289,22 @@ 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 chats with members + No comment provided by engineer. + No contacts selected Nie wybrano kontaktów @@ -4833,6 +5354,10 @@ To jest twój link do grupy %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -4857,6 +5382,10 @@ To jest twój link do grupy %@! Brak uprawnień do nagrywania wiadomości głosowej No comment provided by engineer. + + No private routing session + alert title + No push server Lokalnie @@ -4883,6 +5412,14 @@ To jest twój link do grupy %@! 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. @@ -4893,6 +5430,10 @@ To jest twój link do grupy %@! Nie kompatybilny! No comment provided by engineer. + + Notes + No comment provided by engineer. + Nothing selected Nic nie jest zaznaczone @@ -4913,10 +5454,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. @@ -4939,7 +5488,9 @@ To jest twój link do grupy %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -4999,6 +5550,14 @@ Wymaga włączenia VPN. 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. @@ -5019,6 +5578,10 @@ Wymaga włączenia VPN. Tylko Ty możesz wysyłać znikające wiadomości. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Tylko Ty możesz wysyłać wiadomości głosowe. @@ -5044,6 +5607,10 @@ Wymaga włączenia VPN. Tylko Twój kontakt może wysyłać znikające wiadomości. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Tylko Twój kontakt może wysyłać wiadomości głosowe. @@ -5052,7 +5619,7 @@ Wymaga włączenia VPN. Open Otwórz - No comment provided by engineer. + alert action Open Settings @@ -5066,27 +5633,63 @@ Wymaga włączenia VPN. Open chat Otwórz czat - No comment provided by engineer. + new chat action Open chat console Otwórz konsolę czatu authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group Grupa otwarta - No comment provided by engineer. + new chat action + + + Open link? + alert title Open migration to another device Otwórz migrację na innym urządzeniu authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… Otwieranie aplikacji… @@ -5128,6 +5731,10 @@ Wymaga włączenia VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other Inne @@ -5185,11 +5792,6 @@ Wymaga włączenia VPN. Hasło do wyświetlenia No comment provided by engineer. - - Past member %@ - Były członek %@ - past/unknown group member - Paste desktop address Wklej adres komputera @@ -5260,7 +5862,7 @@ Proszę podzielić się innymi problemami z deweloperami. Please check your network connection with %@ and try again. Sprawdzić połączenie sieciowe z %@ i spróbować ponownie. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5319,6 +5921,22 @@ 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 group moderators to review your request to join the group. + snd group event chat item + + + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + token info + Polish interface Polski interfejs @@ -5329,11 +5947,6 @@ Błąd: %@ 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 - server test error - Preserve the last message draft, with attachments. Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami. @@ -5367,16 +5980,28 @@ Błąd: %@ 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 @@ -5400,7 +6025,11 @@ Błąd: %@ Private routing error Błąd prywatnego trasowania - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5452,6 +6081,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. @@ -5499,6 +6132,10 @@ Włącz w ustawianiach *Sieć i serwery* . Chroń swoje profile czatu hasłem! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout Limit czasu protokołu @@ -5604,11 +6241,6 @@ Włącz w ustawianiach *Sieć i serwery* . Otrzymane o: %@ copied message info - - Received file event - Otrzymano zdarzenie pliku - notification - Received message Otrzymano wiadomość @@ -5709,11 +6341,24 @@ Włącz w ustawianiach *Sieć i serwery* . 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5723,7 +6368,11 @@ Włącz w ustawianiach *Sieć i serwery* . Reject contact request Odrzuć prośbę kontaktu - No comment provided by engineer. + alert title + + + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5750,6 +6399,10 @@ Włącz w ustawianiach *Sieć i serwery* . Usuń obraz No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Usuń członka @@ -5765,6 +6418,10 @@ Włącz w ustawianiach *Sieć i serwery* . Usunąć hasło z pęku kluczy? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate Renegocjuj @@ -5780,11 +6437,6 @@ Włącz w ustawianiach *Sieć i serwery* . Renegocjować szyfrowanie? No comment provided by engineer. - - Repeat connection request? - Powtórzyć prośbę połączenia? - No comment provided by engineer. - Repeat download Powtórz pobieranie @@ -5795,11 +6447,6 @@ Włącz w ustawianiach *Sieć i serwery* . Powtórz importowanie No comment provided by engineer. - - Repeat join request? - Powtórzyć prośbę dołączenia? - No comment provided by engineer. - Repeat upload Powtórz wgrywanie @@ -5810,6 +6457,50 @@ Włącz w ustawianiach *Sieć i serwery* . 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 sent to moderators + alert title + + + 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 @@ -5888,7 +6579,7 @@ Włącz w ustawianiach *Sieć i serwery* . Retry Ponów - No comment provided by engineer. + alert action Reveal @@ -5899,10 +6590,18 @@ Włącz w ustawianiach *Sieć i serwery* . Review conditions No comment provided by engineer. - - Review later + + Review group members No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Odwołaj @@ -5952,13 +6651,21 @@ Włącz w ustawianiach *Sieć i serwery* . Save Zapisz alert button - chat item action +chat item action Save (and notify contacts) Zapisz (i powiadom kontakty) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Zapisz i powiadom kontakt @@ -5984,6 +6691,14 @@ Włącz w ustawianiach *Sieć i serwery* . Zapisz profil grupy No comment provided by engineer. + + Save group profile? + alert title + + + Save list + No comment provided by engineer. + Save passphrase and open chat Zapisz hasło i otwórz czat @@ -6174,6 +6889,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wysyłaj wiadomości na żywo - będą one aktualizowane dla odbiorcy(ów) w trakcie ich wpisywania No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to Wyślij potwierdzenia dostawy do @@ -6224,6 +6943,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wyślij powiadomienia No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas Wyślij pytania i pomysły @@ -6234,6 +6957,14 @@ Włącz w ustawianiach *Sieć i serwery* . Wyślij potwierdzenia No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. Wyślij je z galerii lub niestandardowych klawiatur. @@ -6244,6 +6975,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wysyłaj do 100 ostatnich wiadomości do nowych członków. No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. Nadawca anulował transfer pliku. @@ -6309,11 +7044,6 @@ Włącz w ustawianiach *Sieć i serwery* . Wysłano bezpośrednio No comment provided by engineer. - - Sent file event - Wyślij zdarzenie pliku - notification - Sent message Wyślij wiadomość @@ -6380,13 +7110,13 @@ Włącz w ustawianiach *Sieć i serwery* . Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Serwer wymaga autoryzacji do przesłania, sprawdź hasło server test error @@ -6435,6 +7165,10 @@ Włącz w ustawianiach *Sieć i serwery* . Ustaw 1 dzień No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… Ustaw nazwę kontaktu… @@ -6455,6 +7189,14 @@ Włącz w ustawianiach *Sieć i serwery* . Ustaw go zamiast uwierzytelniania systemowego. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Ustaw pin @@ -6470,6 +7212,10 @@ Włącz w ustawianiach *Sieć i serwery* . Ustaw hasło do eksportu No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! Ustaw wiadomość wyświetlaną nowym członkom! @@ -6499,7 +7245,7 @@ Włącz w ustawianiach *Sieć i serwery* . Share Udostępnij alert action - chat item action +chat item action Share 1-time link @@ -6538,6 +7284,14 @@ Włącz w ustawianiach *Sieć i serwery* . Udostępnij link No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile Udostępnij profil @@ -6558,6 +7312,22 @@ Włącz w ustawianiach *Sieć i serwery* . Udostępnij kontaktom No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + No comment provided by engineer. + Show QR code Pokaż kod QR @@ -6655,6 +7425,15 @@ Włącz w ustawianiach *Sieć i serwery* . SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX address settings + Ustawienia automatycznej akceptacji + alert title + + + SimpleX channel link + simplex link type + SimpleX contact address Adres kontaktowy SimpleX @@ -6694,6 +7473,10 @@ Włącz w ustawianiach *Sieć i serwery* . SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Uproszczony tryb incognito @@ -6754,6 +7537,11 @@ Włącz w ustawianiach *Sieć i serwery* . Ktoś notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. Kwadrat, okrąg lub cokolwiek pomiędzy. @@ -6839,6 +7627,10 @@ Włącz w ustawianiach *Sieć i serwery* . Zatrzymywanie czatu No comment provided by engineer. + + Storage + No comment provided by engineer. + Strong Silne @@ -6892,11 +7684,19 @@ Włącz w ustawianiach *Sieć i serwery* . Połączenie TCP No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -6922,10 +7722,26 @@ Włącz w ustawianiach *Sieć i serwery* . Zrób zdjęcie No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join group + No comment provided by engineer. + Tap button Naciśnij przycisk @@ -6964,13 +7780,17 @@ Włącz w ustawianiach *Sieć i serwery* . Temporary file error Tymczasowy błąd pliku - No comment provided by engineer. + 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 @@ -7008,6 +7828,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + 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ć. @@ -7066,6 +7890,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Hash poprzedniej wiadomości jest inny. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. Wiadomość zostanie usunięta dla wszystkich członków. @@ -7091,19 +7919,10 @@ 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. - No comment provided by engineer. - The same conditions will apply to operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! No comment provided by engineer. @@ -7116,7 +7935,7 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom The sender will NOT be notified Nadawca NIE zostanie powiadomiony - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7166,6 +7985,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. @@ -7201,14 +8024,8 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Ta grupa już nie istnieje. No comment provided by engineer. - - This is your own SimpleX address! - To jest twój własny adres SimpleX! - No comment provided by engineer. - - - This is your own one-time link! - To jest twój jednorazowy 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. @@ -7216,11 +8033,23 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title Tytuł @@ -7300,11 +8129,19 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Aby obsługiwać natychmiastowe powiadomienia push, należy zmigrować bazę danych czatu. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. @@ -7324,6 +8161,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Przełącz incognito przy połączeniu. No comment provided by engineer. + + Token status: %@. + token status + Toolbar opacity Nieprzezroczystość paska narzędzi @@ -7344,15 +8185,9 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.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: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7488,13 +8323,17 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Unmute Wyłącz wyciszenie - swipe action + notification label action Unread Nieprzeczytane swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. Do nowych członków wysyłanych jest do 100 ostatnich wiadomości. @@ -7520,16 +8359,44 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Zaktualizować ustawienia? No comment provided by engineer. + + Updated conditions + No comment provided by engineer. + Updating settings will re-connect the client to all servers. Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami. No comment provided by engineer. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat Zaktualizuj i otwórz czat No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors Błędy przesłania @@ -7579,6 +8446,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc 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 @@ -7587,7 +8462,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Use current profile Użyj obecnego profilu - No comment provided by engineer. + new chat action Use for files @@ -7612,10 +8487,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Użyj interfejsu połączeń iOS No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile Użyj nowego profilu incognito - No comment provided by engineer. + new chat action Use only local notifications? @@ -7651,6 +8530,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Korzystaj z aplikacji jedną ręką. No comment provided by engineer. + + Use web port + No comment provided by engineer. + User selection Wybór użytkownika @@ -7839,6 +8722,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wiadomość powitalna jest zbyt długa No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new Co nowego @@ -7960,12 +8847,12 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc You are already connecting to %@. Już się łączysz z %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Już jesteś połączony z tym jednorazowym linkiem! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7975,35 +8862,33 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc You are already joining the group %@. Już dołączasz do grupy %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Już dołączasz do grupy przez ten link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Już dołączasz do grupy przez ten link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Już dołączasz do grupy! Powtórzyć prośbę dołączenia? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Jesteś połączony z serwerem używanym do odbierania wiadomości od tego kontaktu. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Jesteś zaproszony do grupy No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + 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. @@ -8019,10 +8904,6 @@ Powtórzyć prośbę dołączenia? Możesz to zmienić w ustawieniach wyglądu. No comment provided by engineer. - - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. No comment provided by engineer. @@ -8111,10 +8992,14 @@ Powtórzyć prośbę dołączenia? Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Nie możesz wysyłać wiadomości! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8126,17 +9011,12 @@ Powtórzyć prośbę dołączenia? 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! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Już prosiłeś o połączenie! Powtórzyć prośbę połączenia? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8193,6 +9073,14 @@ 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 able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później! @@ -8218,11 +9106,6 @@ Powtórzyć prośbę połączenia? Uwierzytelnienie będzie wymagane przy uruchamianiu lub wznawianiu aplikacji po 30 sekundach w tle. No comment provided by engineer. - - You will connect to all group members. - Zostaniesz połączony ze wszystkimi członkami grupy. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne. @@ -8257,16 +9140,15 @@ Powtórzyć prośbę połączenia? 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 business contact + No comment provided by engineer. + Your calls Twoje połączenia @@ -8292,11 +9174,19 @@ Powtórzyć prośbę połączenia? Twoje profile czatu No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching 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 + 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 (%@). @@ -8327,6 +9217,10 @@ Powtórzyć prośbę połączenia? Twój obecny profil No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences Twoje preferencje @@ -8347,6 +9241,11 @@ Powtórzyć prośbę połączenia? Twój profil **%@** zostanie udostępniony. No comment provided by engineer. + + 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 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. @@ -8357,11 +9256,6 @@ Powtórzyć prośbę połączenia? Twój profil został zmieniony. Jeśli go zapiszesz, zaktualizowany profil zostanie wysłany do wszystkich kontaktów. alert message - - Your profile, contacts and delivered messages are stored on your device. - Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu. - No comment provided by engineer. - Your random profile Twój losowy profil @@ -8374,6 +9268,7 @@ Powtórzyć prośbę połączenia? Your servers + Twoje serwery No comment provided by engineer. @@ -8411,6 +9306,10 @@ Powtórzyć prośbę połączenia? powyżej, a następnie wybierz: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call zaakceptowane połączenie @@ -8420,6 +9319,10 @@ Powtórzyć prośbę połączenia? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin administrator @@ -8440,6 +9343,10 @@ Powtórzyć prośbę połączenia? uzgadnianie szyfrowania… chat item text + + all + member criteria value + all members wszyscy członkowie @@ -8455,6 +9362,10 @@ Powtórzyć prośbę połączenia? i %lld innych wydarzeń No comment provided by engineer. + + archived report + No comment provided by engineer. + attempts próby @@ -8493,7 +9404,8 @@ Powtórzyć prośbę połączenia? blocked by admin zablokowany przez admina - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8520,6 +9432,10 @@ Powtórzyć prośbę połączenia? dzwonie… call status + + can't send messages + No comment provided by engineer. + cancelled %@ anulowany %@ @@ -8570,11 +9486,6 @@ Powtórzyć prośbę połączenia? połączony No comment provided by engineer. - - connected directly - połącz bezpośrednio - rcv group event chat item - connecting łączenie @@ -8625,6 +9536,14 @@ Powtórzyć prośbę połączenia? kontakt %1$@ zmieniony na %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption kontakt posiada szyfrowanie e2e @@ -8635,6 +9554,14 @@ Powtórzyć prośbę połączenia? kontakt nie posiada szyfrowania e2e No comment provided by engineer. + + contact not ready + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator twórca @@ -8663,7 +9590,8 @@ Powtórzyć prośbę połączenia? default (%@) domyślne (%@) - pref value + delete after time +pref value default (no) @@ -8790,30 +9718,29 @@ Powtórzyć prośbę połączenia? błąd No comment provided by engineer. - - event happened - nowe wydarzenie - No comment provided by engineer. - expired wygasły No comment provided by engineer. - - for better metadata privacy. - No comment provided by engineer. - forwarded przekazane dalej No comment provided by engineer. + + group + shown on group welcome message + group deleted grupa usunięta No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated zaktualizowano profil grupy @@ -8909,11 +9836,6 @@ Powtórzyć prośbę połączenia? kursywa No comment provided by engineer. - - join as %@ - dołącz jako %@ - No comment provided by engineer. - left opuścił @@ -8939,6 +9861,10 @@ Powtórzyć prośbę połączenia? połączony rcv group event chat item + + member has old version + No comment provided by engineer. + message wiadomość @@ -8969,20 +9895,19 @@ Powtórzyć prośbę połączenia? moderowany przez %@ marked deleted chat item preview text + + moderator + member role + months miesiące time unit - - mute - wycisz - No comment provided by engineer. - never nigdy - No comment provided by engineer. + delete after time new message @@ -8999,11 +9924,19 @@ Powtórzyć prośbę połączenia? brak szyfrowania e2e No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text brak tekstu copied message info in history + + not synchronized + No comment provided by engineer. + observer obserwator @@ -9013,8 +9946,9 @@ Powtórzyć prośbę połączenia? off wyłączony enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9056,6 +9990,18 @@ Powtórzyć prośbę połączenia? peer-to-peer No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + + + pending review + No comment provided by engineer. + quantum resistant e2e encryption kwantowo odporne szyfrowanie e2e @@ -9071,6 +10017,10 @@ Powtórzyć prośbę połączenia? otrzymano potwierdzenie… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call odrzucone połączenie @@ -9091,6 +10041,10 @@ Powtórzyć prośbę połączenia? usunięto adres kontaktu profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture usunięto zdjęcie profilu @@ -9101,10 +10055,34 @@ Powtórzyć prośbę połączenia? usunął cię rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved zapisane @@ -9140,11 +10118,6 @@ Powtórzyć prośbę połączenia? kod bezpieczeństwa zmieniony chat item text - - send direct message - wyślij wiadomość bezpośrednią - No comment provided by engineer. - server queue info: %1$@ @@ -9204,11 +10177,6 @@ ostatnia otrzymana wiadomość: %2$@ nieznany status No comment provided by engineer. - - unmute - wyłącz wyciszenie - No comment provided by engineer. - unprotected niezabezpieczony @@ -9299,10 +10267,9 @@ ostatnia otrzymana wiadomość: %2$@ Ty No comment provided by engineer. - - you are invited to group - jesteś zaproszony do grupy - No comment provided by engineer. + + you accepted this member + snd group event chat item you are observer @@ -9373,7 +10340,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
@@ -9410,7 +10377,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
@@ -9432,13 +10399,17 @@ ostatnia otrzymana wiadomość: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9451,15 +10422,11 @@ ostatnia otrzymana wiadomość: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9481,7 +10448,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json index 0074d85662..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 93ba6f357b..d9af0624bf 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 @@ -2,7 +2,7 @@
- +
@@ -2352,8 +2352,8 @@ We will be adding server redundancy to prevent lost messages. Guarde a senha em um local seguro, você NÃO poderá alterá-la se a perder. No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Possivelmente, a impressão digital do certificado no endereço do servidor está incorreta server test error @@ -2727,8 +2727,8 @@ We will be adding server redundancy to prevent lost messages. Mensagens enviadas serão excluídas depois do tempo definido. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. O servidor requer autorização para criar filas, verifique a senha server test error @@ -3002,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. @@ -3934,7 +3934,7 @@ SimpleX servers cannot see your profile. itálico No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -4130,8 +4130,8 @@ SimpleX servers cannot see your profile. sim pref value - - you are invited to group + + You are invited to group você está convidado para o grupo No comment provided by engineer. @@ -4610,8 +4610,8 @@ Disponível na 5.1 Salvar e atualizar perfil do grupo No comment provided by engineer. - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. O servidor requer autorização para fazer upload, verifique a senha server test error @@ -5565,8 +5565,8 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi Chat migrated! Conversa migrada! - - Auto-accept settings + + SimpleX address settings Aceitar automaticamente configurações @@ -5645,11 +5645,99 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi Chat profile Perfil da conversa + + %d seconds(s) + %d segundo(s) + + + **Scan / Paste link**: to connect via a link you received. + **Escanear / Colar link**: para conectar através de um link que você recebeu. + + + 1 year + 1 ano + + + About operators + Sobre operadores + + + Accept as member + Aceitar como membro + + + Accept as observer + Aceitar como observador + + + Accept conditions + Aceitar condições + + + Accept contact request + Aceitar solicitação de contato + + + Accept member + Aceitar membro + + + Accepted conditions + Condições aceitas + + + Active + Ativo + + + Add friends + Adicionar amigos + + + Add list + Adicionar lista + + + Add message + Adicionar mensagem + + + Add team members + Adicionar membros da equipe + + + Add to list + Adicionar à lista + + + Add your team members to the conversations. + Adicione membros da sua equipe às conversas. + + + Added media & file servers + Servidores de mídia e arquivos adicionados + + + Added message servers + Servidores de mensagem adicionados + + + Address or 1-time link? + Endereço ou link de uso único? + + + Address settings + Configurações de endereço + + + All + Todos +
- +
@@ -5677,7 +5765,7 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
- +
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 de1787bdad..e4fac55bcb 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -39,7 +39,7 @@ Available in v5.1 (can be copied) - .(pode ser copiado) + (pode ser copiado) No comment provided by engineer. @@ -2453,8 +2453,8 @@ Available in v5.1 Polish interface No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -2829,12 +2829,12 @@ Available in v5.1 Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. server test error @@ -3146,8 +3146,8 @@ 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. + + Your profile is stored on your device and only shared with your contacts. No comment provided by engineer. @@ -4043,7 +4043,7 @@ SimpleX servers cannot see your profile. italic No comment provided by engineer. - + join as %@ No comment provided by engineer. @@ -4220,8 +4220,8 @@ SimpleX servers cannot see your profile. yes pref value - - you are invited to group + + You are invited to group No comment provided by engineer. 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 5809c65216..d885db1350 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (можно скопировать) @@ -144,7 +132,7 @@ %@, %@ and %lld other members connected - %@, %@ и %lld других членов соединены + установлено соединение с %@, %@ и %lld другими членами группы No comment provided by engineer. @@ -179,7 +167,7 @@ %d hours - %d ч. + %d час. time interval @@ -202,6 +190,11 @@ %d сек time interval + + %d seconds(s) + %d секунд + delete after time + %d skipped message(s) %d пропущенных сообщение(й) @@ -239,7 +232,7 @@ %lld members - Членов группы: %lld + %lld членов No comment provided by engineer. @@ -272,11 +265,6 @@ %lld новых языков интерфейса No comment provided by engineer. - - %lld second(s) - %lld секунд - No comment provided by engineer. - %lld seconds %lld секунд @@ -327,11 +315,6 @@ %u сообщений пропущено. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (новое) @@ -342,11 +325,6 @@ (это устройство v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Добавить контакт**: создать и поделиться новой ссылкой-приглашением. @@ -412,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). @@ -453,11 +426,6 @@ - история редактирования. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -471,7 +439,8 @@ 1 day 1 день - time interval + delete after time +time interval 1 hour @@ -486,12 +455,19 @@ 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 @@ -518,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> @@ -553,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. @@ -591,8 +562,19 @@ Accept Принять accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Принять в группу + alert action + + + Accept as observer + Принять как читателя + alert action Accept conditions @@ -604,6 +586,11 @@ Принять запрос? No comment provided by engineer. + + Accept contact request + Принять запрос на соединение + alert title + Accept contact request from %@? Принять запрос на соединение от %@? @@ -612,8 +599,13 @@ Accept incognito Принять инкогнито - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Принять члена + alert title Accepted conditions @@ -630,6 +622,11 @@ Ошибки подтверждения No comment provided by engineer. + + Active + Активный + token status text + Active connections Активные соединения @@ -645,6 +642,16 @@ Добавить друзей No comment provided by engineer. + + Add list + Добавить список + No comment provided by engineer. + + + Add message + Добавить cообщение + placeholder for sending contact request + Add profile Добавить профиль @@ -670,6 +677,11 @@ Добавить на другое устройство No comment provided by engineer. + + Add to list + Добавить в список + No comment provided by engineer. + Add welcome message Добавить приветственное сообщение @@ -745,6 +757,11 @@ Настройки сети No comment provided by engineer. + + All + Все + No comment provided by engineer. + All app data is deleted. Все данные приложения будут удалены. @@ -755,6 +772,11 @@ Все чаты и сообщения будут удалены - это нельзя отменить! 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. Все данные удаляются при его вводе. @@ -767,7 +789,7 @@ All group members will remain connected. - Все члены группы, которые соединились через эту ссылку, останутся в группе. + Все члены группы останутся соединены. No comment provided by engineer. @@ -795,6 +817,16 @@ Все профили 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. Все контакты, которые соединились через этот адрес, сохранятся. @@ -835,6 +867,11 @@ Разрешить прямую доставку No comment provided by engineer. + + Allow files and media only if your contact allows them. + Разрешить файлы и медиа, только если их разрешает Ваш контакт. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа) @@ -852,7 +889,7 @@ Allow sending direct messages to members. - Разрешить посылать прямые сообщения членам группы. + Разрешить личные сообщения членам группы. No comment provided by engineer. @@ -870,6 +907,11 @@ Разрешить необратимо удалять отправленные сообщения. (24 часа) No comment provided by engineer. + + Allow to report messsages to moderators. + Разрешить отправлять сообщения о нарушениях модераторам. + No comment provided by engineer. + Allow to send SimpleX links. Разрешить отправлять ссылки SimpleX. @@ -915,6 +957,11 @@ Разрешить Вашим контактам отправлять исчезающие сообщения. No comment provided by engineer. + + Allow your contacts to send files and media. + Разрешить Вашим контактам отправлять файлы и медиа. + No comment provided by engineer. + Allow your contacts to send voice messages. Разрешить Вашим контактам отправлять голосовые сообщения. @@ -928,12 +975,12 @@ Already connecting! Уже соединяется! - No comment provided by engineer. + new chat sheet title Already joining the group! Вступление в группу уже начато! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -950,6 +997,11 @@ Будет создан пустой профиль чата с указанным именем, и приложение откроется в обычном режиме. No comment provided by engineer. + + Another reason + Другая причина + report reason + Answer call Принять звонок @@ -975,6 +1027,11 @@ Приложение шифрует новые локальные файлы (кроме видео). No comment provided by engineer. + + App group: + Группа приложения: + No comment provided by engineer. + App icon Иконка @@ -1020,6 +1077,21 @@ Применить к 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 Архивировать и загрузить @@ -1030,6 +1102,21 @@ Архивируйте контакты чтобы продолжить переписку. 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 Архивированные контакты @@ -1100,11 +1187,6 @@ Автоприем изображений No comment provided by engineer. - - Auto-accept settings - Настройки автоприема - alert title - Back Назад @@ -1140,6 +1222,11 @@ Улучшенные группы No comment provided by engineer. + + Better groups performance + Улучшенная производительность групп + No comment provided by engineer. + Better message dates. Улучшенные даты сообщений. @@ -1160,6 +1247,11 @@ Улучшенные уведомления No comment provided by engineer. + + Better privacy and security + Улучшенная конфиденциальность и безопасность + No comment provided by engineer. + Better security ✅ Улучшенная безопасность ✅ @@ -1170,6 +1262,16 @@ Улучшенный интерфейс No comment provided by engineer. + + Bio + О себе + No comment provided by engineer. + + + Bio too large + Описание слишком длинное + alert title + Black Черная @@ -1187,7 +1289,7 @@ Block group members - Блокируйте членов группы + Заблокировать членов группы No comment provided by engineer. @@ -1197,7 +1299,7 @@ Block member for all? - Заблокировать члена для всех? + Заблокировать для всех? No comment provided by engineer. @@ -1220,6 +1322,11 @@ Размытие изображений No comment provided by engineer. + + Bot + Бот + No comment provided by engineer. + Both you and your contact can add message reactions. И Вы, и Ваш контакт можете добавлять реакции на сообщения. @@ -1240,6 +1347,11 @@ Вы и Ваш контакт можете отправлять исчезающие сообщения. No comment provided by engineer. + + Both you and your contact can send files and media. + Вы и Ваш контакт можете отправлять файлы и медиа. + No comment provided by engineer. + Both you and your contact can send voice messages. Вы и Ваш контакт можете отправлять голосовые сообщения. @@ -1260,11 +1372,30 @@ Бизнес разговоры No comment provided by engineer. + + Business connection + Бизнес контакт + 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! Звонок уже завершен! @@ -1292,9 +1423,14 @@ Can't call member - Не удается позвонить члену группы + Не удаётся позвонить члену группы No comment provided by engineer. + + Can't change profile + Нельзя поменять профиль + alert title + Can't invite contact! Нельзя пригласить контакт! @@ -1307,14 +1443,15 @@ Can't message member - Не удается написать члену группы + Не удаётся отправить сообщение члену группы No comment provided by engineer. Cancel Отменить alert action - alert button +alert button +new chat action Cancel migration @@ -1351,6 +1488,11 @@ Поменять No comment provided by engineer. + + Change automatic message deletion? + Измененить автоматическое удаление сообщений? + alert title + Change chat profiles Поменять профили @@ -1400,7 +1542,7 @@ Change self-destruct passcode Изменить код самоуничтожения authentication reason - set passcode view +set passcode view Chat @@ -1415,7 +1557,7 @@ Chat already exists! Разговор уже существует! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1502,11 +1644,31 @@ Разговор будет удален для Вас - это действие нельзя отменить! No comment provided by engineer. + + Chat with admins + Чат с админами + chat toolbar + + + Chat with member + Чат с членом группы + No comment provided by engineer. + + + Chat with members before they join. + Общайтесь с членами до того как принять их. + No comment provided by engineer. + Chats Чаты No comment provided by engineer. + + Chats with members + Чаты с членами группы + No comment provided by engineer. + Check messages every 20 min. Проверять сообщения каждые 20 минут. @@ -1572,6 +1734,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? Очистить личные заметки? @@ -1592,6 +1764,11 @@ Режим цветов No comment provided by engineer. + + Community guidelines violation + Нарушение правил группы + report reason + Compare file Сравнение файла @@ -1625,17 +1802,7 @@ Conditions of use Условия использования - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - Условия будут приняты для включенных операторов через 30 дней. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Условия будут приняты для оператора(ов): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1657,6 +1824,11 @@ Настройка ICE серверов No comment provided by engineer. + + Configure server operators + Настроить операторов серверов + No comment provided by engineer. + Confirm Подтвердить @@ -1707,6 +1879,11 @@ Подтвердить загрузку No comment provided by engineer. + + Confirmed + Подтвержденный + token status text + Connect Соединиться @@ -1717,9 +1894,9 @@ Соединяться автоматически No comment provided by engineer. - - Connect incognito - Соединиться Инкогнито + + Connect faster! 🚀 + Соединяйтесь быстрее! 🚀 No comment provided by engineer. @@ -1732,44 +1909,39 @@ Соединяйтесь с друзьями быстрее. No comment provided by engineer. - - Connect to yourself? - Соединиться с самим собой? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Соединиться с самим собой? Это ваш собственный адрес SimpleX! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Соединиться с самим собой? Это ваша собственная одноразовая ссылка! - No comment provided by engineer. + new chat sheet title Connect via contact address Соединиться через адрес - No comment provided by engineer. + new chat sheet title Connect via link Соединиться через ссылку - No comment provided by engineer. + new chat sheet title Connect via one-time link Соединиться через одноразовую ссылку - No comment provided by engineer. + new chat sheet title Connect with %@ Соединиться с %@ - No comment provided by engineer. + new chat action Connected @@ -1826,16 +1998,33 @@ This is your own one-time link! Состояние соединения и серверов. No comment provided by engineer. + + Connection blocked + Соединение заблокировано + No comment provided by engineer. + Connection error Ошибка соединения - No comment provided by engineer. + alert title Connection error (AUTH) Ошибка соединения (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 Уведомления по соединениям @@ -1846,6 +2035,11 @@ This is your own one-time link! Запрос на соединение отправлен! No comment provided by engineer. + + Connection requires encryption renegotiation. + Соединение требует повторного согласования шифрования. + No comment provided by engineer. + Connection security Безопасность соединения @@ -1859,7 +2053,7 @@ This is your own one-time link! Connection timeout Превышено время соединения - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1911,6 +2105,11 @@ This is your own one-time link! Предпочтения контакта No comment provided by engineer. + + Contact requests from groups + Запросы на соединение из групп + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Контакт будет удален — это нельзя отменить! @@ -1926,6 +2125,11 @@ This is your own one-time link! Контакты могут помечать сообщения для удаления; Вы сможете просмотреть их. No comment provided by engineer. + + Content violates conditions of use + Содержание нарушает условия использования + blocking reason + Continue Продолжить @@ -2001,6 +2205,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/). 💻 @@ -2016,9 +2225,9 @@ This is your own one-time link! Создание очереди server test step - - Create secret group - Создать скрытую группу + + Create your address + Создайте Ваш адрес No comment provided by engineer. @@ -2218,8 +2427,7 @@ This is your own one-time link! Delete Удалить alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2261,6 +2469,11 @@ This is your own one-time link! Удалить разговор No comment provided by engineer. + + Delete chat messages from your device. + Удалить сообщения с вашего устройства. + No comment provided by engineer. + Delete chat profile Удалить профиль чата @@ -2271,6 +2484,11 @@ This is your own one-time link! Удалить профиль? No comment provided by engineer. + + Delete chat with member? + Удалить чат с членом группы? + alert title + Delete chat? Удалить разговор? @@ -2351,6 +2569,11 @@ This is your own one-time link! Удалить ссылку? No comment provided by engineer. + + Delete list? + Удалить список? + alert title + Delete member message? Удалить сообщение участника? @@ -2364,7 +2587,7 @@ This is your own one-time link! Delete messages Удалить сообщения - No comment provided by engineer. + alert button Delete messages after @@ -2401,6 +2624,11 @@ This is your own one-time link! Удаление очереди server test step + + Delete report + Удалить сообщение о нарушении + No comment provided by engineer. + Delete up to 20 messages at once. Удаляйте до 20 сообщений за раз. @@ -2456,11 +2684,21 @@ This is your own one-time link! Отчёты о доставке! No comment provided by engineer. + + Deprecated options + Удалённые настройки + No comment provided by engineer. + Description Описание No comment provided by engineer. + + Description too large + Описание слишком длинное + alert title + Desktop address Адрес компьютера @@ -2543,12 +2781,12 @@ This is your own one-time link! 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. @@ -2561,6 +2799,16 @@ This is your own one-time link! Отключить блокировку SimpleX authentication reason + + Disable automatic message deletion? + Отключить автоматическое удаление сообщений? + alert title + + + Disable delete messages + Отключить удаление сообщений + alert button + Disable for all Выключить для всех @@ -2651,6 +2899,11 @@ This is your own one-time link! Не использовать учетные данные с прокси. No comment provided by engineer. + + Documents: + Документы: + No comment provided by engineer. + Don't create address Не создавать адрес @@ -2661,9 +2914,19 @@ This is your own one-time link! Не включать No comment provided by engineer. + + Don't miss important messages. + Не пропустите важные сообщения. + No comment provided by engineer. + Don't show again Не показывать + alert action + + + Done + Готово No comment provided by engineer. @@ -2675,7 +2938,7 @@ This is your own one-time link! Download Загрузить alert button - chat item action +chat item action Download errors @@ -2742,6 +3005,11 @@ This is your own one-time link! Редактировать профиль группы No comment provided by engineer. + + Empty message! + Пустое сообщение! + No comment provided by engineer. + Enable Включить @@ -2752,9 +3020,9 @@ This is your own one-time link! Включить (кроме исключений) No comment provided by engineer. - - Enable Flux - Включить Flux + + Enable Flux in Network & servers settings for better metadata privacy. + Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных. No comment provided by engineer. @@ -2770,13 +3038,18 @@ This is your own one-time link! Enable automatic message deletion? Включить автоматическое удаление сообщений? - No comment provided by engineer. + alert title Enable camera access Включить доступ к камере No comment provided by engineer. + + Enable disappearing messages by default. + Включите исчезающие сообщения по умолчанию. + No comment provided by engineer. + Enable for all Включить для всех @@ -2897,6 +3170,11 @@ This is your own one-time link! Ошибка нового соглашения о шифровании. No comment provided by engineer. + + Encryption renegotiation in progress. + Выполняется повторное согласование шифрования. + No comment provided by engineer. + Enter Passcode Введите Код @@ -2972,6 +3250,11 @@ This is your own one-time link! Ошибка при принятии запроса на соединение No comment provided by engineer. + + Error accepting member + Ошибка вступления члена группы + alert title + Error adding member(s) Ошибка при добавлении членов группы @@ -2982,11 +3265,21 @@ This is your own one-time link! Ошибка добавления сервера alert title + + Error adding short link + Ошибка создания короткой ссылки + No comment provided by engineer. + Error changing address Ошибка при изменении адреса No comment provided by engineer. + + Error changing chat profile + Ошибка изменения профиля + alert title + Error changing connection profile Ошибка при изменении профиля соединения @@ -3000,17 +3293,26 @@ This is your own one-time link! Error changing setting Ошибка при изменении настройки - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -3027,9 +3329,14 @@ This is your own one-time link! Ошибка при создании ссылки группы No comment provided by engineer. + + Error creating list + Ошибка создания списка + alert title + Error creating member contact - Ошибка создания контакта с членом группы + Ошибка при создании контакта No comment provided by engineer. @@ -3042,20 +3349,30 @@ This is your own one-time link! Ошибка создания профиля! No comment provided by engineer. + + Error creating report + Ошибка создания сообщения о нарушении + No comment provided by engineer. + Error decrypting file Ошибка расшифровки файла No comment provided by engineer. + + Error deleting chat + Ошибка при удалении чата с членом группы + alert title + Error deleting chat database Ошибка при удалении данных чата - No comment provided by engineer. + alert title Error deleting chat! Ошибка при удалении чата! - No comment provided by engineer. + alert title Error deleting connection @@ -3065,12 +3382,12 @@ This is your own one-time link! Error deleting database Ошибка при удалении данных чата - No comment provided by engineer. + alert title Error deleting old database Ошибка при удалении предыдущей версии данных чата - No comment provided by engineer. + alert title Error deleting token @@ -3105,7 +3422,7 @@ This is your own one-time link! Error exporting chat database Ошибка при экспорте архива чата - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3115,7 +3432,7 @@ This is your own one-time link! Error importing chat database Ошибка при импорте архива чата - No comment provided by engineer. + alert title Error joining group @@ -3134,7 +3451,12 @@ This is your own one-time link! Error opening chat - Ошибка доступа к чату + Ошибка при открытии чата + No comment provided by engineer. + + + Error opening group + Ошибка при открытии группы No comment provided by engineer. @@ -3152,10 +3474,25 @@ This is your own one-time link! Ошибка переподключения к серверам No comment provided by engineer. + + Error registering for notifications + Ошибка регистрации для уведомлений + alert title + + + Error rejecting contact request + Ошибка отклонения запроса + alert title + Error removing member Ошибка при удалении члена группы - No comment provided by engineer. + alert title + + + Error reordering lists + Ошибка сортировки списков + alert title Error resetting statistics @@ -3167,6 +3504,11 @@ This is your own one-time link! Ошибка при сохранении ICE серверов No comment provided by engineer. + + Error saving chat list + Ошибка сохранения списка чатов + alert title + Error saving group profile Ошибка при сохранении профиля группы @@ -3209,7 +3551,7 @@ This is your own one-time link! Error sending member contact invitation - Ошибка отправки приглашения члену группы + Ошибка при отправке приглашения члену No comment provided by engineer. @@ -3217,6 +3559,11 @@ This is your own one-time link! Ошибка при отправке сообщения No comment provided by engineer. + + Error setting auto-accept + Ошибка при установке автоприёма запросов + No comment provided by engineer. + Error setting delivery receipts! Ошибка настроек отчётов о доставке! @@ -3235,7 +3582,7 @@ This is your own one-time link! Error switching profile Ошибка переключения профиля - No comment provided by engineer. + alert title Error switching profile! @@ -3247,6 +3594,11 @@ This is your own one-time link! Ошибка синхронизации соединения No comment provided by engineer. + + Error testing server connection + Ошибка проверки соединения с сервером + No comment provided by engineer. + Error updating group link Ошибка обновления ссылки группы @@ -3290,7 +3642,14 @@ This is your own one-time link! Error: %@ Ошибка: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + Ошибка: %@. + server test error Error: URL is invalid @@ -3327,6 +3686,11 @@ This is your own one-time link! Раскрыть chat item action + + Expired + Истекший + token status text + Export database Экспорт архива чата @@ -3367,20 +3731,35 @@ This is your own one-time link! Быстрые и не нужно ждать, когда отправитель онлайн! 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 Ошибка файла - No comment provided by engineer. + file error alert title File errors: @@ -3389,6 +3768,13 @@ This is your own one-time link! %@ alert message + + File is blocked by server operator: +%@. + Файл заблокирован оператором сервера: +%@. + file error text + File not found - most likely file was deleted or cancelled. Файл не найден - скорее всего, файл был удален или отменен. @@ -3444,6 +3830,11 @@ This is your own one-time link! Файлы и медиа chat feature + + Files and media are prohibited in this chat. + Файлы и медиа запрещены в этом чате. + No comment provided by engineer. + Files and media are prohibited. Файлы и медиа запрещены в этой группе. @@ -3484,6 +3875,26 @@ This is your own one-time link! Быстро найти чаты No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + Хэш в адресе сервера назначения не соответствует сертификату: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + Хэш в адресе пересылающего сервера не соответствует сертификату: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Возможно, хэш сертификата в адресе сервера неверный. + server test error + + + Fingerprint in server address does not match certificate: %@. + Хэш в адресе сервера не соответствует сертификату: %@. + No comment provided by engineer. + Fix Починить @@ -3511,7 +3922,12 @@ This is your own one-time link! Fix not supported by group member - Починка не поддерживается членом группы + Починка не поддерживается членом группы. + No comment provided by engineer. + + + For all moderators + Для всех модераторов No comment provided by engineer. @@ -3529,6 +3945,11 @@ This is your own one-time link! Например, если Ваш контакт получает сообщения через сервер SimpleX Chat, Ваше приложение доставит их через сервер Flux. No comment provided by engineer. + + For me + Для меня + No comment provided by engineer. + For private routing Для доставки сообщений @@ -3585,9 +4006,9 @@ This is your own one-time link! No comment provided by engineer. - Forwarding server %@ failed to connect to destination server %@. Please try later. - Пересылающий сервер %@ не смог подключиться к серверу назначения %@. Попробуйте позже. - No comment provided by engineer. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + Пересылающий сервер %1$@ не смог подключиться к серверу назначения %2$@. Попробуйте позже. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3653,6 +4074,11 @@ Error: %2$@ ГИФ файлы и стикеры No comment provided by engineer. + + Get notified when mentioned. + Уведомления, когда Вас упомянули. + No comment provided by engineer. + Good afternoon! Добрый день! @@ -3676,7 +4102,7 @@ Error: %2$@ Group already exists! Группа уже существует! - No comment provided by engineer. + new chat sheet title Group display name @@ -3743,6 +4169,11 @@ Error: %2$@ Профиль группы хранится на устройствах членов, а не на серверах. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + Профиль группы изменен. Если Вы сохраните его, новый профиль будет отправлен членам группы. + alert message + Group welcome message Приветственное сообщение группы @@ -3758,11 +4189,21 @@ Error: %2$@ Группа будет удалена для Вас - это действие нельзя отменить! 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 Скрытое @@ -3823,6 +4264,11 @@ Error: %2$@ Как это улучшает конфиденциальность No comment provided by engineer. + + How it works + Как это работает + alert button + How to Инфо @@ -3964,6 +4410,16 @@ More improvements are coming soon! Звуки во время звонков No comment provided by engineer. + + Inappropriate content + Неприемлемый контент + report reason + + + Inappropriate profile + Неприемлемый профиль + report reason + Incognito Инкогнито @@ -4056,6 +4512,31 @@ More improvements are coming soon! Цвета интерфейса 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 код @@ -4074,7 +4555,7 @@ More improvements are coming soon! Invalid link Ошибка ссылки - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4187,37 +4668,32 @@ More improvements are coming soon! Вступить swipe action + + Join as %@ + вступить как %@ + No comment provided by engineer. + Join group Вступить в группу - No comment provided by engineer. + new chat sheet title Join group conversations Присоединяйтесь к разговорам в группах No comment provided by engineer. - - Join group? - Вступить в группу? - No comment provided by engineer. - Join incognito Вступить инкогнито No comment provided by engineer. - - Join with current profile - Вступить с активным профилем - No comment provided by engineer. - Join your group? This is your link for group %@! Вступить в вашу группу? Это ваша ссылка на группу %@! - No comment provided by engineer. + new chat action Joining group @@ -4244,6 +4720,11 @@ This is your link for group %@! Оставить неиспользованное приглашение? alert title + + Keep your chats clean + Очищайте Ваши чаты + No comment provided by engineer. + Keep your connections Сохраните Ваши соединения @@ -4299,6 +4780,11 @@ This is your link for group %@! Выйти из группы? No comment provided by engineer. + + Less traffic on mobile networks. + Меньше трафик в мобильных сетях. + No comment provided by engineer. + Let's talk in SimpleX Chat Давайте поговорим в SimpleX Chat @@ -4329,6 +4815,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! Живое сообщение! @@ -4339,6 +4840,11 @@ This is your link for group %@! "Живые" сообщения No comment provided by engineer. + + Loading profile… + Загрузка профиля… + in progress text + Local name Локальное имя @@ -4414,11 +4920,31 @@ This is your link for group %@! Член группы No comment provided by engineer. + + Member %@ + Член группы %@ + past/unknown group member + + + Member admission + Приём членов в группу + No comment provided by engineer. + Member inactive Член неактивен item status text + + Member is deleted - can't accept request + Член группы удалён - невозможно принять запрос + No comment provided by engineer. + + + Member reports + Сообщения о нарушениях + chat feature + Member role will be changed to "%@". All chat members will be notified. Роль участника будет изменена на "%@". Все участники разговора получат уведомление. @@ -4426,12 +4952,12 @@ This is your link for group %@! 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. @@ -4444,39 +4970,54 @@ This is your link for group %@! Член группы будет удален - это действие нельзя отменить! No comment provided by engineer. + + Member will join the group, accept member? + Участник хочет присоединиться к группе. Принять? + alert message + Members can add message reactions. - Члены группы могут добавлять реакции на сообщения. + Члены могут добавлять реакции на сообщения. No comment provided by engineer. Members can irreversibly delete sent messages. (24 hours) - Члены группы могут необратимо удалять отправленные сообщения. (24 часа) + Члены могут необратимо удалять отправленные сообщения. (24 часа) + No comment provided by engineer. + + + Members can report messsages to moderators. + Члены группы могут пожаловаться модераторам. No comment provided by engineer. Members can send SimpleX links. - Члены группы могут отправлять ссылки SimpleX. + Члены могут отправлять ссылки 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. @@ -4509,6 +5050,11 @@ This is your link for group %@! Сообщение переслано item status text + + Message instantly once you tap Connect. + Отправляйте сообщения сразу после соединения. + No comment provided by engineer. + Message may be delivered later if member becomes active. Сообщение может быть доставлено позже, если член группы станет активным. @@ -4584,11 +5130,21 @@ This is your link for group %@! Сообщения No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + Сообщения защищены **end-to-end шифрованием**. + No comment provided by engineer. + Messages from %@ will be shown! Сообщения от %@ будут показаны! No comment provided by engineer. + + Messages in this chat will never be deleted. + Сообщения в этом чате никогда не будут удалены. + alert message + Messages received Получено сообщений @@ -4689,6 +5245,11 @@ This is your link for group %@! Модерировано: %@ copied message info + + More + Больше + swipe action + More improvements are coming soon! Дополнительные улучшения скоро! @@ -4717,7 +5278,12 @@ This is your link for group %@! Mute Без звука - swipe action + notification label action + + + Mute all + Все без звука + notification label action Muted when inactive! @@ -4731,7 +5297,7 @@ This is your link for group %@! Network & servers - Сеть & серверы + Сеть и серверы No comment provided by engineer. @@ -4767,7 +5333,12 @@ This is your link for group %@! Network status Состояние сети - No comment provided by engineer. + alert title + + + New + Новый + token status text New Passcode @@ -4819,6 +5390,11 @@ This is your link for group %@! Новые события notification + + New group role: Moderator + Новая роль в группах: Модератор + No comment provided by engineer. + New in %@ Новое в %@ @@ -4834,6 +5410,11 @@ This is your link for group %@! Роль члена группы No comment provided by engineer. + + New member wants to join the group. + Новый участник хочет присоединиться к группе. + rcv group event chat item + New message Новое сообщение @@ -4859,6 +5440,26 @@ 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 chats with members + Нет чатов с членами группы + No comment provided by engineer. + No contacts selected Контакты не выбраны @@ -4909,6 +5510,11 @@ This is your link for group %@! Нет серверов файлов и медиа. servers error + + No message + Нет сообщения + No comment provided by engineer. + No message servers. Нет серверов сообщений. @@ -4934,6 +5540,11 @@ This is your link for group %@! Нет разрешения для записи голосового сообщения No comment provided by engineer. + + No private routing session + Нет сессии конфиденциальной доставки + alert title + No push server Без сервера нотификаций @@ -4964,6 +5575,16 @@ This is your link for group %@! Нет серверов для отправки файлов. servers error + + No token! + Нет токена! + alert title + + + No unread chats + Нет непрочитанных чатов + No comment provided by engineer. + No user identifiers. Без идентификаторов пользователей. @@ -4974,6 +5595,11 @@ This is your link for group %@! Несовместимая версия! No comment provided by engineer. + + Notes + Заметки + No comment provided by engineer. + Nothing selected Ничего не выбрано @@ -4994,18 +5620,28 @@ 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. @@ -5021,7 +5657,9 @@ This is your link for group %@! Ok Ок - alert button + alert action +alert button +new chat action Old database @@ -5082,6 +5720,16 @@ Requires compatible VPN. Только владельцы группы могут разрешить голосовые сообщения. 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. Только Вы можете добавлять реакции на сообщения. @@ -5102,6 +5750,11 @@ Requires compatible VPN. Только Вы можете отправлять исчезающие сообщения. No comment provided by engineer. + + Only you can send files and media. + Только Вы можете отправлять файлы и медиа. + No comment provided by engineer. + Only you can send voice messages. Только Вы можете отправлять голосовые сообщения. @@ -5127,6 +5780,11 @@ Requires compatible VPN. Только Ваш контакт может отправлять исчезающие сообщения. No comment provided by engineer. + + Only your contact can send files and media. + Только Ваш контакт может отправлять файлы и медиа. + No comment provided by engineer. + Only your contact can send voice messages. Только Ваш контакт может отправлять голосовые сообщения. @@ -5135,7 +5793,7 @@ Requires compatible VPN. Open Открыть - No comment provided by engineer. + alert action Open Settings @@ -5150,28 +5808,73 @@ Requires compatible VPN. Open chat Открыть чат - No comment provided by engineer. + new chat action Open chat console Открыть консоль authentication reason + + Open clean link + Открыть очищенную ссылку + alert action + Open conditions Открыть условия No comment provided by engineer. + + Open full link + Открыть полную ссылку + alert action + Open group Открыть группу - No comment provided by engineer. + new chat action + + + Open link? + Открыть ссылку? + alert title Open migration to another device Открытие миграции на другое устройство authentication reason + + Open new chat + Открыть новый чат + new chat action + + + Open new group + Открыть новую группу + new chat action + + + Open to accept + Откройте чтобы принять + No comment provided by engineer. + + + Open to connect + Откройте чтобы соединиться + No comment provided by engineer. + + + Open to join + Откройте чтобы вступить + No comment provided by engineer. + + + Open to use bot + Откройте чтобы использовать бот + No comment provided by engineer. + Opening app… Приложение отрывается… @@ -5217,6 +5920,11 @@ Requires compatible VPN. Или поделиться конфиденциально No comment provided by engineer. + + Organize chats into lists + Организуйте чаты в списки + No comment provided by engineer. + Other Другaя сеть @@ -5274,11 +5982,6 @@ Requires compatible VPN. Пароль чтобы раскрыть No comment provided by engineer. - - Past member %@ - Бывший член %@ - past/unknown group member - Paste desktop address Вставить адрес компьютера @@ -5301,7 +6004,7 @@ Requires compatible VPN. Pending - В ожидании + Ожидает No comment provided by engineer. @@ -5349,7 +6052,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. Пожалуйста, проверьте Ваше соединение с %@ и попробуйте еще раз. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5408,6 +6111,26 @@ Error: %@ Пожалуйста, надежно сохраните пароль, Вы НЕ сможете его поменять, если потеряете. No comment provided by engineer. + + Please try to disable and re-enable notfications. + Попробуйте выключить и снова включить уведомления. + token info + + + Please wait for group moderators to review your request to join the group. + Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление. + snd group event chat item + + + Please wait for token activation to complete. + Пожалуйста, дождитесь завершения активации токена. + token info + + + Please wait for token to be registered. + Пожалуйста, дождитесь регистрации токена. + token info + Polish interface Польский интерфейс @@ -5418,11 +6141,6 @@ Error: %@ Порт No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Возможно, хэш сертификата в адресе сервера неверный - server test error - Preserve the last message draft, with attachments. Сохранить последний черновик, вместе с вложениями. @@ -5458,16 +6176,31 @@ Error: %@ Конфиденциальность для ваших покупателей. 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 Конфиденциальная доставка сообщений @@ -5491,7 +6224,12 @@ Error: %@ Private routing error Ошибка конфиденциальной доставки - No comment provided by engineer. + alert title + + + Private routing timeout + Таймаут конфиденциальной доставки + alert title Profile and server connections @@ -5543,6 +6281,11 @@ Error: %@ Запретить реакции на сообщения. No comment provided by engineer. + + Prohibit reporting messages to moderators. + Запретить жаловаться модераторам группы. + No comment provided by engineer. + Prohibit sending SimpleX links. Запретить отправку ссылок SimpleX. @@ -5590,6 +6333,11 @@ Enable in *Network & servers* settings. Защитите Ваши профили чата паролем! No comment provided by engineer. + + Protocol background timeout + Фоновый таймаут протокола + No comment provided by engineer. + Protocol timeout Таймаут протокола @@ -5695,11 +6443,6 @@ Enable in *Network & servers* settings. Получено: %@ copied message info - - Received file event - Загрузка файла - notification - Received message Полученное сообщение @@ -5800,11 +6543,27 @@ Enable in *Network & servers* settings. Уменьшенное потребление батареи 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5814,7 +6573,12 @@ Enable in *Network & servers* settings. Reject contact request Отклонить запрос - No comment provided by engineer. + alert title + + + Reject member? + Отклонить участника? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5841,6 +6605,11 @@ Enable in *Network & servers* settings. Удалить изображение No comment provided by engineer. + + Remove link tracking + Удалять параметры отслеживания + No comment provided by engineer. + Remove member Удалить члена группы @@ -5856,6 +6625,11 @@ Enable in *Network & servers* settings. Удалить пароль из Keychain? No comment provided by engineer. + + Removes messages and blocks members. + Может удалять сообщения и блокировать членов. + No comment provided by engineer. + Renegotiate Пересогласовать @@ -5871,11 +6645,6 @@ Enable in *Network & servers* settings. Пересогласовать шифрование? No comment provided by engineer. - - Repeat connection request? - Повторить запрос на соединение? - No comment provided by engineer. - Repeat download Повторить загрузку @@ -5886,11 +6655,6 @@ Enable in *Network & servers* settings. Повторить импорт No comment provided by engineer. - - Repeat join request? - Повторить запрос на вступление? - No comment provided by engineer. - Repeat upload Повторить загрузку @@ -5901,6 +6665,61 @@ Enable in *Network & servers* settings. Ответить 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 sent to moderators + Жалоба отправлена модераторам + alert title + + + 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 Обязательно @@ -5979,7 +6798,7 @@ Enable in *Network & servers* settings. Retry Повторить - No comment provided by engineer. + alert action Reveal @@ -5991,11 +6810,21 @@ Enable in *Network & servers* settings. Посмотреть условия No comment provided by engineer. - - Review later - Посмотреть позже + + Review group members + Одобрять членов группы No comment provided by engineer. + + Review members + Одобрять членов + admission stage + + + Review members before admitting ("knocking"). + Вручную одобрять членов для вступления в группу. + admission stage description + Revoke Отозвать @@ -6045,13 +6874,23 @@ Enable in *Network & servers* settings. Save Сохранить alert button - chat item action +chat item action Save (and notify contacts) Сохранить (и уведомить контакты) alert button + + Save (and notify members) + Сохранить (и уведомить членов) + alert button + + + Save admission settings? + Сохранить настройки вступления? + alert title + Save and notify contact Сохранить и уведомить контакт @@ -6077,6 +6916,16 @@ Enable in *Network & servers* settings. Сохранить профиль группы No comment provided by engineer. + + Save group profile? + Сохранить профиль группы? + alert title + + + Save list + Сохранить список + No comment provided by engineer. + Save passphrase and open chat Сохранить пароль и открыть чат @@ -6267,6 +7116,11 @@ Enable in *Network & servers* settings. Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите No comment provided by engineer. + + Send contact request? + Отправить запрос на соединение? + No comment provided by engineer. + Send delivery receipts to Отправка отчётов о доставке @@ -6317,6 +7171,11 @@ Enable in *Network & servers* settings. Отправлять уведомления No comment provided by engineer. + + Send private reports + Вы можете сообщить о нарушениях + No comment provided by engineer. + Send questions and ideas Отправьте вопросы и идеи @@ -6324,7 +7183,17 @@ Enable in *Network & servers* settings. Send receipts - Отправлять отчёты о доставке + Отчёты о доставке + No comment provided by engineer. + + + Send request + Отправить запрос + No comment provided by engineer. + + + Send request without message + Отправить запрос без сообщения No comment provided by engineer. @@ -6337,6 +7206,11 @@ Enable in *Network & servers* settings. Отправить до 100 последних сообщений новым членам. No comment provided by engineer. + + Send your private feedback to groups. + Отправляйте Ваши конфиденциальные предложения группе. + No comment provided by engineer. + Sender cancelled file transfer. Отправитель отменил передачу файла. @@ -6402,11 +7276,6 @@ Enable in *Network & servers* settings. Отправлено напрямую No comment provided by engineer. - - Sent file event - Отправка файла - notification - Sent message Отправленное сообщение @@ -6477,14 +7346,14 @@ Enable in *Network & servers* settings. Протокол сервера изменен. alert title - - Server requires authorization to create queues, check password - Сервер требует авторизации для создания очередей, проверьте пароль + + Server requires authorization to create queues, check password. + Сервер требует авторизации для создания очередей, проверьте пароль. server test error - - Server requires authorization to upload, check password - Сервер требует авторизации для загрузки, проверьте пароль + + Server requires authorization to upload, check password. + Сервер требует авторизации для загрузки, проверьте пароль. server test error @@ -6532,6 +7401,11 @@ Enable in *Network & servers* settings. Установить 1 день No comment provided by engineer. + + Set chat name… + Имя чата… + No comment provided by engineer. + Set contact name… Имя контакта… @@ -6552,6 +7426,16 @@ Enable in *Network & servers* settings. Установите код вместо системной аутентификации. No comment provided by engineer. + + Set member admission + Приём членов в группу + No comment provided by engineer. + + + Set message expiration in chats. + Установите срок хранения сообщений в чатах. + No comment provided by engineer. + Set passcode Установить код доступа @@ -6567,6 +7451,11 @@ Enable in *Network & servers* settings. Установите пароль No comment provided by engineer. + + Set profile bio and welcome message. + Добавьте описание и приветственное сообщение. + No comment provided by engineer. + Set the message shown to new members! Установить сообщение для новых членов группы! @@ -6596,7 +7485,7 @@ Enable in *Network & servers* settings. Share Поделиться alert action - chat item action +chat item action Share 1-time link @@ -6638,6 +7527,16 @@ Enable in *Network & servers* settings. Поделиться ссылкой No comment provided by engineer. + + Share old address + Поделиться старым адресом + alert button + + + Share old link + Поделиться старой ссылкой + alert button + Share profile Поделиться профилем @@ -6658,6 +7557,26 @@ Enable in *Network & servers* settings. Поделиться с контактами No comment provided by engineer. + + Share your address + Поделитесь Вашим адресом + No comment provided by engineer. + + + Short SimpleX address + Короткий адрес SimpleX + No comment provided by engineer. + + + Short description + Цель + No comment provided by engineer. + + + Short link + Короткая ссылка + No comment provided by engineer. + Show QR code Показать QR код @@ -6758,6 +7677,16 @@ Enable in *Network & servers* settings. Адрес SimpleX или одноразовая ссылка? No comment provided by engineer. + + SimpleX address settings + Настройки автоприема + alert title + + + SimpleX channel link + SimpleX ссылка канала + simplex link type + SimpleX contact address SimpleX ссылка-контакт @@ -6798,6 +7727,11 @@ Enable in *Network & servers* settings. Аудит SimpleX протоколов от Trail of Bits. No comment provided by engineer. + + SimpleX relay link + Ссылка SimpleX relay + simplex link type + Simplified incognito mode Упрощенный режим Инкогнито @@ -6860,6 +7794,12 @@ Enable in *Network & servers* settings. Контакт notification title + + Spam + Спам + blocking reason +report reason + Square, circle, or anything in between. Квадрат, круг и все, что между ними. @@ -6945,6 +7885,11 @@ Enable in *Network & servers* settings. Остановка чата No comment provided by engineer. + + Storage + Хранилище + No comment provided by engineer. + Strong Сильное @@ -7000,11 +7945,21 @@ Enable in *Network & servers* settings. TCP-соединение No comment provided by engineer. + + TCP connection bg timeout + Фоновый таймаут 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 @@ -7030,11 +7985,31 @@ Enable in *Network & servers* settings. Сделать фото No comment provided by engineer. + + Tap Connect to chat + Нажмите Соединиться + No comment provided by engineer. + + + Tap Connect to send request + Нажмите Соединиться, чтобы отправить запрос + No comment provided by engineer. + + + Tap Connect to use bot + Нажмите Соединиться, чтобы использовать бот + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. No comment provided by engineer. + + Tap Join group + Нажмите Вступить в группу + No comment provided by engineer. + Tap button Нажмите кнопку @@ -7073,13 +8048,18 @@ Enable in *Network & servers* settings. Temporary file error Временная ошибка файла - No comment provided by engineer. + file error alert title Test failed at step %@. Ошибка теста на шаге %@. server test failure + + Test notifications + Протестировать уведомления + No comment provided by engineer. + Test server Тестировать сервер @@ -7117,6 +8097,11 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + Адрес будет коротким, и Ваш профиль будет добавлен в адрес. + alert message + The app can notify you when you receive messages or contact requests - please open settings to enable. Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках. @@ -7177,6 +8162,11 @@ It can happen because of some bug or when the connection is compromised.Хэш предыдущего сообщения отличается. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + Ссылка будет короткой, и профиль группы будет добавлен в ссылку. + alert message + The message will be deleted for all members. Сообщение будет удалено для всех членов группы. @@ -7202,21 +8192,11 @@ 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. - Профиль отправляется только Вашим контактам. - No comment provided by engineer. - The same conditions will apply to operator **%@**. Те же самые условия будут приняты для оператора **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - Те же самые условия будут приняты для оператора(ов): **%@**. - No comment provided by engineer. - The second preset operator in the app! Второй оператор серверов в приложении! @@ -7230,7 +8210,7 @@ It can happen because of some bug or when the connection is compromised. The sender will NOT be notified Отправитель не будет уведомлён - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7282,6 +8262,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. Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. @@ -7309,7 +8294,7 @@ 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 членов, отчёты о доставке выключены. + В этой группе более %lld членов, отчёты о доставке не отправляются. No comment provided by engineer. @@ -7317,14 +8302,9 @@ It can happen because of some bug or when the connection is compromised.Эта группа больше не существует. No comment provided by engineer. - - This is your own SimpleX address! - Это ваш собственный адрес SimpleX! - No comment provided by engineer. - - - This is your own one-time 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. @@ -7332,11 +8312,26 @@ It can happen because of some bug or when the connection is compromised.Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере. 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. + + This setting is for your current profile **%@**. + Эта настройка применяется к Вашему текущему профилю чата **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + Время удаления устанавливается только для новых контактов. + No comment provided by engineer. + Title Заголовок @@ -7419,11 +8414,21 @@ You will be prompted to complete authentication before this feature is enabled.< Для оправки No comment provided by engineer. + + To send commands you must be connected. + Вы должны быть соединены, чтобы отправлять команды. + alert message + To support instant push notifications the chat database has to be migrated. Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + Чтобы использовать другой профиль после попытки соединения, удалите чат и используйте ссылку снова. + alert message + To use the servers of **%@**, accept conditions of use. Чтобы использовать серверы оператора **%@**, примите условия использования. @@ -7444,6 +8449,11 @@ You will be prompted to complete authentication before this feature is enabled.< Установите режим Инкогнито при соединении. No comment provided by engineer. + + Token status: %@. + Статус токена: %@. + token status + Toolbar opacity Прозрачность тулбара @@ -7464,15 +8474,9 @@ You will be prompted to complete authentication before this feature is enabled.< Транспортные сессии 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. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7609,13 +8613,18 @@ To connect, please ask your contact to create another connection link and check Unmute Уведомлять - swipe action + 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 последних сообщений отправляются новым членам. @@ -7641,16 +8650,51 @@ To connect, please ask your contact to create another connection link and check Обновить настройки? No comment provided by engineer. + + Updated conditions + Обновленные условия + No comment provided by engineer. + Updating settings will re-connect the client to all servers. Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами. No comment provided by engineer. + + Upgrade + Обновить + alert button + + + Upgrade address + Обновить адрес + No comment provided by engineer. + + + Upgrade address? + Обновить адрес? + alert message + Upgrade and open chat Обновить и открыть чат No comment provided by engineer. + + Upgrade group link? + Обновить ссылку группы? + alert message + + + Upgrade link + Обновить ссылку + No comment provided by engineer. + + + Upgrade your address + Обновите Ваш адрес + No comment provided by engineer. + Upload errors Ошибки загрузки @@ -7701,6 +8745,16 @@ To connect, please ask your contact to create another connection link and check Использовать серверы предосталенные 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. + Использовать TCP-порт 443 только для серверов по умолчанию. + No comment provided by engineer. + Use chat Использовать чат @@ -7709,7 +8763,7 @@ To connect, please ask your contact to create another connection link and check Use current profile Использовать активный профиль - No comment provided by engineer. + new chat action Use for files @@ -7736,10 +8790,15 @@ To connect, please ask your contact to create another connection link and check Использовать интерфейс iOS для звонков No comment provided by engineer. + + Use incognito profile + Использовать профиль инкогнито + No comment provided by engineer. + Use new incognito profile Использовать новый Инкогнито профиль - No comment provided by engineer. + new chat action Use only local notifications? @@ -7776,6 +8835,11 @@ To connect, please ask your contact to create another connection link and check Используйте приложение одной рукой. No comment provided by engineer. + + Use web port + Использовать веб-порт + No comment provided by engineer. + User selection Выбор пользователя @@ -7966,6 +9030,11 @@ To connect, please ask your contact to create another connection link and check Приветственное сообщение слишком длинное No comment provided by engineer. + + Welcome your contacts 👋 + Приветствуйте Ваши контакты 👋 + No comment provided by engineer. + What's new Новые функции @@ -8089,12 +9158,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. Вы уже соединяетесь с %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Вы уже соединяетесь по этой одноразовой ссылке! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8104,35 +9173,33 @@ To connect, please ask your contact to create another connection link and check 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. + new chat sheet message You are already joining the group via this link. Вы уже вступаете в группу по этой ссылке. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Вы уже вступаете в группу! Повторить запрос на вступление? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Установлено соединение с сервером, через который Вы получаете сообщения от этого контакта. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Вы приглашены в группу No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка. @@ -8148,11 +9215,6 @@ Repeat join request? Вы можете изменить это в настройках Интерфейса. No comment provided by engineer. - - You can configure operators in Network & servers settings. - Вы можете настроить операторов в настройках Сети и серверов. - No comment provided by engineer. - You can configure servers via settings. Вы можете настроить серверы позже. @@ -8243,10 +9305,15 @@ Repeat join request? Вы можете увидеть ссылку-приглашение снова открыв соединение. alert message + + You can view your reports in Chat with admins. + Вы можете найти Ваши жалобы в Чате с админами. + alert message + You can't send messages! Вы не можете отправлять сообщения! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8258,17 +9325,12 @@ Repeat join request? Вы определяете, кто может соединиться. No comment provided by engineer. - - You have already requested connection via this address! - Вы уже запросили соединение через этот адрес! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Вы уже запросили соединение! Повторить запрос? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8325,6 +9387,16 @@ Repeat connection request? Вы отправили приглашение в группу No comment provided by engineer. + + You should receive notifications. + Вы должны получать уведомления. + token info + + + You will be able to send messages **only after your request is accepted**. + Вы сможете отправлять сообщения **только после того как Ваш запрос будет принят**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже! @@ -8350,11 +9422,6 @@ Repeat connection request? Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме. No comment provided by engineer. - - You will connect to all group members. - Вы соединитесь со всеми членами группы. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Вы все равно получите звонки и уведомления в профилях без звука, когда они активные. @@ -8390,16 +9457,16 @@ Repeat connection request? Ваши 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 business contact + Ваш бизнес контакт + No comment provided by engineer. + Your calls Ваши звонки @@ -8425,11 +9492,21 @@ Repeat connection request? Ваши профили чата No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Соединение было перемещено в профиль %@, но при переключении профиля произошла ошибка. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. Соединение было перемещено на %@, но при смене профиля произошла неожиданная ошибка. No comment provided by engineer. + + Your contact + Ваш контакт + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Ваш контакт отправил файл, размер которого превышает максимальный размер (%@). @@ -8460,6 +9537,11 @@ Repeat connection request? Ваш активный профиль No comment provided by engineer. + + Your group + Ваша группа + No comment provided by engineer. + Your preferences Ваши предпочтения @@ -8480,6 +9562,11 @@ Repeat connection request? Будет отправлен Ваш профиль **%@**. No comment provided by engineer. + + Your profile is stored on your device and only shared with your contacts. + Ваш профиль хранится на Вашем устройстве и отправляется только контактам. + No comment provided by engineer. + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю. @@ -8490,11 +9577,6 @@ Repeat connection request? Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам. alert message - - Your profile, contacts and delivered messages are stored on your device. - Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве. - No comment provided by engineer. - Your random profile Случайный профиль @@ -8545,6 +9627,11 @@ Repeat connection request? наверху, затем выберите: No comment provided by engineer. + + accepted %@ + принят %@ + rcv group event chat item + accepted call принятый звонок @@ -8555,6 +9642,11 @@ Repeat connection request? принятое приглашение chat list item title + + accepted you + Вы приняты + rcv group event chat item + admin админ @@ -8575,6 +9667,11 @@ Repeat connection request? шифрование согласовывается… chat item text + + all + все + member criteria value + all members все члены @@ -8590,6 +9687,11 @@ Repeat connection request? и %lld других событий No comment provided by engineer. + + archived report + заархивированное сообщение о нарушении + No comment provided by engineer. + attempts попытки @@ -8628,7 +9730,8 @@ Repeat connection request? blocked by admin заблокировано администратором - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8655,6 +9758,11 @@ Repeat connection request? входящий звонок… call status + + can't send messages + нельзя отправлять + No comment provided by engineer. + cancelled %@ отменил(a) %@ @@ -8705,11 +9813,6 @@ Repeat connection request? соединение установлено No comment provided by engineer. - - connected directly - соединен(а) напрямую - rcv group event chat item - connecting соединяется @@ -8760,6 +9863,16 @@ Repeat connection request? контакт %1$@ изменён на %2$@ profile update event chat item + + contact deleted + контакт удален + No comment provided by engineer. + + + contact disabled + контакт выключен + No comment provided by engineer. + contact has e2e encryption у контакта есть e2e шифрование @@ -8770,6 +9883,16 @@ Repeat connection request? у контакта нет e2e шифрования No comment provided by engineer. + + contact not ready + контакт не готов + No comment provided by engineer. + + + contact should accept… + контакт должен принять… + No comment provided by engineer. + creator создатель @@ -8798,7 +9921,8 @@ Repeat connection request? default (%@) по умолчанию (%@) - pref value + delete after time +pref value default (no) @@ -8925,31 +10049,31 @@ Repeat connection request? ошибка No comment provided by engineer. - - event happened - событие произошло - No comment provided by engineer. - expired истекло No comment provided by engineer. - - for better metadata privacy. - для лучшей конфиденциальности метаданных. - No comment provided by engineer. - forwarded переслано No comment provided by engineer. + + group + группа + shown on group welcome message + group deleted группа удалена No comment provided by engineer. + + group is deleted + группа удалена + No comment provided by engineer. + group profile updated профиль группы обновлен @@ -9045,11 +10169,6 @@ Repeat connection request? курсив No comment provided by engineer. - - join as %@ - вступить как %@ - No comment provided by engineer. - left покинул(а) группу @@ -9075,6 +10194,11 @@ Repeat connection request? соединен(а) rcv group event chat item + + member has old version + член имеет старую версию + No comment provided by engineer. + message написать @@ -9105,20 +10229,20 @@ Repeat connection request? удалено %@ marked deleted chat item preview text + + moderator + модератор + member role + months месяцев time unit - - mute - без звука - No comment provided by engineer. - never никогда - No comment provided by engineer. + delete after time new message @@ -9135,11 +10259,20 @@ Repeat connection request? нет e2e шифрования No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text нет текста copied message info in history + + not synchronized + не синхронизирован + No comment provided by engineer. + observer читатель @@ -9149,8 +10282,9 @@ Repeat connection request? off нет enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9192,6 +10326,21 @@ Repeat connection request? peer-to-peer No comment provided by engineer. + + pending + ожидает + No comment provided by engineer. + + + pending approval + ожидает утверждения + No comment provided by engineer. + + + pending review + ожидает одобрения + No comment provided by engineer. + quantum resistant e2e encryption квантово-устойчивое e2e шифрование @@ -9207,6 +10356,11 @@ Repeat connection request? получено подтверждение… No comment provided by engineer. + + rejected + отклонён + No comment provided by engineer. + rejected call отклонённый звонок @@ -9227,6 +10381,11 @@ Repeat connection request? удалён адрес контакта profile update event chat item + + removed from group + удален из группы + No comment provided by engineer. + removed profile picture удалена картинка профиля @@ -9237,11 +10396,41 @@ Repeat connection request? удалил(а) Вас из группы rcv group event chat item + + request is sent + запрос отправлен + No comment provided by engineer. + + + request to join rejected + запрос на вступление отклонён + No comment provided by engineer. + + + requested connection + запрос на соединение + rcv group event chat item + + + requested connection from group %@ + запрос на соединение из группы %@ + rcv direct event chat item + requested to connect запрошено соединение chat list item title + + review + рассмотрение + No comment provided by engineer. + + + reviewed by admins + одобрен админами + No comment provided by engineer. + saved сохранено @@ -9277,11 +10466,6 @@ Repeat connection request? код безопасности изменился chat item text - - send direct message - отправьте сообщение - No comment provided by engineer. - server queue info: %1$@ @@ -9341,11 +10525,6 @@ last received msg: %2$@ неизвестный статус No comment provided by engineer. - - unmute - уведомлять - No comment provided by engineer. - unprotected незащищённый @@ -9436,10 +10615,10 @@ last received msg: %2$@ Вы No comment provided by engineer. - - you are invited to group - Вы приглашены в группу - No comment provided by engineer. + + you accepted this member + Вы приняли этого члена + snd group event chat item you are observer @@ -9510,7 +10689,7 @@ last received msg: %2$@
- +
@@ -9547,7 +10726,7 @@ last received msg: %2$@
- +
@@ -9569,7 +10748,7 @@ last received msg: %2$@
- +
@@ -9577,6 +10756,11 @@ last received msg: %2$@ %d новых сообщений notification body + + From %d chat(s) + Из %d чатов + notification body + From: %@ От: %@ @@ -9592,16 +10776,11 @@ last received msg: %2$@ Новые сообщения notification - - New messages in %d chats - Новые сообщения в %d разговоре(ах) - notification body -
- +
@@ -9623,7 +10802,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json index a28b0ed489..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 4317787f67..ecb4d20fbb 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (สามารถคัดลอกได้) @@ -184,6 +172,10 @@ %d วินาที time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d ข้อความที่ถูกข้าม @@ -248,11 +240,6 @@ %lld new interface languages No comment provided by engineer. - - %lld second(s) - %lld วินาที - No comment provided by engineer. - %lld seconds %lld วินาที @@ -303,11 +290,6 @@ %u ข้อความที่ถูกข้าม No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) No comment provided by engineer. @@ -316,11 +298,6 @@ (this device v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. @@ -381,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). @@ -416,11 +388,6 @@ - ประวัติการแก้ไข No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec time to disappear @@ -433,7 +400,8 @@ 1 day 1 วัน - time interval + delete after time +time interval 1 hour @@ -448,12 +416,18 @@ 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 @@ -478,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> @@ -548,8 +517,17 @@ Accept รับ accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + alert action + + + Accept as observer + alert action Accept conditions @@ -559,6 +537,10 @@ Accept connection request? No comment provided by engineer. + + Accept contact request + alert title + Accept contact request from %@? รับการขอติดต่อจาก %@? @@ -567,8 +549,12 @@ Accept incognito ยอมรับโหมดไม่ระบุตัวตน - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + alert title Accepted conditions @@ -582,6 +568,10 @@ Acknowledgement errors No comment provided by engineer. + + Active + token status text + Active connections No comment provided by engineer. @@ -595,6 +585,14 @@ Add friends No comment provided by engineer. + + Add list + No comment provided by engineer. + + + Add message + placeholder for sending contact request + Add profile เพิ่มโปรไฟล์ @@ -619,6 +617,10 @@ เพิ่มเข้าไปในอุปกรณ์อื่น No comment provided by engineer. + + Add to list + No comment provided by engineer. + Add welcome message เพิ่มข้อความต้อนรับ @@ -684,6 +686,10 @@ Advanced settings No comment provided by engineer. + + All + No comment provided by engineer. + All app data is deleted. ข้อมูลแอปทั้งหมดถูกลบแล้ว. @@ -694,6 +700,10 @@ แชทและข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้! 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. ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน @@ -729,6 +739,14 @@ 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. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. @@ -766,6 +784,10 @@ Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น @@ -800,6 +822,10 @@ อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร 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. @@ -844,6 +870,10 @@ อนุญาตให้ผู้ติดต่อของคุณส่งข้อความที่จะหายไปหลังปิดแชท (disappearing messages) No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. อนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียง @@ -856,11 +886,11 @@ Already connecting! - No comment provided by engineer. + new chat sheet title Already joining the group! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -876,6 +906,10 @@ โปรไฟล์แชทที่ว่างเปล่าพร้อมชื่อที่ให้ไว้ได้ถูกสร้างขึ้นและแอปจะเปิดตามปกติ No comment provided by engineer. + + Another reason + report reason + Answer call รับสาย @@ -899,6 +933,10 @@ App encrypts new local files (except videos). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon ไอคอนแอป @@ -941,6 +979,18 @@ 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. @@ -949,6 +999,18 @@ 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. @@ -1017,10 +1079,6 @@ ยอมรับภาพอัตโนมัติ No comment provided by engineer. - - Auto-accept settings - alert title - Back กลับ @@ -1052,6 +1110,10 @@ Better groups No comment provided by engineer. + + Better groups performance + No comment provided by engineer. + Better message dates. No comment provided by engineer. @@ -1069,6 +1131,10 @@ Better notifications No comment provided by engineer. + + Better privacy and security + No comment provided by engineer. + Better security ✅ No comment provided by engineer. @@ -1077,6 +1143,14 @@ Better user experience No comment provided by engineer. + + Bio + No comment provided by engineer. + + + Bio too large + alert title + Black No comment provided by engineer. @@ -1117,6 +1191,10 @@ Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้ @@ -1137,6 +1215,10 @@ ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความที่หายไปได้ No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้ @@ -1154,11 +1236,25 @@ Business chats No comment provided by engineer. + + Business connection + 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! สิ้นสุดการโทรแล้ว! @@ -1185,6 +1281,10 @@ Can't call member No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! ไม่สามารถเชิญผู้ติดต่อได้! @@ -1203,7 +1303,8 @@ Cancel ยกเลิก alert action - alert button +alert button +new chat action Cancel migration @@ -1236,6 +1337,10 @@ เปลี่ยน No comment provided by engineer. + + Change automatic message deletion? + alert title + Change chat profiles authentication reason @@ -1284,7 +1389,7 @@ Change self-destruct passcode เปลี่ยนรหัสผ่านแบบทำลายตัวเอง authentication reason - set passcode view +set passcode view Chat @@ -1296,7 +1401,7 @@ Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1374,11 +1479,27 @@ Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + + + Chat with members before they join. + No comment provided by engineer. + Chats แชท No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1438,6 +1559,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. @@ -1455,6 +1584,10 @@ Color mode No comment provided by engineer. + + Community guidelines violation + report reason + Compare file เปรียบเทียบไฟล์ @@ -1483,15 +1616,7 @@ Conditions of use - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1510,6 +1635,10 @@ กำหนดค่าเซิร์ฟเวอร์ ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm ยืนยัน @@ -1555,6 +1684,10 @@ Confirm upload No comment provided by engineer. + + Confirmed + token status text + Connect เชื่อมต่อ @@ -1564,8 +1697,8 @@ Connect automatically No comment provided by engineer. - - Connect incognito + + Connect faster! 🚀 No comment provided by engineer. @@ -1576,36 +1709,32 @@ Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link เชื่อมต่อผ่านลิงก์ - No comment provided by engineer. + new chat sheet title Connect via one-time link - No comment provided by engineer. + new chat sheet title Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1654,16 +1783,29 @@ This is your own one-time link! Connection and servers status. No comment provided by engineer. + + Connection blocked + No comment provided by engineer. + Connection error การเชื่อมต่อผิดพลาด - No comment provided by engineer. + alert title Connection error (AUTH) การเชื่อมต่อผิดพลาด (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. @@ -1673,6 +1815,10 @@ This is your own one-time link! ส่งคําขอเชื่อมต่อแล้ว! No comment provided by engineer. + + Connection requires encryption renegotiation. + No comment provided by engineer. + Connection security No comment provided by engineer. @@ -1684,7 +1830,7 @@ This is your own one-time link! Connection timeout หมดเวลาการเชื่อมต่อ - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1732,6 +1878,10 @@ This is your own one-time link! การกําหนดลักษณะการติดต่อ No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -1746,6 +1896,10 @@ This is your own one-time link! ผู้ติดต่อสามารถทําเครื่องหมายข้อความเพื่อลบได้ คุณจะสามารถดูได้ No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue ดำเนินการต่อ @@ -1814,6 +1968,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. @@ -1827,9 +1985,8 @@ This is your own one-time link! สร้างคิว server test step - - Create secret group - สร้างกลุ่มลับ + + Create your address No comment provided by engineer. @@ -2018,8 +2175,7 @@ This is your own one-time link! Delete ลบ alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2057,6 +2213,10 @@ This is your own one-time link! Delete chat No comment provided by engineer. + + Delete chat messages from your device. + No comment provided by engineer. + Delete chat profile ลบโปรไฟล์แชท @@ -2067,6 +2227,10 @@ This is your own one-time link! ลบโปรไฟล์แชทไหม? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2144,6 +2308,10 @@ This is your own one-time link! ลบลิงค์ ไหม? No comment provided by engineer. + + Delete list? + alert title + Delete member message? ลบข้อความสมาชิก? @@ -2157,7 +2325,7 @@ This is your own one-time link! Delete messages ลบข้อความ - No comment provided by engineer. + alert button Delete messages after @@ -2193,6 +2361,10 @@ This is your own one-time link! ลบคิว server test step + + Delete report + No comment provided by engineer. + Delete up to 20 messages at once. No comment provided by engineer. @@ -2242,11 +2414,19 @@ This is your own one-time link! ใบตอบรับการจัดส่ง! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description คำอธิบาย No comment provided by engineer. + + Description too large + alert title + Desktop address No comment provided by engineer. @@ -2337,6 +2517,14 @@ This is your own one-time link! ปิดการใช้งาน SimpleX Lock authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all ปิดการใช้งานสำหรับทุกคน @@ -2419,6 +2607,10 @@ This is your own one-time link! Do not use credentials with proxy. No comment provided by engineer. + + Documents: + No comment provided by engineer. + Don't create address อย่าสร้างที่อยู่ @@ -2429,9 +2621,17 @@ This is your own one-time link! อย่าเปิดใช้งาน No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again ไม่ต้องแสดงอีก + alert action + + + Done No comment provided by engineer. @@ -2442,7 +2642,7 @@ This is your own one-time link! Download alert button - chat item action +chat item action Download errors @@ -2501,6 +2701,10 @@ This is your own one-time link! แก้ไขโปรไฟล์กลุ่ม No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable เปิดใช้งาน @@ -2511,8 +2715,8 @@ This is your own one-time link! เปิดใช้งาน (เก็บการแทนที่) No comment provided by engineer. - - Enable Flux + + Enable Flux in Network & servers settings for better metadata privacy. No comment provided by engineer. @@ -2528,12 +2732,16 @@ This is your own one-time link! Enable automatic message deletion? เปิดใช้งานการลบข้อความอัตโนมัติ? - No comment provided by engineer. + alert title Enable camera access No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all เปิดใช้งานสําหรับทุกคน @@ -2646,6 +2854,10 @@ This is your own one-time link! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode ใส่รหัสผ่าน @@ -2716,6 +2928,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการรับคำขอติดต่อ No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) เกิดข้อผิดพลาดในการเพิ่มสมาชิก @@ -2725,11 +2941,19 @@ This is your own one-time link! Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่ No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile No comment provided by engineer. @@ -2742,15 +2966,23 @@ This is your own one-time link! Error changing setting เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2767,6 +2999,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการสร้างลิงก์กลุ่ม No comment provided by engineer. + + Error creating list + alert title + Error creating member contact No comment provided by engineer. @@ -2780,19 +3016,27 @@ This is your own one-time link! เกิดข้อผิดพลาดในการสร้างโปรไฟล์! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท - No comment provided by engineer. + alert title Error deleting chat! เกิดข้อผิดพลาดในการลบแชท! - No comment provided by engineer. + alert title Error deleting connection @@ -2802,12 +3046,12 @@ This is your own one-time link! Error deleting database เกิดข้อผิดพลาดในการลบฐานข้อมูล - No comment provided by engineer. + alert title Error deleting old database เกิดข้อผิดพลาดในการลบฐานข้อมูลเก่า - No comment provided by engineer. + alert title Error deleting token @@ -2841,7 +3085,7 @@ This is your own one-time link! Error exporting chat database เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -2850,7 +3094,7 @@ This is your own one-time link! Error importing chat database เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท - No comment provided by engineer. + alert title Error joining group @@ -2869,6 +3113,10 @@ This is your own one-time link! Error opening chat No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file เกิดข้อผิดพลาดในการรับไฟล์ @@ -2882,10 +3130,22 @@ This is your own one-time link! Error reconnecting servers No comment provided by engineer. + + Error registering for notifications + alert title + + + Error rejecting contact request + alert title + Error removing member เกิดข้อผิดพลาดในการลบสมาชิก - No comment provided by engineer. + alert title + + + Error reordering lists + alert title Error resetting statistics @@ -2896,6 +3156,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม @@ -2942,6 +3206,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการส่งข้อความ No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! เกิดข้อผิดพลาดในการตั้งค่าใบตอบรับการจัดส่ง! @@ -2959,7 +3227,7 @@ This is your own one-time link! Error switching profile - No comment provided by engineer. + alert title Error switching profile! @@ -2971,6 +3239,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการซิงโครไนซ์การเชื่อมต่อ No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link เกิดข้อผิดพลาดในการอัปเดตลิงก์กลุ่ม @@ -3011,7 +3283,13 @@ This is your own one-time link! Error: %@ ข้อผิดพลาด: % @ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3045,6 +3323,10 @@ This is your own one-time link! Expand chat item action + + Expired + token status text + Export database ส่งออกฐานข้อมูล @@ -3083,24 +3365,41 @@ This is your own one-time link! รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์! 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 - No comment provided by engineer. + 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 @@ -3151,6 +3450,10 @@ This is your own one-time link! ไฟล์และสื่อ chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้ @@ -3188,6 +3491,23 @@ This is your own one-time link! ค้นหาแชทได้เร็วขึ้น No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix แก้ไข @@ -3218,6 +3538,10 @@ This is your own one-time link! การแก้ไขไม่สนับสนุนโดยสมาชิกกลุ่ม No comment provided by engineer. + + For all moderators + No comment provided by engineer. + For chat profile %@: servers error @@ -3231,6 +3555,10 @@ This is your own one-time link! 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. @@ -3276,8 +3604,8 @@ This is your own one-time link! No comment provided by engineer. - Forwarding server %@ failed to connect to destination server %@. Please try later. - No comment provided by engineer. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3335,6 +3663,10 @@ Error: %2$@ GIFs และสติกเกอร์ No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + Good afternoon! message preview @@ -3354,7 +3686,7 @@ Error: %2$@ Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3421,6 +3753,10 @@ Error: %2$@ โปรไฟล์กลุ่มถูกจัดเก็บไว้ในอุปกรณ์ของสมาชิก ไม่ใช่บนเซิร์ฟเวอร์ No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message ข้อความต้อนรับกลุ่ม @@ -3436,11 +3772,19 @@ Error: %2$@ กลุ่มจะถูกลบสำหรับคุณ - ไม่สามารถยกเลิกได้! 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 ซ่อนอยู่ @@ -3498,6 +3842,10 @@ Error: %2$@ How it helps privacy No comment provided by engineer. + + How it works + alert button + How to วิธี @@ -3630,6 +3978,14 @@ More improvements are coming soon! In-call sounds No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito ไม่ระบุตัวตน @@ -3718,6 +4074,26 @@ More improvements are coming soon! 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. @@ -3733,7 +4109,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -3840,32 +4216,29 @@ More improvements are coming soon! เข้าร่วม swipe action + + Join as %@ + เข้าร่วมเป็น %@ + No comment provided by engineer. + Join group เข้าร่วมกลุ่ม - No comment provided by engineer. + new chat sheet title Join group conversations No comment provided by engineer. - - Join group? - No comment provided by engineer. - Join incognito เข้าร่วมแบบไม่ระบุตัวตน No comment provided by engineer. - - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -3888,6 +4261,10 @@ This is your link for group %@! Keep unused invitation? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections รักษาการเชื่อมต่อของคุณ @@ -3941,6 +4318,10 @@ This is your link for group %@! ออกจากกลุ่ม? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat มาคุยกันใน SimpleX Chat @@ -3968,6 +4349,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! ข้อความสด! @@ -3978,6 +4371,10 @@ This is your link for group %@! ข้อความสด No comment provided by engineer. + + Loading profile… + in progress text + Local name ชื่อภายในเครื่องเท่านั้น @@ -4051,10 +4448,26 @@ This is your link for group %@! สมาชิก No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + No comment provided by engineer. + Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + Member reports + chat feature + Member role will be changed to "%@". All chat members will be notified. No comment provided by engineer. @@ -4078,6 +4491,10 @@ This is your link for group %@! สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ @@ -4088,6 +4505,10 @@ This is your link for group %@! สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร 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. @@ -4112,6 +4533,10 @@ This is your link for group %@! สมาชิกกลุ่มสามารถส่งข้อความเสียง No comment provided by engineer. + + Mention members 👋 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4139,6 +4564,10 @@ This is your link for group %@! Message forwarded item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. item status description @@ -4205,10 +4634,18 @@ This is your link for group %@! ข้อความและไฟล์ No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + Messages received No comment provided by engineer. @@ -4297,6 +4734,10 @@ This is your link for group %@! กลั่นกรองที่: %@ copied message info + + More + swipe action + More improvements are coming soon! การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้! @@ -4322,7 +4763,11 @@ This is your link for group %@! Mute ปิดเสียง - swipe action + notification label action + + + Mute all + notification label action Muted when inactive! @@ -4367,7 +4812,11 @@ This is your link for group %@! Network status สถานะเครือข่าย - No comment provided by engineer. + alert title + + + New + token status text New Passcode @@ -4413,6 +4862,10 @@ This is your link for group %@! New events notification + + New group role: Moderator + No comment provided by engineer. + New in %@ ใหม่ใน %@ @@ -4427,6 +4880,10 @@ This is your link for group %@! บทบาทของสมาชิกใหม่ No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message ข้อความใหม่ @@ -4451,6 +4908,22 @@ 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 chats with members + No comment provided by engineer. + No contacts selected ไม่ได้เลือกผู้ติดต่อ @@ -4497,6 +4970,10 @@ This is your link for group %@! No media & file servers. servers error + + No message + No comment provided by engineer. + No message servers. servers error @@ -4518,6 +4995,10 @@ This is your link for group %@! ไม่อนุญาตให้บันทึกข้อความเสียง No comment provided by engineer. + + No private routing session + alert title + No push server ในเครื่อง @@ -4544,6 +5025,14 @@ This is your link for group %@! No servers to send files. servers error + + No token! + alert title + + + No unread chats + No comment provided by engineer. + No user identifiers. แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว @@ -4553,6 +5042,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Notes + No comment provided by engineer. + Nothing selected No comment provided by engineer. @@ -4571,10 +5064,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. @@ -4596,7 +5097,9 @@ This is your link for group %@! Ok ตกลง - alert button + alert action +alert button +new chat action Old database @@ -4653,6 +5156,14 @@ Requires compatible VPN. เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานข้อความเสียงได้ 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. มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้ @@ -4673,6 +5184,10 @@ Requires compatible VPN. มีเพียงคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. มีเพียงคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ @@ -4698,6 +5213,10 @@ Requires compatible VPN. เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ @@ -4705,7 +5224,7 @@ Requires compatible VPN. Open - No comment provided by engineer. + alert action Open Settings @@ -4719,25 +5238,61 @@ Requires compatible VPN. Open chat เปิดแชท - No comment provided by engineer. + new chat action Open chat console เปิดคอนโซลการแชท authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group - No comment provided by engineer. + new chat action + + + Open link? + alert title Open migration to another device authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -4774,6 +5329,10 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Organize chats into lists + No comment provided by engineer. + Other No comment provided by engineer. @@ -4827,10 +5386,6 @@ Requires compatible VPN. รหัสผ่านที่จะแสดง No comment provided by engineer. - - Past member %@ - past/unknown group member - Paste desktop address No comment provided by engineer. @@ -4892,7 +5447,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. โปรดตรวจสอบการเชื่อมต่อเครือข่ายของคุณกับ %@ แล้วลองอีกครั้ง - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -4948,6 +5503,22 @@ Error: %@ โปรดจัดเก็บรหัสผ่านอย่างปลอดภัย คุณจะไม่สามารถเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย No comment provided by engineer. + + Please try to disable and re-enable notfications. + token info + + + Please wait for group moderators to review your request to join the group. + snd group event chat item + + + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + token info + Polish interface อินเตอร์เฟซภาษาโปแลนด์ @@ -4957,11 +5528,6 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง - server test error - Preserve the last message draft, with attachments. เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ @@ -4994,16 +5560,28 @@ Error: %@ 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. @@ -5022,7 +5600,11 @@ Error: %@ Private routing error - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5072,6 +5654,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. @@ -5115,6 +5701,10 @@ Enable in *Network & servers* settings. ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout หมดเวลาโปรโตคอล @@ -5211,11 +5801,6 @@ Enable in *Network & servers* settings. ได้รับเมื่อ: %@ copied message info - - Received file event - ได้รับไฟล์ - notification - Received message ได้รับข้อความ @@ -5306,11 +5891,24 @@ Enable in *Network & servers* settings. ลดการใช้แบตเตอรี่ 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5319,7 +5917,11 @@ Enable in *Network & servers* settings. Reject contact request ปฏิเสธคำขอติดต่อ - No comment provided by engineer. + alert title + + + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5344,6 +5946,10 @@ Enable in *Network & servers* settings. Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member ลบสมาชิกออก @@ -5359,6 +5965,10 @@ Enable in *Network & servers* settings. ลบรหัสผ่านออกจาก keychain หรือไม่? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate เจรจาใหม่ @@ -5374,10 +5984,6 @@ Enable in *Network & servers* settings. เจรจา enryption ใหม่หรือไม่? No comment provided by engineer. - - Repeat connection request? - No comment provided by engineer. - Repeat download No comment provided by engineer. @@ -5386,10 +5992,6 @@ Enable in *Network & servers* settings. Repeat import No comment provided by engineer. - - Repeat join request? - No comment provided by engineer. - Repeat upload No comment provided by engineer. @@ -5399,6 +6001,50 @@ Enable in *Network & servers* settings. ตอบ 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 sent to moderators + alert title + + + 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 ที่จำเป็น @@ -5471,7 +6117,7 @@ Enable in *Network & servers* settings. Retry - No comment provided by engineer. + alert action Reveal @@ -5482,10 +6128,18 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later + + Review group members No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke ถอน @@ -5531,13 +6185,21 @@ Enable in *Network & servers* settings. Save บันทึก alert button - chat item action +chat item action Save (and notify contacts) บันทึก (และแจ้งผู้ติดต่อ) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact บันทึกและแจ้งผู้ติดต่อ @@ -5562,6 +6224,14 @@ Enable in *Network & servers* settings. บันทึกโปรไฟล์กลุ่ม No comment provided by engineer. + + Save group profile? + alert title + + + Save list + No comment provided by engineer. + Save passphrase and open chat บันทึกรหัสผ่านและเปิดแชท @@ -5737,6 +6407,10 @@ Enable in *Network & servers* settings. ส่งข้อความสด - มันจะอัปเดตสําหรับผู้รับในขณะที่คุณพิมพ์ No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to ส่งใบเสร็จรับการจัดส่งข้อความไปที่ @@ -5782,6 +6456,10 @@ Enable in *Network & servers* settings. ส่งการแจ้งเตือน No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas ส่งคําถามและความคิด @@ -5792,6 +6470,14 @@ Enable in *Network & servers* settings. ส่งใบเสร็จ No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. ส่งจากแกลเลอรีหรือแป้นพิมพ์แบบกำหนดเอง @@ -5801,6 +6487,10 @@ Enable in *Network & servers* settings. Send up to 100 last messages to new members. No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. ผู้ส่งยกเลิกการโอนไฟล์ @@ -5863,11 +6553,6 @@ Enable in *Network & servers* settings. Sent directly No comment provided by engineer. - - Sent file event - เหตุการณ์ไฟล์ที่ส่ง - notification - Sent message ข้อความที่ส่งแล้ว @@ -5926,13 +6611,13 @@ Enable in *Network & servers* settings. Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน server test error @@ -5975,6 +6660,10 @@ Enable in *Network & servers* settings. ตั้ง 1 วัน No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… ตั้งชื่อผู้ติดต่อ… @@ -5994,6 +6683,14 @@ Enable in *Network & servers* settings. ตั้งแทนการรับรองความถูกต้องของระบบ No comment provided by engineer. + + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + No comment provided by engineer. + Set passcode ตั้งรหัสผ่าน @@ -6008,6 +6705,10 @@ Enable in *Network & servers* settings. ตั้งรหัสผ่านเพื่อส่งออก No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! ตั้งข้อความที่แสดงต่อสมาชิกใหม่! @@ -6035,7 +6736,7 @@ Enable in *Network & servers* settings. Share แชร์ alert action - chat item action +chat item action Share 1-time link @@ -6073,6 +6774,14 @@ Enable in *Network & servers* settings. แชร์ลิงก์ No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile No comment provided by engineer. @@ -6090,6 +6799,22 @@ Enable in *Network & servers* settings. แชร์กับผู้ติดต่อ No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -6181,6 +6906,14 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX address settings + alert title + + + SimpleX channel link + simplex link type + SimpleX contact address ที่อยู่ติดต่อ SimpleX @@ -6218,6 +6951,10 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode No comment provided by engineer. @@ -6271,6 +7008,11 @@ Enable in *Network & servers* settings. ใครบางคน notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. No comment provided by engineer. @@ -6350,6 +7092,10 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Storage + No comment provided by engineer. + Strong blur media @@ -6398,11 +7144,19 @@ Enable in *Network & servers* settings. TCP connection No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -6427,10 +7181,26 @@ Enable in *Network & servers* settings. ถ่ายภาพ No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join group + No comment provided by engineer. + Tap button แตะปุ่ม @@ -6465,13 +7235,17 @@ Enable in *Network & servers* settings. Temporary file error - No comment provided by engineer. + file error alert title Test failed at step %@. การทดสอบล้มเหลวในขั้นตอน %@ server test failure + + Test notifications + No comment provided by engineer. + Test server เซิร์ฟเวอร์ทดสอบ @@ -6510,6 +7284,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + The app can notify you when you receive messages or contact requests - please open settings to enable. แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน @@ -6566,6 +7344,10 @@ It can happen because of some bug or when the connection is compromised.แฮชของข้อความก่อนหน้านี้แตกต่างกัน No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. ข้อความจะถูกลบสำหรับสมาชิกทั้งหมด @@ -6589,19 +7371,10 @@ 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. - โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น - No comment provided by engineer. - The same conditions will apply to operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! No comment provided by engineer. @@ -6614,7 +7387,7 @@ It can happen because of some bug or when the connection is compromised. The sender will NOT be notified ผู้ส่งจะไม่ได้รับแจ้ง - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -6660,6 +7433,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. การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร @@ -6690,23 +7467,31 @@ It can happen because of some bug or when the connection is compromised.ไม่มีกลุ่มนี้แล้ว No comment provided by engineer. - - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time 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. 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title No comment provided by engineer. @@ -6781,11 +7566,19 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. @@ -6803,6 +7596,10 @@ You will be prompted to complete authentication before this feature is enabled.< Toggle incognito when connecting. No comment provided by engineer. + + Token status: %@. + token status + Toolbar opacity No comment provided by engineer. @@ -6820,15 +7617,9 @@ You will be prompted to complete authentication before this feature is enabled.< 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. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -6955,13 +7746,17 @@ To connect, please ask your contact to create another connection link and check Unmute เปิดเสียง - swipe action + notification label action Unread เปลี่ยนเป็นยังไม่ได้อ่าน swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. No comment provided by engineer. @@ -6985,16 +7780,44 @@ To connect, please ask your contact to create another connection link and check Update settings? No comment provided by engineer. + + Updated conditions + No comment provided by engineer. + Updating settings will re-connect the client to all servers. การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง No comment provided by engineer. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat อัปเกรดและเปิดการแชท No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors No comment provided by engineer. @@ -7038,6 +7861,14 @@ To connect, please ask your contact to create another connection link and check ใช้เซิร์ฟเวอร์ 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 ใช้แชท @@ -7045,7 +7876,7 @@ To connect, please ask your contact to create another connection link and check Use current profile - No comment provided by engineer. + new chat action Use for files @@ -7069,9 +7900,13 @@ To connect, please ask your contact to create another connection link and check ใช้อินเทอร์เฟซการโทร iOS No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile - No comment provided by engineer. + new chat action Use only local notifications? @@ -7102,6 +7937,10 @@ To connect, please ask your contact to create another connection link and check Use the app with one hand. No comment provided by engineer. + + Use web port + No comment provided by engineer. + User selection No comment provided by engineer. @@ -7275,6 +8114,10 @@ To connect, please ask your contact to create another connection link and check Welcome message is too long No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new มีอะไรใหม่ @@ -7383,11 +8226,11 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7395,31 +8238,30 @@ To connect, please ask your contact to create another connection link and check 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. + new chat sheet message You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group คุณได้รับเชิญให้เข้าร่วมกลุ่ม No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. No comment provided by engineer. @@ -7433,10 +8275,6 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. - - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. No comment provided by engineer. @@ -7520,10 +8358,14 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! คุณไม่สามารถส่งข้อความได้! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -7535,14 +8377,10 @@ Repeat join request? ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น No comment provided by engineer. - - You have already requested connection via this address! - No comment provided by engineer. - You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -7595,6 +8433,14 @@ Repeat connection request? คุณส่งคำเชิญเข้าร่วมกลุ่มแล้ว No comment provided by engineer. + + You should receive notifications. + token info + + + You will be able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! คุณจะเชื่อมต่อกับกลุ่มเมื่ออุปกรณ์โฮสต์ของกลุ่มออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง! @@ -7619,10 +8465,6 @@ Repeat connection request? คุณจะต้องตรวจสอบสิทธิ์เมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน @@ -7657,16 +8499,15 @@ Repeat connection request? เซิร์ฟเวอร์ 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 business contact + No comment provided by engineer. + Your calls การโทรของคุณ @@ -7691,8 +8532,16 @@ Repeat connection request? โปรไฟล์แชทของคุณ No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. + No comment provided by engineer. + + + Your contact No comment provided by engineer. @@ -7724,6 +8573,10 @@ Repeat connection request? โปรไฟล์ปัจจุบันของคุณ No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences การตั้งค่าของคุณ @@ -7742,6 +8595,11 @@ Repeat connection request? Your profile **%@** will be shared. No comment provided by engineer. + + Your profile is stored on your device and only shared with your contacts. + โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น + No comment provided by engineer. + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้ @@ -7751,11 +8609,6 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message - - Your profile, contacts and delivered messages are stored on your device. - โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ - No comment provided by engineer. - Your random profile โปรไฟล์แบบสุ่มของคุณ @@ -7805,6 +8658,10 @@ Repeat connection request? ด้านบน จากนั้นเลือก: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call รับสายแล้ว @@ -7814,6 +8671,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin ผู้ดูแลระบบ @@ -7833,6 +8694,10 @@ Repeat connection request? เห็นด้วยกับการ encryption… chat item text + + all + member criteria value + all members feature role @@ -7846,6 +8711,10 @@ Repeat connection request? and %lld other events No comment provided by engineer. + + archived report + No comment provided by engineer. + attempts No comment provided by engineer. @@ -7879,7 +8748,8 @@ Repeat connection request? blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -7905,6 +8775,10 @@ Repeat connection request? กำลังโทร… call status + + can't send messages + No comment provided by engineer. + cancelled %@ ยกเลิก %@ @@ -7955,10 +8829,6 @@ Repeat connection request? เชื่อมต่อสำเร็จ No comment provided by engineer. - - connected directly - rcv group event chat item - connecting กำลังเชื่อมต่อ @@ -8008,6 +8878,14 @@ Repeat connection request? contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption ผู้ติดต่อมีการ encrypt จากต้นจนจบ @@ -8018,6 +8896,14 @@ Repeat connection request? ผู้ติดต่อไม่มีการ encrypt จากต้นจนจบ No comment provided by engineer. + + contact not ready + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator ผู้สร้าง @@ -8045,7 +8931,8 @@ Repeat connection request? default (%@) ค่าเริ่มต้น (%@) - pref value + delete after time +pref value default (no) @@ -8169,27 +9056,27 @@ Repeat connection request? ผิดพลาด No comment provided by engineer. - - event happened - No comment provided by engineer. - expired No comment provided by engineer. - - for better metadata privacy. - No comment provided by engineer. - forwarded No comment provided by engineer. + + group + shown on group welcome message + group deleted ลบกลุ่มแล้ว No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated อัปเดตโปรไฟล์กลุ่มแล้ว @@ -8283,11 +9170,6 @@ Repeat connection request? ตัวเอียง No comment provided by engineer. - - join as %@ - เข้าร่วมเป็น %@ - No comment provided by engineer. - left ออกแล้ว @@ -8312,6 +9194,10 @@ Repeat connection request? เชื่อมต่อสำเร็จ rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8341,19 +9227,19 @@ Repeat connection request? กลั่นกรองโดย %@ marked deleted chat item preview text + + moderator + member role + months เดือน time unit - - mute - No comment provided by engineer. - never ไม่เคย - No comment provided by engineer. + delete after time new message @@ -8370,11 +9256,19 @@ Repeat connection request? ไม่มีการ encrypt จากต้นจนจบ No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text ไม่มีข้อความ copied message info in history + + not synchronized + No comment provided by engineer. + observer ผู้สังเกตการณ์ @@ -8384,8 +9278,9 @@ Repeat connection request? off ปิด enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -8424,6 +9319,18 @@ Repeat connection request? เพื่อนต่อเพื่อน No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -8438,6 +9345,10 @@ Repeat connection request? ได้รับการยืนยัน… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call สายถูกปฏิเสธ @@ -8457,6 +9368,10 @@ Repeat connection request? removed contact address profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profile update event chat item @@ -8466,10 +9381,34 @@ Repeat connection request? ลบคุณออกแล้ว rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved No comment provided by engineer. @@ -8502,10 +9441,6 @@ Repeat connection request? เปลี่ยนรหัสความปลอดภัยแล้ว chat item text - - send direct message - No comment provided by engineer. - server queue info: %1$@ @@ -8556,10 +9491,6 @@ last received msg: %2$@ unknown status No comment provided by engineer. - - unmute - No comment provided by engineer. - unprotected No comment provided by engineer. @@ -8644,10 +9575,9 @@ last received msg: %2$@ you No comment provided by engineer. - - you are invited to group - คุณได้รับเชิญให้เข้าร่วมกลุ่ม - No comment provided by engineer. + + you accepted this member + snd group event chat item you are observer @@ -8716,7 +9646,7 @@ last received msg: %2$@
- +
@@ -8752,7 +9682,7 @@ last received msg: %2$@
- +
@@ -8774,13 +9704,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -8793,15 +9727,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -8820,7 +9750,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/th.xcloc/contents.json b/apps/ios/SimpleX Localizations/th.xcloc/contents.json index 4562ab8385..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 261752aefc..57151a95b5 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (kopyalanabilir) @@ -114,10 +102,12 @@ %@ server + %@ sunucu No comment provided by engineer. %@ servers + %@ sunucular No comment provided by engineer. @@ -200,6 +190,11 @@ %d saniye time interval + + %d seconds(s) + %d saniye(ler) + delete after time + %d skipped message(s) %d okunmamış mesaj(lar) @@ -270,11 +265,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 @@ -325,11 +315,6 @@ %u mesajlar atlandı. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (yeni) @@ -340,11 +325,6 @@ (bu cihaz v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Kişi ekle**: yeni bir davet bağlantısı oluşturmak için, ya da aldığın bağlantıyla bağlan. @@ -382,6 +362,7 @@ **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. @@ -409,11 +390,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). @@ -450,11 +426,6 @@ - düzenleme geçmişi. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 saniye @@ -468,7 +439,8 @@ 1 day 1 gün - time interval + delete after time +time interval 1 hour @@ -483,19 +455,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 + 1 yıl + 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. @@ -513,11 +494,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> @@ -574,6 +550,7 @@ About operators + Operatörler hakkında No comment provided by engineer. @@ -585,11 +562,23 @@ Accept Kabul et accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Üye olarak kabul et + alert action + + + Accept as observer + Gözlemci olarak kabul et + alert action Accept conditions + Koşulları kabul et No comment provided by engineer. @@ -597,6 +586,11 @@ Bağlantı isteği kabul edilsin mi? No comment provided by engineer. + + Accept contact request + Kişi isteğini kabul et + alert title + Accept contact request from %@? %@ 'den gelen iletişim isteği kabul edilsin mi? @@ -605,16 +599,22 @@ Accept incognito Takma adla kabul et - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Üyeyi kabul et + alert title Accepted conditions + Kabul edilmiş koşullar No comment provided by engineer. Acknowledged - Onaylandı + Onaylı No comment provided by engineer. @@ -622,6 +622,11 @@ Onay hataları No comment provided by engineer. + + Active + Aktif + token status text + Active connections Aktif bağlantılar @@ -634,8 +639,19 @@ Add friends + Arkadaş ekle No comment provided by engineer. + + Add list + Liste ekle + No comment provided by engineer. + + + Add message + Mesaj ekle + placeholder for sending contact request + Add profile Profil ekle @@ -653,6 +669,7 @@ Add team members + Takım üyesi ekle No comment provided by engineer. @@ -660,6 +677,11 @@ Başka bir cihaza ekle No comment provided by engineer. + + Add to list + Listeye ekle + No comment provided by engineer. + Add welcome message Karşılama mesajı ekleyin @@ -667,14 +689,17 @@ 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. @@ -704,10 +729,12 @@ 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. @@ -730,6 +757,11 @@ Gelişmiş ayarlar No comment provided by engineer. + + All + Hepsi + No comment provided by engineer. + All app data is deleted. Tüm uygulama verileri silinir. @@ -740,6 +772,11 @@ 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. + Tüm sohbetler %@ listesinden kaldırılacak ve liste silinecek. + alert message + All data is erased when it is entered. Kullanıldığında bütün veriler silinir. @@ -757,6 +794,7 @@ 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. @@ -779,6 +817,16 @@ Tüm Profiller profile dropdown + + All reports will be archived for you. + Tüm raporlar sizin için arşivlenecek. + No comment provided by engineer. + + + All servers + Tüm sunucular + No comment provided by engineer. + All your contacts will remain connected. Konuştuğun kişilerin tümü bağlı kalacaktır. @@ -819,6 +867,11 @@ Sürüm düşürmeye izin ver No comment provided by engineer. + + Allow files and media only if your contact allows them. + Dosyalara ve medyaya yalnızca iletişiminiz izin verdiğinde izin verin. + 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) @@ -854,6 +907,11 @@ 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. + Mesajları moderatörlere bildirmeye izin ver. + No comment provided by engineer. + Allow to send SimpleX links. SimpleX bağlantıları göndilmesine izin ver. @@ -899,6 +957,11 @@ Kişilerinizin kaybolan mesajlar göndermesine izin verin. No comment provided by engineer. + + Allow your contacts to send files and media. + Kişilerinizin dosya ve medya göndermesine izin verin. + No comment provided by engineer. + Allow your contacts to send voice messages. Kişilerinizin sesli mesajlar göndermesine izin verin. @@ -912,12 +975,12 @@ Already connecting! Zaten bağlanılıyor! - No comment provided by engineer. + new chat sheet title Already joining the group! Zaten gruba bağlanılıyor! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -934,6 +997,11 @@ Verilen adla boş bir sohbet profili oluşturulur ve uygulama her zamanki gibi açılır. No comment provided by engineer. + + Another reason + Başka bir sebep + report reason + Answer call Aramayı cevapla @@ -959,6 +1027,11 @@ Uygulama yerel dosyaları şifreler (videolar dışında). No comment provided by engineer. + + App group: + Uygulama grubu: + No comment provided by engineer. + App icon Uygulama simgesi @@ -1004,6 +1077,21 @@ Şuna uygula No comment provided by engineer. + + Archive + Arşivle + No comment provided by engineer. + + + Archive %lld reports? + %lld raporu arşivle? + No comment provided by engineer. + + + Archive all reports? + Tüm raporlar arşivlensin mi? + No comment provided by engineer. + Archive and upload Arşivle ve yükle @@ -1014,6 +1102,21 @@ Daha sonra görüşmek için kişileri arşivleyin. No comment provided by engineer. + + Archive report + Raporu arşivle + No comment provided by engineer. + + + Archive report? + Rapor arşivlensin mi? + No comment provided by engineer. + + + Archive reports + Raporları arşivle + swipe action + Archived contacts Arşivli kişiler @@ -1084,11 +1187,6 @@ Fotoğrafları otomatik kabul et No comment provided by engineer. - - Auto-accept settings - Ayarları otomatik olarak kabul et - alert title - Back Geri @@ -1124,6 +1222,11 @@ Daha iyi gruplar No comment provided by engineer. + + Better groups performance + Daha iyi grup performansı + No comment provided by engineer. + Better message dates. Daha iyi mesaj tarihleri. @@ -1144,6 +1247,11 @@ Daha iyi bildirimler No comment provided by engineer. + + Better privacy and security + Daha iyi gizlilik ve güvenlik + No comment provided by engineer. + Better security ✅ Daha iyi güvenlik ✅ @@ -1154,6 +1262,16 @@ Daha iyi kullanıcı deneyimi No comment provided by engineer. + + Bio + Biy + No comment provided by engineer. + + + Bio too large + Biyografi çok uzun + alert title + Black Siyah @@ -1204,6 +1322,11 @@ Medyayı bulanıklaştır No comment provided by engineer. + + Bot + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir. @@ -1224,6 +1347,11 @@ Sen ve konuştuğun kişi kaybolan mesajlar gönderebilir. No comment provided by engineer. + + Both you and your contact can send files and media. + Sen de kişilerin de medya ve dosya gönderebilir. + No comment provided by engineer. + Both you and your contact can send voice messages. Sen ve konuştuğun kişi sesli mesaj gönderebilir. @@ -1236,10 +1364,22 @@ Business address + İş adresi No comment provided by engineer. Business chats + İş konuşmaları + No comment provided by engineer. + + + Business connection + İş bağlantısı + No comment provided by engineer. + + + Businesses + İşletmeler No comment provided by engineer. @@ -1247,6 +1387,15 @@ Sohbet profiline göre (varsayılan) veya [bağlantıya göre](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + SimpleX Chat'i kullanarak şunları kabul etmiş olursunuz: +- herkese açık gruplarda yalnızca yasal içerik göndermek. +- diğer kullanıcılara saygı göstermek – spam yapmamak. + No comment provided by engineer. + Call already ended! Arama çoktan bitti! @@ -1277,6 +1426,11 @@ Üye aranamaz No comment provided by engineer. + + Can't change profile + Profil değiştirilemiyor + alert title + Can't invite contact! Kişi davet edilemiyor! @@ -1296,7 +1450,8 @@ Cancel İptal et alert action - alert button +alert button +new chat action Cancel migration @@ -1333,8 +1488,14 @@ Değiştir No comment provided by engineer. + + Change automatic message deletion? + Otomatik mesaj silme değiştirilsin mi? + alert title + Change chat profiles + Sohbet profillerini değiştir authentication reason @@ -1381,19 +1542,22 @@ Change self-destruct passcode Kendini yok eden parolayı değiştir authentication reason - set passcode view +set passcode view Chat + Sohbet No comment provided by engineer. Chat already exists + Sohbet zaten mevcut No comment provided by engineer. Chat already exists! - No comment provided by engineer. + Sohbet zaten mevcut! + new chat sheet title Chat colors @@ -1472,10 +1636,27 @@ 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. + + + Chat with admins + Yöneticilerle sohbet et + chat toolbar + + + Chat with member + Üye ile sohbet et + No comment provided by engineer. + + + Chat with members before they join. + Üyeler katılmadan önce onlarla sohbet edin. No comment provided by engineer. @@ -1483,12 +1664,19 @@ Sohbetler No comment provided by engineer. + + Chats with members + Üyelerle 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. @@ -1546,6 +1734,16 @@ Sohbet temizlensin mi? No comment provided by engineer. + + Clear group? + Grup temizlensin mi? + No comment provided by engineer. + + + Clear or delete group? + Grup temizlensin veya silinsin mi? + No comment provided by engineer. + Clear private notes? Gizli notlar temizlensin mi? @@ -1566,6 +1764,11 @@ Renk modu No comment provided by engineer. + + Community guidelines violation + Topluluk kurallarının ihlali + report reason + Compare file Dosya karşılaştır @@ -1583,38 +1786,37 @@ 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 - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + Kullanım koşulları + alert button 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. @@ -1622,6 +1824,11 @@ ICE sunucularını ayarla No comment provided by engineer. + + Configure server operators + Sunucu operatörlerini yapılandır + No comment provided by engineer. + Confirm Onayla @@ -1672,6 +1879,11 @@ Yüklemeyi onayla No comment provided by engineer. + + Confirmed + Onaylandı + token status text + Connect Bağlan @@ -1682,9 +1894,9 @@ Otomatik olarak bağlan No comment provided by engineer. - - Connect incognito - Gizli bağlan + + Connect faster! 🚀 + Daha hızlı bağlanın! 🚀 No comment provided by engineer. @@ -1697,44 +1909,39 @@ Arkadaşlarınıza daha hızlı bağlanın. No comment provided by engineer. - - Connect to yourself? - Kendine mi bağlanacaksın? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Kendine mi bağlanacaksın? Bu senin kendi SimpleX adresin! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Kendine mi bağlanacaksın? Bu senin kendi tek kullanımlık bağlantın! - No comment provided by engineer. + new chat sheet title Connect via contact address Kişi adresi aracılığıyla bağlan - No comment provided by engineer. + new chat sheet title Connect via link Bağlantı aracılığıyla bağlan - No comment provided by engineer. + new chat sheet title Connect via one-time link Tek kullanımlık bağlantı aracılığıyla bağlan - No comment provided by engineer. + new chat sheet title Connect with %@ %@ ile bağlan - No comment provided by engineer. + new chat action Connected @@ -1791,16 +1998,33 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı ve sunucuların durumu. No comment provided by engineer. + + Connection blocked + Bağlantı engellendi + No comment provided by engineer. + Connection error Bağlantı hatası - No comment provided by engineer. + alert title Connection error (AUTH) Bağlantı hatası (DOĞRULAMA) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + Bağlantı sunucu operatörü tarafından engellendi: +%@ + No comment provided by engineer. + + + Connection not ready. + Bağlantı hazır değil. + No comment provided by engineer. + Connection notifications Bağlantı bildirimleri @@ -1811,8 +2035,14 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı daveti gönderildi! No comment provided by engineer. + + Connection requires encryption renegotiation. + Bağlantı için şifreleme yeniden görüşmesi gerekiyor. + No comment provided by engineer. + Connection security + Bağlantı güvenliği No comment provided by engineer. @@ -1823,7 +2053,7 @@ Bu senin kendi tek kullanımlık bağlantın! Connection timeout Bağlantı süresi geçmiş - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1875,6 +2105,11 @@ Bu senin kendi tek kullanımlık bağlantın! Kişi tercihleri No comment provided by engineer. + + Contact requests from groups + Gruplardan gelen iletişim talepleri + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Kişiler silinecek - bu geri alınamaz ! @@ -1890,6 +2125,11 @@ 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 + İçerik kullanım koşullarını ihlal ediyor + blocking reason + Continue Devam et @@ -1932,6 +2172,7 @@ Bu senin kendi tek kullanımlık bağlantın! Create 1-time link + Tek kullanımlık bağlantı oluştur No comment provided by engineer. @@ -1964,6 +2205,11 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı oluştur No comment provided by engineer. + + Create list + Liste oluştur + No comment provided by engineer. + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 [bilgisayar uygulaması] nda yeni bir profil oluştur(https://simplex.chat/downloads/). 💻 @@ -1979,9 +2225,9 @@ Bu senin kendi tek kullanımlık bağlantın! Sıra oluştur server test step - - Create secret group - Gizli grup oluştur + + Create your address + Adresinizi oluşturun No comment provided by engineer. @@ -2021,6 +2267,7 @@ Bu senin kendi tek kullanımlık bağlantın! 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. @@ -2180,8 +2427,7 @@ Bu senin kendi tek kullanımlık bağlantın! Delete Sil alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2220,6 +2466,12 @@ Bu senin kendi tek kullanımlık bağlantın! Delete chat + Sohbeti sil + No comment provided by engineer. + + + Delete chat messages from your device. + Sohbet mesajlarını cihazınızdan silin. No comment provided by engineer. @@ -2232,8 +2484,14 @@ Bu senin kendi tek kullanımlık bağlantın! Sohbet profili silinsin mi? No comment provided by engineer. + + Delete chat with member? + Üye ile sohbet silinsin mi? + alert title + Delete chat? + Sohbet silinsin mi? No comment provided by engineer. @@ -2311,6 +2569,11 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı silinsin mi? No comment provided by engineer. + + Delete list? + Liste silinsin mi? + alert title + Delete member message? Kişinin mesajı silinsin mi? @@ -2324,7 +2587,7 @@ Bu senin kendi tek kullanımlık bağlantın! Delete messages Mesajları sil - No comment provided by engineer. + alert button Delete messages after @@ -2361,6 +2624,11 @@ Bu senin kendi tek kullanımlık bağlantın! Sırayı sil server test step + + Delete report + Raporu sil + No comment provided by engineer. + Delete up to 20 messages at once. Tek seferde en fazla 20 mesaj silin. @@ -2398,6 +2666,7 @@ Bu senin kendi tek kullanımlık bağlantın! Delivered even when Apple drops them. + Apple tarafından düşürülse bile teslim edilir. No comment provided by engineer. @@ -2415,11 +2684,21 @@ Bu senin kendi tek kullanımlık bağlantın! Mesaj gönderildi bilgisi! No comment provided by engineer. + + Deprecated options + Kullanımdan kaldırılan seçenekler + No comment provided by engineer. + Description Açıklama No comment provided by engineer. + + Description too large + Açıklama çok büyük + alert title + Desktop address Bilgisayar adresi @@ -2502,6 +2781,7 @@ Bu senin kendi tek kullanımlık bağlantın! Direct messages between members are prohibited in this chat. + Üyeler arası doğrudan mesajlar bu sohbette yasaktır. No comment provided by engineer. @@ -2519,6 +2799,16 @@ Bu senin kendi tek kullanımlık bağlantın! SimpleX Kilidini devre dışı bırak authentication reason + + Disable automatic message deletion? + Otomatik mesaj silme devre dışı bırakılsın mı? + alert title + + + Disable delete messages + Mesaj silmeyi devre dışı bırak + alert button + Disable for all Herkes için devre dışı bırak @@ -2609,6 +2899,11 @@ Bu senin kendi tek kullanımlık bağlantın! Kimlik bilgilerini proxy ile kullanmayın. No comment provided by engineer. + + Documents: + Belgeler: + No comment provided by engineer. + Don't create address Adres oluşturma @@ -2619,9 +2914,19 @@ Bu senin kendi tek kullanımlık bağlantın! Etkinleştirme No comment provided by engineer. + + Don't miss important messages. + Önemli mesajları kaçırmayın. + No comment provided by engineer. + Don't show again Yeniden gösterme + alert action + + + Done + Tamam No comment provided by engineer. @@ -2633,7 +2938,7 @@ Bu senin kendi tek kullanımlık bağlantın! Download İndir alert button - chat item action +chat item action Download errors @@ -2687,6 +2992,7 @@ Bu senin kendi tek kullanımlık bağlantın! E2E encrypted notifications. + Uçtan uca şifrelenmiş bildirimler. No comment provided by engineer. @@ -2699,6 +3005,11 @@ Bu senin kendi tek kullanımlık bağlantın! Grup profilini düzenle No comment provided by engineer. + + Empty message! + Boş mesaj! + No comment provided by engineer. + Enable Etkinleştir @@ -2709,8 +3020,9 @@ Bu senin kendi tek kullanımlık bağlantın! Etkinleştir (geçersiz kılmaları koru) No comment provided by engineer. - - Enable Flux + + Enable Flux in Network & servers settings for better metadata privacy. + Daha iyi meta veri gizliliği için Ağ & sunucu ayarlarında Flux'u etkinleştirin. No comment provided by engineer. @@ -2726,13 +3038,18 @@ Bu senin kendi tek kullanımlık bağlantın! Enable automatic message deletion? Otomatik mesaj silme etkinleştirilsin mi? - No comment provided by engineer. + alert title Enable camera access Kamera erişimini etkinleştir No comment provided by engineer. + + Enable disappearing messages by default. + Varsayılan olarak kaybolan mesajları etkinleştirin. + No comment provided by engineer. + Enable for all Herkes için etkinleştir @@ -2853,6 +3170,11 @@ Bu senin kendi tek kullanımlık bağlantın! Şifreleme yeniden anlaşma başarısız oldu. No comment provided by engineer. + + Encryption renegotiation in progress. + Şifreleme yeniden görüşmesi devam ediyor. + No comment provided by engineer. + Enter Passcode Şifre gir @@ -2920,6 +3242,7 @@ Bu senin kendi tek kullanımlık bağlantın! Error accepting conditions + Koşulları kabul ederken hata oluştu alert title @@ -2927,6 +3250,11 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı isteği kabul edilirken hata oluştu No comment provided by engineer. + + Error accepting member + Üyeyi kabul etme hatası + alert title + Error adding member(s) Üye(ler) eklenirken hata oluştu @@ -2934,13 +3262,24 @@ Bu senin kendi tek kullanımlık bağlantın! Error adding server + Sunucu eklenirken hata oluştu alert title + + Error adding short link + Kısa bağlantı ekleme hatası + No comment provided by engineer. + Error changing address Adres değiştirilirken hata oluştu No comment provided by engineer. + + Error changing chat profile + Sohbet profilini değiştirme hatası + alert title + Error changing connection profile Bağlantı profili değiştirilirken hata oluştu @@ -2954,17 +3293,26 @@ Bu senin kendi tek kullanımlık bağlantın! Error changing setting Ayar değiştirilirken hata oluştu - No comment provided by engineer. + alert title Error changing to incognito! Gizli moduna geçerken hata oluştu! No comment provided by engineer. + + Error checking token status + Jeton durumu kontrol hatası + 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2981,6 +3329,11 @@ Bu senin kendi tek kullanımlık bağlantın! Grup bağlantısı oluşturulurken hata oluştu No comment provided by engineer. + + Error creating list + Liste oluşturma hatası + alert title + Error creating member contact Kişi iletişimi oluşturulurken hata oluştu @@ -2996,20 +3349,30 @@ Bu senin kendi tek kullanımlık bağlantın! Profil oluşturulurken hata oluştu! No comment provided by engineer. + + Error creating report + Rapor oluşturma hatası + No comment provided by engineer. + Error decrypting file Dosya şifresi çözülürken hata oluştu No comment provided by engineer. + + Error deleting chat + Üye ile sohbet silme hatası + alert title + Error deleting chat database Sohbet veritabanı silinirken sorun oluştu - No comment provided by engineer. + alert title Error deleting chat! Sohbet silinirken hata oluştu! - No comment provided by engineer. + alert title Error deleting connection @@ -3019,12 +3382,12 @@ Bu senin kendi tek kullanımlık bağlantın! Error deleting database Veritabanı silinirken hata oluştu - No comment provided by engineer. + alert title Error deleting old database Eski veritabanı silinirken hata oluştu - No comment provided by engineer. + alert title Error deleting token @@ -3059,7 +3422,7 @@ Bu senin kendi tek kullanımlık bağlantın! Error exporting chat database Sohbet veritabanı dışa aktarılırken hata oluştu - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3069,7 +3432,7 @@ Bu senin kendi tek kullanımlık bağlantın! Error importing chat database Sohbet veritabanı içe aktarılırken hata oluştu - No comment provided by engineer. + alert title Error joining group @@ -3078,6 +3441,7 @@ Bu senin kendi tek kullanımlık bağlantın! Error loading servers + Sunucular yüklenirken hata oluştu alert title @@ -3087,7 +3451,12 @@ Bu senin kendi tek kullanımlık bağlantın! Error opening chat - Sohbeti açarken sorun oluştu + Kişiyi hazırlama hatası + No comment provided by engineer. + + + Error opening group + Grubu hazırlama hatası No comment provided by engineer. @@ -3105,10 +3474,25 @@ Bu senin kendi tek kullanımlık bağlantın! Hata sunuculara yeniden bağlanılıyor No comment provided by engineer. + + Error registering for notifications + Bildirimler için kayıt hatası + alert title + + + Error rejecting contact request + Kişi isteğini reddetme hatası + alert title + Error removing member Kişiyi silerken sorun oluştu - No comment provided by engineer. + alert title + + + Error reordering lists + Listeleri yeniden sıralama hatası + alert title Error resetting statistics @@ -3120,6 +3504,11 @@ Bu senin kendi tek kullanımlık bağlantın! ICE sunucularını kaydedirken sorun oluştu No comment provided by engineer. + + Error saving chat list + Sohbet listesini kaydetme hatası + alert title + Error saving group profile Grup profili kaydedilirken sorun oluştu @@ -3137,6 +3526,7 @@ Bu senin kendi tek kullanımlık bağlantın! Error saving servers + Sunucular kaydedilirken hata oluştu alert title @@ -3169,6 +3559,11 @@ Bu senin kendi tek kullanımlık bağlantın! Mesaj gönderilirken hata oluştu No comment provided by engineer. + + Error setting auto-accept + Otomatik kabul ayarında hata + No comment provided by engineer. + Error setting delivery receipts! Görüldü ayarlanırken hata oluştu! @@ -3187,7 +3582,7 @@ Bu senin kendi tek kullanımlık bağlantın! Error switching profile Profil değiştirme sırasında hata oluştu - No comment provided by engineer. + alert title Error switching profile! @@ -3199,6 +3594,11 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı senkronizasyonunda hata oluştu No comment provided by engineer. + + Error testing server connection + Sunucu bağlantısını test etme hatası + No comment provided by engineer. + Error updating group link Grup bağlantısı güncellenirken hata oluştu @@ -3211,6 +3611,7 @@ Bu senin kendi tek kullanımlık bağlantın! Error updating server + Sunucu güncellenirken hata oluştu alert title @@ -3241,7 +3642,13 @@ Bu senin kendi tek kullanımlık bağlantın! Error: %@ Hata: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3260,6 +3667,7 @@ Bu senin kendi tek kullanımlık bağlantın! Errors in servers configuration. + Sunucular yapılandırılırken hatalar oluştu. servers error @@ -3277,6 +3685,11 @@ Bu senin kendi tek kullanımlık bağlantın! Genişlet chat item action + + Expired + Süresi dolmuş + token status text + Export database Veritabanını dışarı aktar @@ -3317,20 +3730,35 @@ Bu senin kendi tek kullanımlık bağlantın! Hızlı ve gönderici çevrimiçi olana kadar beklemek yok! No comment provided by engineer. + + Faster deletion of groups. + Grupların daha hızlı silinmesi. + 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. + Mesajların daha hızlı gönderilmesi. + No comment provided by engineer. + Favorite Favori swipe action + + Favorites + Favoriler + No comment provided by engineer. + File error Dosya hatası - No comment provided by engineer. + file error alert title File errors: @@ -3339,6 +3767,13 @@ Bu senin kendi tek kullanımlık bağlantın! %@ alert message + + File is blocked by server operator: +%@. + Dosya sunucu operatörü tarafından engellendi: +%@. + file error text + File not found - most likely file was deleted or cancelled. Dosya bulunamadı - muhtemelen dosya silindi veya göderim iptal edildi. @@ -3394,6 +3829,11 @@ Bu senin kendi tek kullanımlık bağlantın! Dosyalar ve medya chat feature + + Files and media are prohibited in this chat. + Bu sohbette dosyalar ve medya yasaktır. + No comment provided by engineer. + Files and media are prohibited. Dosyalar ve medya bu grupta yasaklandı. @@ -3434,6 +3874,23 @@ Bu senin kendi tek kullanımlık bağlantın! Sohbetleri daha hızlı bul No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Düzelt @@ -3464,8 +3921,14 @@ Bu senin kendi tek kullanımlık bağlantın! Düzeltme grup üyesi tarafından desteklenmiyor No comment provided by engineer. + + For all moderators + Tüm moderatörler için + No comment provided by engineer. + For chat profile %@: + Sohbet profili için %@: servers error @@ -3475,14 +3938,22 @@ Bu senin kendi tek kullanımlık bağlantın! 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 + Benim için 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. @@ -3531,9 +4002,9 @@ Bu senin kendi tek kullanımlık bağlantın! 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 %1$@ failed to connect to destination server %2$@. Please try later. + Yönlendirme sunucusu %1$@, hedef sunucu %2$@'ya bağlanamadı. Lütfen daha sonra deneyin. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3599,6 +4070,11 @@ Hata: %2$@ GİFler ve çıkartmalar No comment provided by engineer. + + Get notified when mentioned. + Bahsedildiğinde bildirim alın. + No comment provided by engineer. + Good afternoon! İyi öğlenler! @@ -3622,7 +4098,7 @@ Hata: %2$@ Group already exists! Grup çoktan mevcut! - No comment provided by engineer. + new chat sheet title Group display name @@ -3689,6 +4165,11 @@ Hata: %2$@ Grup profili üyelerin cihazlarında saklanır, sunucularda değil. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + Grup profili değiştirildi. Eğer kaydederseniz, güncellenmiş profil grup üyelerine gönderilecektir. + alert message + Group welcome message Grup hoşgeldin mesajı @@ -3704,11 +4185,21 @@ Hata: %2$@ Grup senden silinecektir - bu geri alınamaz! No comment provided by engineer. + + Groups + Gruplar + No comment provided by engineer. + Help Yardım No comment provided by engineer. + + Help admins moderating their groups. + Yöneticilere gruplarını yönetmelerinde yardımcı olun. + No comment provided by engineer. + Hidden Gizlenmiş @@ -3761,12 +4252,19 @@ Hata: %2$@ 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ışır + alert button + How to Nasıl yapılır @@ -3909,6 +4407,16 @@ Daha fazla iyileştirme yakında geliyor! Arama içi sesler No comment provided by engineer. + + Inappropriate content + Uygunsuz içerik + report reason + + + Inappropriate profile + Uygunsuz profil + report reason + Incognito Gizli @@ -4001,6 +4509,31 @@ Daha fazla iyileştirme yakında geliyor! Arayüz renkleri No comment provided by engineer. + + Invalid + Geçersiz + token status text + + + Invalid (bad token) + Geçersiz (kötü jeton) + token status text + + + Invalid (expired) + Geçersiz (süresi dolmuş) + token status text + + + Invalid (unregistered) + Geçersiz (kayıtlı değil) + token status text + + + Invalid (wrong topic) + Geçersiz (yanlış konu) + token status text + Invalid QR code Geçersiz QR kodu @@ -4019,7 +4552,7 @@ Daha fazla iyileştirme yakında geliyor! Invalid link Geçersiz bağlantı - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4063,6 +4596,7 @@ Daha fazla iyileştirme yakında geliyor! Invite to chat + Sohbete davet et No comment provided by engineer. @@ -4131,37 +4665,32 @@ Daha fazla iyileştirme yakında geliyor! Katıl swipe action + + Join as %@ + %@ olarak katıl + No comment provided by engineer. + Join group Gruba katıl - No comment provided by engineer. + new chat sheet title Join group conversations Grup sohbetlerine katıl No comment provided by engineer. - - Join group? - Gruba katılınsın mı? - No comment provided by engineer. - Join incognito Gizli katıl No comment provided by engineer. - - Join with current profile - Şu anki profille katıl - No comment provided by engineer. - Join your group? This is your link for group %@! Bu gruba katılınsın mı? Bu senin grup için bağlantın %@! - No comment provided by engineer. + new chat action Joining group @@ -4188,6 +4717,11 @@ Bu senin grup için bağlantın %@! Kullanılmamış davet tutulsun mu? alert title + + Keep your chats clean + Sohbetlerinizi temiz tutun + No comment provided by engineer. + Keep your connections Bağlantılarınızı koruyun @@ -4225,10 +4759,12 @@ Bu senin grup için bağlantın %@! Leave chat + Sohbetten ayrıl No comment provided by engineer. Leave chat? + Sohbetten ayrılsın mı? No comment provided by engineer. @@ -4241,6 +4777,11 @@ Bu senin grup için bağlantın %@! Gruptan çıkılsın mı? No comment provided by engineer. + + Less traffic on mobile networks. + Mobil ağlarda daha az trafik. + No comment provided by engineer. + Let's talk in SimpleX Chat Hadi SimpleX Chat'te konuşalım @@ -4271,6 +4812,21 @@ Bu senin grup için bağlantın %@! Bağlanmış bilgisayarlar No comment provided by engineer. + + List + Liste + swipe action + + + List name and emoji should be different for all lists. + Liste adı ve emojisi tüm listeler için farklı olmalıdır. + No comment provided by engineer. + + + List name... + Liste adı... + No comment provided by engineer. + Live message! Canlı mesaj! @@ -4281,6 +4837,11 @@ Bu senin grup için bağlantın %@! Canlı mesajlar No comment provided by engineer. + + Loading profile… + Profil yükleniyor… + in progress text + Local name Yerel isim @@ -4356,13 +4917,34 @@ Bu senin grup için bağlantın %@! Kişi No comment provided by engineer. + + Member %@ + Üye%@ + past/unknown group member + + + Member admission + Üye kabulü + No comment provided by engineer. + Member inactive Üye inaktif item status text + + Member is deleted - can't accept request + Üye silinmiş - istek kabul edilemez + No comment provided by engineer. + + + Member reports + Üye raporları + 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. @@ -4377,6 +4959,7 @@ Bu senin grup için bağlantın %@! Member will be removed from chat - this cannot be undone! + Üye sohbetten kaldırılacak - bu geri alınamaz! No comment provided by engineer. @@ -4384,6 +4967,11 @@ Bu senin grup için bağlantın %@! Üye gruptan çıkarılacaktır - bu geri alınamaz! No comment provided by engineer. + + Member will join the group, accept member? + Üye gruba katılacak, kabul edilsin mi? + alert message + Members can add message reactions. Grup üyeleri mesaj tepkileri ekleyebilir. @@ -4394,6 +4982,11 @@ Bu senin grup için bağlantın %@! Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) No comment provided by engineer. + + Members can report messsages to moderators. + Üyeler mesajları moderatörlere bildirebilir. + No comment provided by engineer. + Members can send SimpleX links. Grup üyeleri SimpleX bağlantıları gönderebilir. @@ -4419,6 +5012,11 @@ Bu senin grup için bağlantın %@! Grup üyeleri sesli mesajlar gönderebilir. No comment provided by engineer. + + Mention members 👋 + Üyeleri belirtin 👋 + No comment provided by engineer. + Menus Menüler @@ -4449,6 +5047,11 @@ Bu senin grup için bağlantın %@! Mesaj iletildi item status text + + Message instantly once you tap Connect. + Bağlan'a dokunduğunuzda hemen mesaj gönderin. + No comment provided by engineer. + Message may be delivered later if member becomes active. Kullanıcı aktif olursa mesaj iletilebilir. @@ -4524,11 +5127,21 @@ Bu senin grup için bağlantın %@! Mesajlar & dosyalar No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + Mesajlar **uçtan uca şifreleme** ile korunmaktadır. + No comment provided by engineer. + Messages from %@ will be shown! %@ den gelen mesajlar gösterilecektir! No comment provided by engineer. + + Messages in this chat will never be deleted. + Bu sohbetteki mesajlar asla silinmeyecek. + alert message + Messages received Mesajlar alındı @@ -4629,6 +5242,11 @@ Bu senin grup için bağlantın %@! %@ de yönetildi copied message info + + More + Daha fazla + swipe action + More improvements are coming soon! Daha fazla geliştirmeler yakında geliyor! @@ -4641,6 +5259,7 @@ Bu senin grup için bağlantın %@! More reliable notifications + Daha güvenilir bildirimler No comment provided by engineer. @@ -4656,7 +5275,12 @@ Bu senin grup için bağlantın %@! Mute Sustur - swipe action + notification label action + + + Mute all + Tümünü sessize al + notification label action Muted when inactive! @@ -4680,6 +5304,7 @@ Bu senin grup için bağlantın %@! Network decentralization + Ağ merkeziyetsizliği No comment provided by engineer. @@ -4694,6 +5319,7 @@ Bu senin grup için bağlantın %@! Network operator + Ağ operatörü No comment provided by engineer. @@ -4704,7 +5330,12 @@ Bu senin grup için bağlantın %@! Network status Ağ durumu - No comment provided by engineer. + alert title + + + New + Yeni + token status text New Passcode @@ -4753,8 +5384,14 @@ Bu senin grup için bağlantın %@! New events + Yeni etkinlikler notification + + New group role: Moderator + Yeni grup rolü: Moderatör + No comment provided by engineer. + New in %@ %@ da yeni @@ -4770,6 +5407,11 @@ Bu senin grup için bağlantın %@! Yeni üye rolü No comment provided by engineer. + + New member wants to join the group. + Yeni üye gruba katılmak istiyor. + rcv group event chat item + New message Yeni mesaj @@ -4782,6 +5424,7 @@ Bu senin grup için bağlantın %@! New server + Yeni sunucu No comment provided by engineer. @@ -4794,6 +5437,26 @@ Bu senin grup için bağlantın %@! Uygulama şifresi yok Authentication unavailable + + No chats + Hiç sohbet yok + No comment provided by engineer. + + + No chats found + Hiç sohbet bulunamadı + No comment provided by engineer. + + + No chats in list %@ + Listede hiç sohbet yok %@ + No comment provided by engineer. + + + No chats with members + Üyelerle hiç sohbet yok + No comment provided by engineer. + No contacts selected Hiçbir kişi seçilmedi @@ -4841,10 +5504,17 @@ Bu senin grup için bağlantın %@! No media & file servers. + Hiç medya & dosya sunucusu yok. servers error + + No message + Mesaj yok + No comment provided by engineer. + No message servers. + Hiç mesaj sunucusu yok. servers error @@ -4867,6 +5537,11 @@ Bu senin grup için bağlantın %@! Sesli mesaj kaydetmek için izin yok No comment provided by engineer. + + No private routing session + Özel yönlendirme oturumu yok + alert title + No push server Yerel @@ -4879,20 +5554,34 @@ Bu senin grup için bağlantın %@! No servers for private message routing. + Özel mesaj yönlendirmesi için hiç sunucu yok. servers error No servers to receive files. + Dosya almak için hiç sunucu yok. servers error No servers to receive messages. + Mesaj almak için hiç sunucu yok. servers error No servers to send files. + Dosya göndermek için hiç sunucu yok. servers error + + No token! + Hiç jeton yok! + alert title + + + No unread chats + Okunmamış sohbet yok + No comment provided by engineer. + No user identifiers. Herhangi bir kullanıcı tanımlayıcısı yok. @@ -4903,6 +5592,11 @@ Bu senin grup için bağlantın %@! Uyumlu değil! No comment provided by engineer. + + Notes + Notlar + No comment provided by engineer. + Nothing selected Hiçbir şey seçilmedi @@ -4923,10 +5617,21 @@ Bu senin grup için bağlantın %@! Bildirimler devre dışı! No comment provided by engineer. + + Notifications error + Bildirim hatası + alert title + Notifications privacy + Bildirim gizliliği No comment provided by engineer. + + Notifications status + Bildirim durumu + alert title + Now admins can: - delete members' messages. @@ -4949,7 +5654,9 @@ Bu senin grup için bağlantın %@! Ok Tamam - alert button + alert action +alert button +new chat action Old database @@ -4982,6 +5689,7 @@ VPN'nin etkinleştirilmesi gerekir. Only chat owners can change preferences. + Yalnızca sohbet sahipleri tercihleri değiştirebilir. No comment provided by engineer. @@ -5009,6 +5717,16 @@ VPN'nin etkinleştirilmesi gerekir. Yalnızca grup sahipleri sesli mesajları etkinleştirebilir. No comment provided by engineer. + + Only sender and moderators see it + Sadece gönderici ve moderatörler görür + No comment provided by engineer. + + + Only you and moderators see it + Sadece siz ve moderatörler görür + No comment provided by engineer. + Only you can add message reactions. Sadece siz mesaj tepkileri ekleyebilirsiniz. @@ -5029,6 +5747,11 @@ VPN'nin etkinleştirilmesi gerekir. Sadece sen kaybolan mesajlar gönderebilirsin. No comment provided by engineer. + + Only you can send files and media. + Sadece sen dosya ve medya gönderebilirsin. + No comment provided by engineer. + Only you can send voice messages. Sadece sen sesli mesajlar gönderebilirsin. @@ -5054,6 +5777,11 @@ VPN'nin etkinleştirilmesi gerekir. Sadece karşıdaki kişi kaybolan mesajlar gönderebilir. No comment provided by engineer. + + Only your contact can send files and media. + Sadece kişilerin dosya ve medya gönderebilir. + No comment provided by engineer. + Only your contact can send voice messages. Sadece karşıdaki kişi sesli mesajlar gönderebilir. @@ -5062,7 +5790,7 @@ VPN'nin etkinleştirilmesi gerekir. Open - No comment provided by engineer. + alert action Open Settings @@ -5071,32 +5799,79 @@ VPN'nin etkinleştirilmesi gerekir. Open changes + Açık değişiklikler No comment provided by engineer. Open chat Sohbeti aç - No comment provided by engineer. + new chat action Open chat console Sohbet konsolunu aç authentication reason + + Open clean link + Temiz linki aç + alert action + Open conditions + Açık koşullar No comment provided by engineer. + + Open full link + Tam linki aç + alert action + Open group Grubu aç - No comment provided by engineer. + new chat action + + + Open link? + Bağlantıyı aç? + alert title Open migration to another device Başka bir cihaza açık geçiş authentication reason + + Open new chat + Yeni sohbet aç + new chat action + + + Open new group + Yeni grup aç + new chat action + + + Open to accept + Kabul etmek için aç + No comment provided by engineer. + + + Open to connect + Bağlanmak için aç + No comment provided by engineer. + + + Open to join + Katılmak için aç + No comment provided by engineer. + + + Open to use bot + Botu kullanmak için aç + No comment provided by engineer. + Opening app… Uygulama açılıyor… @@ -5104,14 +5879,17 @@ VPN'nin etkinleştirilmesi gerekir. Operator + Operatör No comment provided by engineer. Operator server + Operatör sunucusu alert title Or import archive file + Veya arşiv dosyasını içe aktar No comment provided by engineer. @@ -5136,6 +5914,12 @@ VPN'nin etkinleştirilmesi gerekir. Or to share privately + Veya özel olarak paylaşmak için + No comment provided by engineer. + + + Organize chats into lists + Sohbetleri listelere ayır No comment provided by engineer. @@ -5195,11 +5979,6 @@ VPN'nin etkinleştirilmesi gerekir. Gösterilecek şifre No comment provided by engineer. - - Past member %@ - Geçmiş üye %@ - past/unknown group member - Paste desktop address Bilgisayar adresini yapıştır @@ -5270,7 +6049,7 @@ Lütfen diğer herhangi bir sorunu geliştiricilerle paylaşın. Please check your network connection with %@ and try again. Lütfen ağ bağlantınızı %@ ile kontrol edin ve tekrar deneyin. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5329,6 +6108,26 @@ 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. + Lütfen bildirimleri devre dışı bırakmayı ve yeniden etkinleştirmeyi deneyin. + token info + + + Please wait for group moderators to review your request to join the group. + Lütfen grup moderatörlerinin gruba katılma isteğinizi incelemesini bekleyin. + snd group event chat item + + + Please wait for token activation to complete. + Lütfen jeton aktivasyonunun tamamlanmasını bekleyin. + token info + + + Please wait for token to be registered. + Lütfen jeton kaydedilene kadar bekleyin. + token info + Polish interface Lehçe arayüz @@ -5339,11 +6138,6 @@ Hata: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil - server test error - Preserve the last message draft, with attachments. Son mesaj taslağını ekleriyle birlikte koru. @@ -5356,6 +6150,7 @@ Hata: %@ Preset servers + Ön ayar sunucuları No comment provided by engineer. @@ -5375,6 +6170,12 @@ Hata: %@ Privacy for your customers. + Müşterileriniz için gizlilik. + No comment provided by engineer. + + + Privacy policy and conditions of use. + Gizlilik politikası ve kullanım koşulları. No comment provided by engineer. @@ -5382,11 +6183,21 @@ Hata: %@ Gizlilik yeniden tanımlandı No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + Özel sohbetler, gruplar ve kişilerinize sunucu operatörleri tarafından erişilemez. + No comment provided by engineer. + Private filenames Gizli dosya adları No comment provided by engineer. + + Private media file names. + Özel medya dosyası adları. + No comment provided by engineer. + Private message routing Gizli mesaj yönlendirme @@ -5410,7 +6221,12 @@ Hata: %@ Private routing error Gizli yönlendirme hatası - No comment provided by engineer. + alert title + + + Private routing timeout + Özel yönlendirme zaman aşımı + alert title Profile and server connections @@ -5462,6 +6278,11 @@ Hata: %@ Mesajlarda tepkileri yasakla. No comment provided by engineer. + + Prohibit reporting messages to moderators. + Mesajları moderatörlere bildirmeyi yasakla. + No comment provided by engineer. + Prohibit sending SimpleX links. SimpleX bağlantısı gönderimini yasakla. @@ -5509,6 +6330,11 @@ Enable in *Network & servers* settings. Bir parolayla birlikte sohbet profillerini koru! No comment provided by engineer. + + Protocol background timeout + Protokol arka plan zaman aşımı + No comment provided by engineer. + Protocol timeout Protokol zaman aşımı @@ -5596,7 +6422,7 @@ Enable in *Network & servers* settings. Receipts are disabled - Alıcılar devre dışı bırakıldı + Alındı onayları devre dışı bırakıldı No comment provided by engineer. @@ -5614,11 +6440,6 @@ Enable in *Network & servers* settings. Şuradan alındı: %@ copied message info - - Received file event - Dosya etkinliği alındı - notification - Received message Mesaj alındı @@ -5719,11 +6540,27 @@ Enable in *Network & servers* settings. Azaltılmış pil kullanımı No comment provided by engineer. + + Register + Kaydol + No comment provided by engineer. + + + Register notification token? + Bildirim jetonunu kaydet? + token info + + + Registered + Kayıtlı + token status text + Reject Reddet - reject incoming call via notification - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5733,7 +6570,12 @@ Enable in *Network & servers* settings. Reject contact request Bağlanma isteğini reddet - No comment provided by engineer. + alert title + + + Reject member? + Üyeyi reddet? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5760,6 +6602,11 @@ Enable in *Network & servers* settings. Resmi kaldır No comment provided by engineer. + + Remove link tracking + Bağlantı izlemeyi kaldır + No comment provided by engineer. + Remove member Kişiyi sil @@ -5775,6 +6622,11 @@ Enable in *Network & servers* settings. Anahtar Zinciri'ndeki parola silinsin mi? No comment provided by engineer. + + Removes messages and blocks members. + Mesajları kaldırır ve üyeleri engeller. + No comment provided by engineer. + Renegotiate Yeniden müzakere @@ -5790,11 +6642,6 @@ Enable in *Network & servers* settings. Şifreleme yeniden müzakere edilsin mi? No comment provided by engineer. - - Repeat connection request? - Bağlantı isteği tekrarlansın mı? - No comment provided by engineer. - Repeat download Tekrar indir @@ -5805,11 +6652,6 @@ Enable in *Network & servers* settings. İthalatı tekrarla No comment provided by engineer. - - Repeat join request? - Katılma isteği tekrarlansın mı? - No comment provided by engineer. - Repeat upload Yüklemeyi tekrarla @@ -5820,6 +6662,61 @@ Enable in *Network & servers* settings. Yanıtla chat item action + + Report + Rapor et + chat item action + + + Report content: only group moderators will see it. + İçeriği rapor et: sadece grup moderatörleri görecek. + report reason + + + Report member profile: only group moderators will see it. + Üye profilini rapor et: sadece grup moderatörleri görecek. + report reason + + + Report other: only group moderators will see it. + Diğerini rapor et: sadece grup moderatörleri görecek. + report reason + + + Report reason? + Rapor nedeni? + No comment provided by engineer. + + + Report sent to moderators + Rapor moderatörlere gönderildi + alert title + + + Report spam: only group moderators will see it. + Spam rapor et: sadece grup moderatörleri görecek. + report reason + + + Report violation: only group moderators will see it. + İhlali rapor et: sadece grup moderatörleri görecek. + report reason + + + Report: %@ + Rapor: %@ + report in notification + + + Reporting messages to moderators is prohibited. + Mesajları moderatörlere bildirmek yasaktır. + No comment provided by engineer. + + + Reports + Raporlar + No comment provided by engineer. + Required Gerekli @@ -5898,7 +6795,7 @@ Enable in *Network & servers* settings. Retry Yeniden dene - No comment provided by engineer. + alert action Reveal @@ -5907,12 +6804,24 @@ Enable in *Network & servers* settings. Review conditions + Koşulları gözden geçir No comment provided by engineer. - - Review later + + Review group members + Grup üyelerini gözden geçir No comment provided by engineer. + + Review members + Üyeleri gözden geçir + admission stage + + + Review members before admitting ("knocking"). + Üyeleri kabul etmeden önce gözden geçir ("kapı çalma"). + admission stage description + Revoke İptal et @@ -5962,13 +6871,23 @@ Enable in *Network & servers* settings. Save Kaydet alert button - chat item action +chat item action Save (and notify contacts) Kaydet (ve kişilere bildir) alert button + + Save (and notify members) + Kaydet (ve üyelere bildir) + alert button + + + Save admission settings? + Kabul ayarlarını kaydet? + alert title + Save and notify contact Kaydet ve kişilere bildir @@ -5994,6 +6913,16 @@ Enable in *Network & servers* settings. Grup profilini kaydet No comment provided by engineer. + + Save group profile? + Grup profilini kaydet? + alert title + + + Save list + Listeyi kaydet + No comment provided by engineer. + Save passphrase and open chat Parolayı kaydet ve sohbeti aç @@ -6184,6 +7113,11 @@ Enable in *Network & servers* settings. Bir canlı mesaj gönder - yazışına göre kişiye(lere) kendini günceller No comment provided by engineer. + + Send contact request? + Kişi isteği gönderilsin mi? + No comment provided by engineer. + Send delivery receipts to Görüldü bilgilerini şuraya gönder @@ -6234,6 +7168,11 @@ Enable in *Network & servers* settings. Bildirimler gönder No comment provided by engineer. + + Send private reports + Özel raporlar gönder + No comment provided by engineer. + Send questions and ideas Fikirler ve sorular gönderin @@ -6244,6 +7183,16 @@ Enable in *Network & servers* settings. Mesajlar gönder No comment provided by engineer. + + Send request + İstek gönder + No comment provided by engineer. + + + Send request without message + Mesaj olmadan istek gönder + No comment provided by engineer. + Send them from gallery or custom keyboards. Bunları galeriden veya özel klavyelerden gönder. @@ -6254,6 +7203,11 @@ Enable in *Network & servers* settings. Yeni üyelere 100 adete kadar son mesajları gönderin. No comment provided by engineer. + + Send your private feedback to groups. + Özel geri bildiriminizi gruplara gönderin. + No comment provided by engineer. + Sender cancelled file transfer. Gönderici dosya gönderimini iptal etti. @@ -6271,7 +7225,7 @@ Enable in *Network & servers* settings. Sending delivery receipts will be enabled for all contacts. - Tüm kişiler için iletim bilgisi gönderme özelliği etkinleştirilecek. + Tüm kişiler için alındı bilgisi gönderme özelliği etkinleştirilecek. No comment provided by engineer. @@ -6319,11 +7273,6 @@ Enable in *Network & servers* settings. Direkt gönderildi No comment provided by engineer. - - Sent file event - Dosya etkinliği gönderildi - notification - Sent message Mesaj gönderildi @@ -6361,6 +7310,7 @@ Enable in *Network & servers* settings. Server added to operator %@. + Sunucu operatör %@'ya eklendi. alert message @@ -6380,23 +7330,26 @@ Enable in *Network & servers* settings. Server operator changed. + Sunucu operatörü değişti. alert title Server operators + Sunucu operatörleri No comment provided by engineer. Server protocol changed. + Sunucu protokolü değişti. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Sunucunun yükleme yapması için yetki gereklidir, şifreyi kontrol edin server test error @@ -6445,6 +7398,11 @@ Enable in *Network & servers* settings. 1 günlüğüne ayarla No comment provided by engineer. + + Set chat name… + Sohbet adını belirle… + No comment provided by engineer. + Set contact name… Kişi adı gir… @@ -6465,6 +7423,16 @@ Enable in *Network & servers* settings. Sistem kimlik doğrulaması yerine ayarla. No comment provided by engineer. + + Set member admission + Üye kabulünü ayarla + No comment provided by engineer. + + + Set message expiration in chats. + Sohbetlerde mesajın sonlanma süresini ayarla. + No comment provided by engineer. + Set passcode Şifre ayarla @@ -6480,6 +7448,11 @@ Enable in *Network & servers* settings. Dışa aktarmak için parola ayarla No comment provided by engineer. + + Set profile bio and welcome message. + Profil biyografisi ve hoşgeldiniz mesajı düzenle. + No comment provided by engineer. + Set the message shown to new members! Yeni üyeler için gösterilen bir mesaj ayarla! @@ -6509,7 +7482,7 @@ Enable in *Network & servers* settings. Share Paylaş alert action - chat item action +chat item action Share 1-time link @@ -6518,10 +7491,12 @@ Enable in *Network & servers* settings. Share 1-time link with a friend + Arkadaşınızla 1 defaya mahsus bağlantı paylaşın No comment provided by engineer. Share SimpleX address on social media. + SimpleX adresini sosyal medyada paylaşın. No comment provided by engineer. @@ -6531,6 +7506,7 @@ Enable in *Network & servers* settings. Share address publicly + Adresinizi herkese açık olarak paylaşın No comment provided by engineer. @@ -6548,6 +7524,16 @@ Enable in *Network & servers* settings. Bağlantıyı paylaş No comment provided by engineer. + + Share old address + Eski adresi paylaş + alert button + + + Share old link + Eski bağlantıyı paylaş + alert button + Share profile Profil paylaş @@ -6568,6 +7554,26 @@ Enable in *Network & servers* settings. Kişilerle paylaş No comment provided by engineer. + + Share your address + Adresini paylaş + No comment provided by engineer. + + + Short SimpleX address + Kısa SimpleX adresi + No comment provided by engineer. + + + Short description + Kısa açıklama + No comment provided by engineer. + + + Short link + Kısa bağlantı + No comment provided by engineer. + Show QR code QR kodunu göster @@ -6625,6 +7631,7 @@ Enable in *Network & servers* settings. SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + SimpleX Chat ve Flux, Flux tarafından işletilen sunucuları uygulamaya dahil etmek için bir anlaşma yaptı. No comment provided by engineer. @@ -6659,12 +7666,24 @@ Enable in *Network & servers* settings. SimpleX address and 1-time links are safe to share via any messenger. + SimpleX adresi ve 1 defaya mahsus bağlantılar, herhangi bir mesajlaşma uygulaması aracılığıyla paylaşmak için güvenlidir. No comment provided by engineer. SimpleX address or 1-time link? + SimpleX adresi mi yoksa 1 defaya mahsus bağlantı mı? No comment provided by engineer. + + SimpleX address settings + Ayarları otomatik olarak kabul et + alert title + + + SimpleX channel link + SimpleX kanal bağlantısı + simplex link type + SimpleX contact address SimpleX kişi adresi @@ -6705,6 +7724,11 @@ Enable in *Network & servers* settings. SimpleX protokolleri Trail of Bits tarafından incelenmiştir. No comment provided by engineer. + + SimpleX relay link + SimpleX aktarıcı bağlantısı + simplex link type + Simplified incognito mode Basitleştirilmiş gizli mod @@ -6758,6 +7782,8 @@ Enable in *Network & servers* settings. Some servers failed the test: %@ + Bazı sunucular testi geçemedi: +%@ alert message @@ -6765,6 +7791,12 @@ Enable in *Network & servers* settings. Biri notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Kare,daire, veya aralarında herhangi bir şey. @@ -6850,6 +7882,11 @@ Enable in *Network & servers* settings. Sohbeti durdurma No comment provided by engineer. + + Storage + Depolama + No comment provided by engineer. + Strong Güçlü @@ -6905,11 +7942,21 @@ Enable in *Network & servers* settings. TCP bağlantısı No comment provided by engineer. + + TCP connection bg timeout + TCP bağlantı arka plan zaman aşımı + No comment provided by engineer. + TCP connection timeout TCP bağlantı zaman aşımı No comment provided by engineer. + + TCP port for messaging + Mesajlaşma için TCP portu + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -6935,8 +7982,29 @@ Enable in *Network & servers* settings. Fotoğraf çek No comment provided by engineer. + + Tap Connect to chat + Sohbet etmek için Bağlan'a dokunun + No comment provided by engineer. + + + Tap Connect to send request + Bağlan'a dokunarak isteği gönderin + No comment provided by engineer. + + + Tap Connect to use bot + Botu kullanmak için Bağlan tuşuna bas + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. + Daha sonra oluşturmak için menüden BasitX adresi oluştur'a dokunun. + No comment provided by engineer. + + + Tap Join group + Gruba katıl'a dokunun No comment provided by engineer. @@ -6977,13 +8045,18 @@ Enable in *Network & servers* settings. Temporary file error Geçici dosya hatası - No comment provided by engineer. + file error alert title Test failed at step %@. Test %@ adımında başarısız oldu. server test failure + + Test notifications + Bildirimleri test et + No comment provided by engineer. + Test server Sunucuyu test et @@ -7021,6 +8094,11 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + Adres kısa olacak ve profiliniz bu adres üzerinden paylaşılacaktır. + alert message + 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. @@ -7028,6 +8106,7 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. The app protects your privacy by using different operators in each conversation. + Uygulama, her sohbette farklı operatörler kullanarak gizliliğinizi korur. No comment provided by engineer. @@ -7047,6 +8126,7 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. The connection reached the limit of undelivered messages, your contact may be offline. + Bağlantı, teslim edilmemiş mesajlar limitine ulaştı, kişiniz çevrimdışı olabilir. No comment provided by engineer. @@ -7079,6 +8159,11 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Önceki mesajın hash'i farklı. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + Bağlantı kısa olacak ve grup profili bağlantı üzerinden paylaşılacaktır. + alert message + The message will be deleted for all members. Mesaj tüm üyeler için silinecektir. @@ -7104,21 +8189,14 @@ 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. - No comment provided by engineer. - The same conditions will apply to operator **%@**. - No comment provided by engineer. - - - The same conditions will apply to operator(s): **%@**. + Aynı koşullar operatör **%@** için de geçerli olacaktır. No comment provided by engineer. The second preset operator in the app! + Uygulamadaki ikinci ön ayar operatörü! No comment provided by engineer. @@ -7129,7 +8207,7 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. The sender will NOT be notified Gönderene BİLDİRİLMEYECEKTİR - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7138,6 +8216,7 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. The servers for new files of your current chat profile **%@**. + Mevcut sohbet profiliniz için yeni dosyaların sunucuları **%@**. No comment provided by engineer. @@ -7157,6 +8236,7 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. These conditions will also apply for: **%@**. + Bu koşullar ayrıca şunlar için de geçerli olacaktır: **%@**. No comment provided by engineer. @@ -7179,6 +8259,11 @@ 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. + Bu işlem geri alınamaz - daha önce seçilen tarihten önceki bu sohbette gönderilen ve alınan mesajlar silinecektir. + 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. @@ -7214,14 +8299,9 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu grup artık mevcut değildir. No comment provided by engineer. - - This is your own SimpleX address! - Bu senin kendi SimpleX adresin! - No comment provided by engineer. - - - This is your own one-time link! - Bu senin kendi tek kullanımlık bağlantın! + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Bu bağlantı daha yeni bir uygulama sürümü gerektiriyor. Lütfen uygulamayı güncelleyin veya kişinizden uyumlu bir bağlantı göndermesini isteyin. No comment provided by engineer. @@ -7229,11 +8309,26 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. 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. + Bu mesaj silindi veya henüz alınmadı. + 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. + + This setting is for your current profile **%@**. + Bu ayar, mevcut profiliniz içindir. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + Kaybolma süresi yalnızca yeni kişiler için ayarlanır. + No comment provided by engineer. + Title Başlık @@ -7261,6 +8356,7 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. To protect against your link being replaced, you can compare contact security codes. + Bağlantınızın değiştirilmesine karşı korunmak için, kişi güvenlik kodlarını karşılaştırabilirsiniz. No comment provided by engineer. @@ -7287,6 +8383,7 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec To receive + Almak için No comment provided by engineer. @@ -7311,15 +8408,27 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec To send + Göndermek için No comment provided by engineer. + + To send commands you must be connected. + Komut göndermek için bağlı olmanız gerekir. + alert message + To support instant push notifications the chat database has to be migrated. Anlık anlık bildirimleri desteklemek için sohbet veritabanının taşınması gerekir. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + Bağlantı denemesinden sonra başka bir profili kullanmak için, sohbeti silin ve bağlantıyı tekrar kullanın. + alert message + To use the servers of **%@**, accept conditions of use. + **%@**'nın sunucularını kullanmak için, kullanım koşullarını kabul edin. No comment provided by engineer. @@ -7337,6 +8446,11 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Bağlanırken gizli moda geçiş yap. No comment provided by engineer. + + Token status: %@. + Jeton durumu: %@. + token status + Toolbar opacity Araç çubuğu opaklığı @@ -7357,15 +8471,9 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Taşıma oturumları No comment provided by engineer. - - Trying to connect to the server used to receive messages from this contact (error: %@). - Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor. - No comment provided by engineer. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7414,6 +8522,7 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Undelivered messages + Teslim edilmemiş mesajlar No comment provided by engineer. @@ -7501,13 +8610,18 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Unmute Susturmayı kaldır - swipe action + notification label action Unread Okunmamış swipe action + + Unsupported connection link + Desteklenmeyen bağlantı bağlantısı + No comment provided by engineer. + Up to 100 last messages are sent to new members. Yeni üyelere 100e kadar en son mesajlar gönderildi. @@ -7533,16 +8647,51 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Ayarları güncelleyelim mi? No comment provided by engineer. + + Updated conditions + Güncellenmiş koşullar + No comment provided by engineer. + Updating settings will re-connect the client to all servers. Ayarların güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır. No comment provided by engineer. + + Upgrade + Yükseltme + alert button + + + Upgrade address + Adres güncelleme + No comment provided by engineer. + + + Upgrade address? + Adres güncellensin mi? + alert message + Upgrade and open chat Yükselt ve sohbeti aç No comment provided by engineer. + + Upgrade group link? + Grub linki güncellensin mi? + alert message + + + Upgrade link + Linki güncelle + No comment provided by engineer. + + + Upgrade your address + Adresini yükselt + No comment provided by engineer. + Upload errors Yükleme hataları @@ -7575,6 +8724,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Use %@ + %@ kullan No comment provided by engineer. @@ -7592,6 +8742,16 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste SimpleX Chat sunucuları kullanılsın mı? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + Port belirtilmediğinde TCP port %@ kullanın. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + Sadece ön ayar sunucuları için TCP port 443 kullanın. + No comment provided by engineer. + Use chat Sohbeti kullan @@ -7600,14 +8760,16 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Use current profile Şu anki profili kullan - No comment provided by engineer. + new chat action Use for files + Dosyalar için kullan No comment provided by engineer. Use for messages + Mesajlar için kullan No comment provided by engineer. @@ -7625,10 +8787,15 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste iOS arama arayüzünden kullan No comment provided by engineer. + + Use incognito profile + Gizli profil kullan + No comment provided by engineer. + Use new incognito profile Yeni gizli profilden kullan - No comment provided by engineer. + new chat action Use only local notifications? @@ -7652,6 +8819,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Use servers + Sunucuları kullan No comment provided by engineer. @@ -7664,6 +8832,11 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Uygulamayı tek elle kullan. No comment provided by engineer. + + Use web port + Web portunu kullan + No comment provided by engineer. + User selection Kullanıcı seçimi @@ -7746,6 +8919,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste View conditions + Koşulları görüntüle No comment provided by engineer. @@ -7755,6 +8929,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste View updated conditions + Güncellenmiş koşulları görüntüle No comment provided by engineer. @@ -7852,6 +9027,11 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Hoş geldiniz mesajı çok uzun No comment provided by engineer. + + Welcome your contacts 👋 + Kişilerine hoş geldin👋 + No comment provided by engineer. + What's new Neler yeni @@ -7869,6 +9049,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Birden fazla operatör etkinleştirildiğinde, hiçbiri kimin kiminle iletişim kurduğunu öğrenmek için meta veriye sahip değildir. No comment provided by engineer. @@ -7968,17 +9149,18 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste You are already connected with %@. + Zaten %@ ile bağlantıdasınız. No comment provided by engineer. You are already connecting to %@. Zaten %@'a bağlanıyorsunuz. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Bu tek seferlik bağlantı üzerinden zaten bağlanıyorsunuz! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7988,35 +9170,33 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste You are already joining the group %@. Zaten %@ grubuna katılıyorsunuz. - No comment provided by engineer. - - - You are already joining the group via this link! - Bu bağlantı üzerinden gruba zaten katılıyorsunuz! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Gruba zaten bu bağlantı üzerinden katılıyorsunuz. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Gruba zaten katılıyorsunuz! Katılma isteği tekrarlansın mı? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Gruba davet edildiniz No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + 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. @@ -8032,12 +9212,9 @@ Katılma isteği tekrarlansın mı? Görünüm ayarlarından değiştirebilirsiniz. No comment provided by engineer. - - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. + Sunucuları ayarlar aracılığıyla yapılandırabilirsiniz. No comment provided by engineer. @@ -8082,6 +9259,7 @@ Katılma isteği tekrarlansın mı? You can set connection name, to remember who the link was shared with. + Bağlantı adını ayarlayabilirsiniz, böylece bağlantının kiminle paylaşıldığını hatırlarsınız. No comment provided by engineer. @@ -8124,10 +9302,15 @@ Katılma isteği tekrarlansın mı? Bağlantı detaylarından davet bağlantısını yeniden görüntüleyebilirsin. alert message + + You can view your reports in Chat with admins. + Raporlarınızı Yöneticilerle Sohbet bölümünde görüntüleyebilirsiniz. + alert message + You can't send messages! Mesajlar gönderemezsiniz! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8139,17 +9322,12 @@ Katılma isteği tekrarlansın mı? Kimin bağlanabileceğine siz karar verirsiniz. No comment provided by engineer. - - You have already requested connection via this address! - Bu adres üzerinden zaten bağlantı talebinde bulundunuz! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Zaten bağlantı isteğinde bulundunuz! Bağlantı isteği tekrarlansın mı? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8206,6 +9384,16 @@ Bağlantı isteği tekrarlansın mı? Grup daveti gönderdiniz No comment provided by engineer. + + You should receive notifications. + Bildirim almanız gerekiyor. + token info + + + You will be able to send messages **only after your request is accepted**. + Mesaj gönderebilmek için **isteğinizin kabul edilmesini beklemelisiniz**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Grup sahibinin cihazı çevrimiçi olduğunda gruba bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin! @@ -8231,11 +9419,6 @@ Bağlantı isteği tekrarlansın mı? Arka planda 30 saniye kaldıktan sonra uygulamayı başlattığınızda veya devam ettirdiğinizde kimlik doğrulaması yapmanız gerekecektir. No comment provided by engineer. - - You will connect to all group members. - Bütün grup üyelerine bağlanacaksın. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz. @@ -8243,6 +9426,7 @@ Bağlantı isteği tekrarlansın mı? You will stop receiving messages from this chat. Chat history will be preserved. + Bu sohbetten mesaj almaya son vereceksiniz. Sohbet geçmişi korunacaktır. No comment provided by engineer. @@ -8270,16 +9454,16 @@ Bağlantı isteği tekrarlansın mı? 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 business contact + İş bağlantınız + No comment provided by engineer. + Your calls Aramaların @@ -8305,11 +9489,21 @@ Bağlantı isteği tekrarlansın mı? Sohbet profillerin No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Sohbetiniz %@'ya taşındı ancak sizi profile yönlendirirken beklenmedik bir hata oluştu. + alert message + + + Your connection was moved to %@ but an error happened when switching 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 + İrtibat kişiniz + 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. @@ -8340,6 +9534,11 @@ Bağlantı isteği tekrarlansın mı? Mevcut profiliniz No comment provided by engineer. + + Your group + Grubunuz + No comment provided by engineer. + Your preferences Tercihleriniz @@ -8360,6 +9559,11 @@ Bağlantı isteği tekrarlansın mı? Profiliniz **%@** paylaşılacaktır. No comment provided by engineer. + + Your profile is stored on your device and only shared with your contacts. + Profil sadece kişilerinle paylaşılacak. + No comment provided by engineer. + 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. @@ -8370,11 +9574,6 @@ Bağlantı isteği tekrarlansın mı? Profiliniz değiştirildi. Kaydederseniz, güncellenmiş profil tüm kişilerinize gönderilecektir. alert message - - Your profile, contacts and delivered messages are stored on your device. - Profiliniz, kişileriniz ve gönderilmiş mesajlar cihazınızda saklanır. - No comment provided by engineer. - Your random profile Rasgele profiliniz @@ -8387,6 +9586,7 @@ Bağlantı isteği tekrarlansın mı? Your servers + Sunucularınız No comment provided by engineer. @@ -8424,6 +9624,11 @@ Bağlantı isteği tekrarlansın mı? yukarı çıkın, ardından seçin: No comment provided by engineer. + + accepted %@ + kabul edildi %@ + rcv group event chat item + accepted call kabul edilen arama @@ -8431,8 +9636,14 @@ Bağlantı isteği tekrarlansın mı? accepted invitation + davetiye kabul edildi chat list item title + + accepted you + seni kabul etti + rcv group event chat item + admin yönetici @@ -8453,6 +9664,11 @@ Bağlantı isteği tekrarlansın mı? şifreleme kabul ediliyor… chat item text + + all + tümü + member criteria value + all members bütün üyeler @@ -8468,6 +9684,11 @@ Bağlantı isteği tekrarlansın mı? ve %lld diğer etkinlikler No comment provided by engineer. + + archived report + arşivlenmiş rapor + No comment provided by engineer. + attempts denemeler @@ -8506,7 +9727,8 @@ Bağlantı isteği tekrarlansın mı? blocked by admin yönetici tarafından engellendi - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8533,6 +9755,11 @@ Bağlantı isteği tekrarlansın mı? aranıyor… call status + + can't send messages + mesaj gönderilemiyor + No comment provided by engineer. + cancelled %@ %@ iptal edildi @@ -8583,11 +9810,6 @@ Bağlantı isteği tekrarlansın mı? bağlanıldı No comment provided by engineer. - - connected directly - doğrudan bağlandı - rcv group event chat item - connecting bağlanılıyor @@ -8638,6 +9860,16 @@ Bağlantı isteği tekrarlansın mı? %1$@ kişisi %2$@ olarak değişti profile update event chat item + + contact deleted + kişi silindi + No comment provided by engineer. + + + contact disabled + kişi devre dışı + No comment provided by engineer. + contact has e2e encryption kişi uçtan uca şifrelemeye sahiptir @@ -8648,6 +9880,16 @@ Bağlantı isteği tekrarlansın mı? kişi uçtan uca şifrelemeye sahip değildir No comment provided by engineer. + + contact not ready + kişi hazır değil + No comment provided by engineer. + + + contact should accept… + kişi kabul etmeli… + No comment provided by engineer. + creator oluşturan @@ -8676,7 +9918,8 @@ Bağlantı isteği tekrarlansın mı? default (%@) varsayılan (%@) - pref value + delete after time +pref value default (no) @@ -8803,18 +10046,9 @@ Bağlantı isteği tekrarlansın mı? hata No comment provided by engineer. - - event happened - etkinlik yaşandı - No comment provided by engineer. - expired - Süresi dolmuş - No comment provided by engineer. - - - for better metadata privacy. + süresi dolmuş No comment provided by engineer. @@ -8822,11 +10056,21 @@ Bağlantı isteği tekrarlansın mı? iletildi No comment provided by engineer. + + group + grup + shown on group welcome message + group deleted grup silindi No comment provided by engineer. + + group is deleted + grup silindi + No comment provided by engineer. + group profile updated grup profili güncellendi @@ -8922,11 +10166,6 @@ Bağlantı isteği tekrarlansın mı? italik No comment provided by engineer. - - join as %@ - %@ olarak katıl - No comment provided by engineer. - left ayrıldı @@ -8952,6 +10191,11 @@ Bağlantı isteği tekrarlansın mı? bağlanıldı rcv group event chat item + + member has old version + üye eski sürümde + No comment provided by engineer. + message mesaj @@ -8982,20 +10226,20 @@ Bağlantı isteği tekrarlansın mı? %@ tarafından yönetilmekte marked deleted chat item preview text + + moderator + moderatör + member role + months aylar time unit - - mute - Sessiz - No comment provided by engineer. - never asla - No comment provided by engineer. + delete after time new message @@ -9012,11 +10256,20 @@ Bağlantı isteği tekrarlansın mı? uçtan uca şifreleme yok No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text metin yok copied message info in history + + not synchronized + senkronize edilmedi + No comment provided by engineer. + observer gözlemci @@ -9026,8 +10279,9 @@ Bağlantı isteği tekrarlansın mı? off kapalı enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9069,6 +10323,21 @@ Bağlantı isteği tekrarlansın mı? eşler arası No comment provided by engineer. + + pending + beklemede + No comment provided by engineer. + + + pending approval + onay bekliyor + No comment provided by engineer. + + + pending review + inceleme bekliyor + No comment provided by engineer. + quantum resistant e2e encryption kuantuma dayanıklı e2e şifreleme @@ -9084,6 +10353,11 @@ Bağlantı isteği tekrarlansın mı? onaylama alındı… No comment provided by engineer. + + rejected + reddedildi + No comment provided by engineer. + rejected call geri çevrilmiş çağrı @@ -9104,6 +10378,11 @@ Bağlantı isteği tekrarlansın mı? kişi adresi silindi profile update event chat item + + removed from group + gruptan çıkarıldı + No comment provided by engineer. + removed profile picture profil fotoğrafı silindi @@ -9114,10 +10393,41 @@ Bağlantı isteği tekrarlansın mı? sen kaldırıldın rcv group event chat item + + request is sent + istek gönderildi + No comment provided by engineer. + + + request to join rejected + katılma isteği reddedildi + No comment provided by engineer. + + + requested connection + istenilen bağlantı + rcv group event chat item + + + requested connection from group %@ + %@ grubundan bağlantı isteği + rcv direct event chat item + requested to connect + bağlanma isteği gönderildi chat list item title + + review + incele + No comment provided by engineer. + + + reviewed by admins + yöneticiler tarafından incelendi + No comment provided by engineer. + saved kaydedildi @@ -9153,11 +10463,6 @@ Bağlantı isteği tekrarlansın mı? güvenlik kodu değiştirildi chat item text - - send direct message - doğrudan mesaj gönder - No comment provided by engineer. - server queue info: %1$@ @@ -9217,11 +10522,6 @@ son alınan msj: %2$@ bilinmeyen durum No comment provided by engineer. - - unmute - susturmayı kaldır - No comment provided by engineer. - unprotected korumasız @@ -9312,10 +10612,10 @@ son alınan msj: %2$@ sen No comment provided by engineer. - - you are invited to group - gruba davet edildiniz - No comment provided by engineer. + + you accepted this member + bu üyeyi kabul ettiniz + snd group event chat item you are observer @@ -9386,7 +10686,7 @@ son alınan msj: %2$@
- +
@@ -9423,7 +10723,7 @@ son alınan msj: %2$@
- +
@@ -9445,34 +10745,39 @@ son alınan msj: %2$@
- +
%d new events + %d yeni olaylar + notification body + + + From %d chat(s) + %d 'dan sohbetler notification body From: %@ + Şuradan: %@@ notification body New events + Yeni etkinlikler notification New messages + Yeni mesajlar notification - - New messages in %d chats - notification body -
- +
@@ -9494,7 +10799,7 @@ son alınan msj: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json index 6f74640a6b..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 d7dcc58dcd..7980685349 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -2,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (можна скопіювати) @@ -202,6 +190,11 @@ %d сек time interval + + %d seconds(s) + %d секунд(и) + delete after time + %d skipped message(s) %d пропущено повідомлення(ь) @@ -272,11 +265,6 @@ %lld нові мови інтерфейсу No comment provided by engineer. - - %lld second(s) - %lld секунд(и) - No comment provided by engineer. - %lld seconds %lld секунд @@ -327,11 +315,6 @@ %u повідомлень пропущено. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (новий) @@ -342,11 +325,6 @@ (цей пристрій v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Додати контакт**: створити нове посилання-запрошення. @@ -412,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). @@ -453,11 +426,6 @@ - історія редагування. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -471,7 +439,8 @@ 1 day 1 день - time interval + delete after time +time interval 1 hour @@ -486,12 +455,19 @@ 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 @@ -518,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> @@ -579,6 +550,7 @@ About operators + Про операторів No comment provided by engineer. @@ -590,8 +562,19 @@ Accept Прийняти accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + Прийняти як учасника + alert action + + + Accept as observer + Прийняти як спостерігача + alert action Accept conditions @@ -603,6 +586,11 @@ Прийняти запит на підключення? No comment provided by engineer. + + Accept contact request + Прийняти запит на контакт + alert title + Accept contact request from %@? Прийняти запит на контакт від %@? @@ -611,8 +599,13 @@ Accept incognito Прийняти інкогніто - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + Прийняти учасника + alert title Accepted conditions @@ -629,6 +622,11 @@ Помилки підтвердження No comment provided by engineer. + + Active + Активний + token status text + Active connections Активні з'єднання @@ -641,8 +639,19 @@ Add friends + Додайте друзів No comment provided by engineer. + + Add list + Додати список + No comment provided by engineer. + + + Add message + Додати повідомлення + placeholder for sending contact request + Add profile Додати профіль @@ -660,6 +669,7 @@ Add team members + Додайте учасників команди No comment provided by engineer. @@ -667,6 +677,11 @@ Додати до іншого пристрою No comment provided by engineer. + + Add to list + Додати до списку + No comment provided by engineer. + Add welcome message Додати вітальне повідомлення @@ -674,6 +689,7 @@ Add your team members to the conversations. + Додайте членів своєї команди до розмов. No comment provided by engineer. @@ -741,6 +757,11 @@ Додаткові налаштування No comment provided by engineer. + + All + Всі + No comment provided by engineer. + All app data is deleted. Всі дані програми видаляються. @@ -751,6 +772,11 @@ Всі чати та повідомлення будуть видалені - це неможливо скасувати! 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. Всі дані стираються при введенні. @@ -791,6 +817,16 @@ Всі профілі 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. Всі ваші контакти залишаться на зв'язку. @@ -831,6 +867,10 @@ Дозволити пониження версії No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години) @@ -866,6 +906,11 @@ Дозволяє безповоротно видаляти надіслані повідомлення. (24 години) No comment provided by engineer. + + Allow to report messsages to moderators. + Дозволити надсилати скаргу на повідомлення модераторам. + No comment provided by engineer. + Allow to send SimpleX links. Дозволити надсилати посилання SimpleX. @@ -911,6 +956,10 @@ Дозвольте своїм контактам надсилати зникаючі повідомлення. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Дозвольте своїм контактам надсилати голосові повідомлення. @@ -924,12 +973,12 @@ Already connecting! Вже підключаємось! - No comment provided by engineer. + new chat sheet title Already joining the group! Вже приєднуємося до групи! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -946,6 +995,11 @@ Створюється порожній профіль чату з вказаним ім'ям, і додаток відкривається у звичайному режимі. No comment provided by engineer. + + Another reason + Інша причина + report reason + Answer call Відповісти на дзвінок @@ -971,6 +1025,11 @@ Додаток шифрує нові локальні файли (крім відео). No comment provided by engineer. + + App group: + Група застосунків: + No comment provided by engineer. + App icon Іконка програми @@ -1016,6 +1075,21 @@ Звертатися до 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 Архівування та завантаження @@ -1026,6 +1100,21 @@ Архівуйте контакти, щоб поспілкуватися пізніше. 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 Архівні контакти @@ -1096,11 +1185,6 @@ Автоматичне прийняття зображень No comment provided by engineer. - - Auto-accept settings - Автоприйняття налаштувань - alert title - Back Назад @@ -1136,6 +1220,11 @@ Кращі групи No comment provided by engineer. + + Better groups performance + Краща продуктивність груп + No comment provided by engineer. + Better message dates. Кращі дати повідомлень. @@ -1156,6 +1245,11 @@ Кращі сповіщення No comment provided by engineer. + + Better privacy and security + Краща конфіденційність і безпека + No comment provided by engineer. + Better security ✅ Краща безпека ✅ @@ -1166,6 +1260,16 @@ Покращений користувацький досвід No comment provided by engineer. + + Bio + Біо + No comment provided by engineer. + + + Bio too large + Біографія занадто велика + alert title + Black Чорний @@ -1216,6 +1320,10 @@ Розмиття медіа No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Реакції на повідомлення можете додавати як ви, так і ваш контакт. @@ -1236,6 +1344,10 @@ Ви і ваш контакт можете надсилати зникаючі повідомлення. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Надсилати голосові повідомлення можете як ви, так і ваш контакт. @@ -1248,10 +1360,22 @@ Business address + Адреса підприємства No comment provided by engineer. Business chats + Ділові чати + No comment provided by engineer. + + + Business connection + Бізнес-зв'язок + No comment provided by engineer. + + + Businesses + Бізнеси No comment provided by engineer. @@ -1259,6 +1383,15 @@ Через профіль чату (за замовчуванням) або [за з'єднанням](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! Дзвінок вже закінчився! @@ -1289,6 +1422,11 @@ Не вдається зателефонувати користувачеві No comment provided by engineer. + + Can't change profile + Не вдається змінити профіль + alert title + Can't invite contact! Не вдається запросити контакт! @@ -1308,7 +1446,8 @@ Cancel Скасувати alert action - alert button +alert button +new chat action Cancel migration @@ -1345,6 +1484,11 @@ Зміна No comment provided by engineer. + + Change automatic message deletion? + Змінити автоматичне видалення повідомлень? + alert title + Change chat profiles Зміна профілів користувачів @@ -1367,7 +1511,7 @@ Change passcode - Змінити пароль + Змінити код доступу authentication reason @@ -1394,19 +1538,22 @@ Change self-destruct passcode Змінити пароль самознищення authentication reason - set passcode view +set passcode view Chat + Чат No comment provided by engineer. Chat already exists + Чат вже існує No comment provided by engineer. Chat already exists! - No comment provided by engineer. + Чат вже існує! + new chat sheet title Chat colors @@ -1485,10 +1632,27 @@ Chat will be deleted for all members - this cannot be undone! + Чат буде видалено для всіх учасників - цю дію неможливо скасувати! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + Чат буде видалено для вас - цю дію неможливо скасувати! + No comment provided by engineer. + + + Chat with admins + Чат з адміністраторами + chat toolbar + + + Chat with member + Чат з учасником + No comment provided by engineer. + + + Chat with members before they join. + Спілкуйтеся з учасниками до того, як вони приєднаються. No comment provided by engineer. @@ -1496,6 +1660,11 @@ Чати No comment provided by engineer. + + Chats with members + Чати з учасниками + No comment provided by engineer. + Check messages every 20 min. Перевіряйте повідомлення кожні 20 хв. @@ -1561,6 +1730,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? Чисті приватні нотатки? @@ -1581,6 +1760,11 @@ Колірний режим No comment provided by engineer. + + Community guidelines violation + Порушення правил спільноти + report reason + Compare file Порівняти файл @@ -1614,17 +1798,7 @@ Conditions of use Умови використання - No comment provided by engineer. - - - Conditions will be accepted for enabled operators after 30 days. - Умови будуть прийняті для ввімкнених операторів через 30 днів. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Умови приймаються для оператора(ів): **%@**. - No comment provided by engineer. + alert button Conditions will be accepted for the operator(s): **%@**. @@ -1646,6 +1820,11 @@ Налаштування серверів ICE No comment provided by engineer. + + Configure server operators + Налаштувати операторів сервера + No comment provided by engineer. + Confirm Підтвердити @@ -1696,6 +1875,11 @@ Підтвердити завантаження No comment provided by engineer. + + Confirmed + Підтверджений + token status text + Connect Підключіться @@ -1706,9 +1890,9 @@ Підключення автоматично No comment provided by engineer. - - Connect incognito - Підключайтеся інкогніто + + Connect faster! 🚀 + Підключайтеся швидше! 🚀 No comment provided by engineer. @@ -1721,44 +1905,39 @@ Швидше спілкуйтеся з друзями. No comment provided by engineer. - - Connect to yourself? - З'єднатися з самим собою? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - З'єднатися з самим собою? + З'єднатися з самим собою? Це ваша власна SimpleX-адреса! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - Підключитися до себе? + Підключитися до себе? Це ваше власне одноразове посилання! - No comment provided by engineer. + new chat sheet title Connect via contact address Підключіться за контактною адресою - No comment provided by engineer. + new chat sheet title Connect via link Підключіться за посиланням - No comment provided by engineer. + new chat sheet title Connect via one-time link Під'єднатися за одноразовим посиланням - No comment provided by engineer. + new chat sheet title Connect with %@ Підключитися до %@ - No comment provided by engineer. + new chat action Connected @@ -1815,16 +1994,33 @@ This is your own one-time link! Стан з'єднання та серверів. No comment provided by engineer. + + Connection blocked + Підключення заблоковано + No comment provided by engineer. + Connection error Помилка підключення - No comment provided by engineer. + alert title Connection error (AUTH) Помилка підключення (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 Сповіщення про підключення @@ -1835,6 +2031,11 @@ This is your own one-time link! Запит на підключення відправлено! No comment provided by engineer. + + Connection requires encryption renegotiation. + Підключення вимагає повторного узгодження шифрування. + No comment provided by engineer. + Connection security Безпека з'єднання @@ -1848,7 +2049,7 @@ This is your own one-time link! Connection timeout Тайм-аут з'єднання - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1900,6 +2101,10 @@ This is your own one-time link! Налаштування контактів No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Контакт буде видалено - це неможливо скасувати! @@ -1915,6 +2120,11 @@ This is your own one-time link! Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути. No comment provided by engineer. + + Content violates conditions of use + Вміст порушує умови використання + blocking reason + Continue Продовжуйте @@ -1990,6 +2200,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/). 💻 Створіть новий профіль у [desktop app](https://simplex.chat/downloads/). 💻 @@ -2005,9 +2220,9 @@ This is your own one-time link! Створити чергу server test step - - Create secret group - Створити секретну групу + + Create your address + Створіть свою адресу No comment provided by engineer. @@ -2207,8 +2422,7 @@ This is your own one-time link! Delete Видалити alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2247,6 +2461,12 @@ This is your own one-time link! Delete chat + Видалити чат + No comment provided by engineer. + + + Delete chat messages from your device. + Видалити повідомлення чату з вашого пристрою. No comment provided by engineer. @@ -2259,8 +2479,14 @@ This is your own one-time link! Видалити профіль чату? No comment provided by engineer. + + Delete chat with member? + Видалити чат з учасником? + alert title + Delete chat? + Видалити чат? No comment provided by engineer. @@ -2338,6 +2564,11 @@ This is your own one-time link! Видалити посилання? No comment provided by engineer. + + Delete list? + Видалити список? + alert title + Delete member message? Видалити повідомлення учасника? @@ -2351,7 +2582,7 @@ This is your own one-time link! Delete messages Видалити повідомлення - No comment provided by engineer. + alert button Delete messages after @@ -2388,6 +2619,11 @@ This is your own one-time link! Видалити чергу server test step + + Delete report + Видалити скаргу + No comment provided by engineer. + Delete up to 20 messages at once. Видаляйте до 20 повідомлень одночасно. @@ -2443,11 +2679,20 @@ This is your own one-time link! Квитанції про доставку! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Опис No comment provided by engineer. + + Description too large + Опис занадто великий + alert title + Desktop address Адреса робочого столу @@ -2530,6 +2775,7 @@ This is your own one-time link! Direct messages between members are prohibited in this chat. + У цьому чаті заборонені прямі повідомлення між учасниками. No comment provided by engineer. @@ -2547,6 +2793,16 @@ This is your own one-time link! Вимкнути SimpleX Lock authentication reason + + Disable automatic message deletion? + Вимкнути автоматичне видалення повідомлень? + alert title + + + Disable delete messages + Вимкнути видалення повідомлень + alert button + Disable for all Вимкнути для всіх @@ -2637,6 +2893,11 @@ This is your own one-time link! Не використовуйте облікові дані з проксі. No comment provided by engineer. + + Documents: + Документи: + No comment provided by engineer. + Don't create address Не створювати адресу @@ -2647,9 +2908,19 @@ This is your own one-time link! Не вмикати No comment provided by engineer. + + Don't miss important messages. + Не пропускайте важливі повідомлення. + No comment provided by engineer. + Don't show again Більше не показувати + alert action + + + Done + Готово No comment provided by engineer. @@ -2661,7 +2932,7 @@ This is your own one-time link! Download Завантажити alert button - chat item action +chat item action Download errors @@ -2728,6 +2999,11 @@ This is your own one-time link! Редагування профілю групи No comment provided by engineer. + + Empty message! + Порожнє повідомлення! + No comment provided by engineer. + Enable Увімкнути @@ -2738,9 +3014,9 @@ This is your own one-time link! Увімкнути (зберегти перевизначення) No comment provided by engineer. - - Enable Flux - Увімкнути Flux + + Enable Flux in Network & servers settings for better metadata privacy. + Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих. No comment provided by engineer. @@ -2756,13 +3032,18 @@ This is your own one-time link! Enable automatic message deletion? Увімкнути автоматичне видалення повідомлень? - No comment provided by engineer. + alert title Enable camera access Увімкніть доступ до камери No comment provided by engineer. + + Enable disappearing messages by default. + Увімкнути зникаючі повідомлення за замовчуванням. + No comment provided by engineer. + Enable for all Увімкнути для всіх @@ -2883,6 +3164,11 @@ This is your own one-time link! Повторне узгодження шифрування не вдалося. No comment provided by engineer. + + Encryption renegotiation in progress. + Виконується повторне узгодження шифрування. + No comment provided by engineer. + Enter Passcode Введіть пароль @@ -2958,6 +3244,11 @@ This is your own one-time link! Помилка при прийнятті запиту на контакт No comment provided by engineer. + + Error accepting member + Помилка при прийомі учасника + alert title + Error adding member(s) Помилка додавання користувача(ів) @@ -2968,11 +3259,21 @@ This is your own one-time link! Помилка додавання сервера alert title + + Error adding short link + Помилка додавання короткого посилання + No comment provided by engineer. + Error changing address Помилка зміни адреси No comment provided by engineer. + + Error changing chat profile + Помилка зміни профілю чату + alert title + Error changing connection profile Помилка при зміні профілю з'єднання @@ -2986,17 +3287,26 @@ This is your own one-time link! Error changing setting Помилка зміни налаштування - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -3013,6 +3323,11 @@ This is your own one-time link! Помилка створення посилання на групу No comment provided by engineer. + + Error creating list + Помилка при створенні списку + alert title + Error creating member contact Помилка при створенні контакту користувача @@ -3028,20 +3343,30 @@ This is your own one-time link! Помилка створення профілю! No comment provided by engineer. + + Error creating report + Помилка при створенні скарги + No comment provided by engineer. + Error decrypting file Помилка розшифрування файлу No comment provided by engineer. + + Error deleting chat + Помилка при видаленні чату з учасником + alert title + Error deleting chat database Помилка видалення бази даних чату - No comment provided by engineer. + alert title Error deleting chat! Помилка видалення чату! - No comment provided by engineer. + alert title Error deleting connection @@ -3051,12 +3376,12 @@ This is your own one-time link! Error deleting database Помилка видалення бази даних - No comment provided by engineer. + alert title Error deleting old database Помилка видалення старої бази даних - No comment provided by engineer. + alert title Error deleting token @@ -3091,7 +3416,7 @@ This is your own one-time link! Error exporting chat database Помилка експорту бази даних чату - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3101,7 +3426,7 @@ This is your own one-time link! Error importing chat database Помилка імпорту бази даних чату - No comment provided by engineer. + alert title Error joining group @@ -3123,6 +3448,11 @@ This is your own one-time link! Помилка відкриття чату No comment provided by engineer. + + Error opening group + Помилка відкриття групи + No comment provided by engineer. + Error receiving file Помилка отримання файлу @@ -3138,10 +3468,25 @@ This is your own one-time link! Помилка перепідключення серверів No comment provided by engineer. + + Error registering for notifications + Помилка під час реєстрації для отримання сповіщень + alert title + + + Error rejecting contact request + Помилка відхилення запиту на контакт + alert title + Error removing member Помилка видалення учасника - No comment provided by engineer. + alert title + + + Error reordering lists + Помилка при переупорядкуванні списків + alert title Error resetting statistics @@ -3153,6 +3498,11 @@ This is your own one-time link! Помилка збереження серверів ICE No comment provided by engineer. + + Error saving chat list + Помилка під час збереження списку чатів + alert title + Error saving group profile Помилка збереження профілю групи @@ -3203,6 +3553,10 @@ This is your own one-time link! Помилка надсилання повідомлення No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Помилка встановлення підтвердження доставлення! @@ -3221,7 +3575,7 @@ This is your own one-time link! Error switching profile Помилка перемикання профілю - No comment provided by engineer. + alert title Error switching profile! @@ -3233,6 +3587,11 @@ This is your own one-time link! Помилка синхронізації з'єднання No comment provided by engineer. + + Error testing server connection + Помилка під час перевірки з'єднання з сервером + No comment provided by engineer. + Error updating group link Помилка оновлення посилання на групу @@ -3276,7 +3635,13 @@ This is your own one-time link! Error: %@ Помилка: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3313,6 +3678,11 @@ This is your own one-time link! Розгорнути chat item action + + Expired + Термін дії закінчився + token status text + Export database Експорт бази даних @@ -3353,20 +3723,35 @@ This is your own one-time link! Швидко і без очікування, поки відправник буде онлайн! 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 Помилка файлу - No comment provided by engineer. + file error alert title File errors: @@ -3375,6 +3760,13 @@ This is your own one-time link! %@ alert message + + File is blocked by server operator: +%@. + Файл заблоковано оператором сервера: +%@. + file error text + File not found - most likely file was deleted or cancelled. Файл не знайдено - найімовірніше, файл було видалено або скасовано. @@ -3430,6 +3822,10 @@ This is your own one-time link! Файли і медіа chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Файли та медіа в цій групі заборонені. @@ -3470,6 +3866,23 @@ This is your own one-time link! Швидше знаходьте чати No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Відбиток в адресі сервера не співпадає з сертифікатом. + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Виправити @@ -3500,6 +3913,11 @@ This is your own one-time link! Виправлення не підтримується учасником групи No comment provided by engineer. + + For all moderators + Для всіх модераторів + No comment provided by engineer. + For chat profile %@: Для профілю чату %@: @@ -3515,6 +3933,11 @@ This is your own one-time link! Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux. No comment provided by engineer. + + For me + Для мене + No comment provided by engineer. + For private routing Для приватної маршрутизації @@ -3571,9 +3994,9 @@ This is your own one-time link! No comment provided by engineer. - Forwarding server %@ failed to connect to destination server %@. Please try later. - Серверу переадресації %@ не вдалося з'єднатися з сервером призначення %@. Спробуйте пізніше. - No comment provided by engineer. + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + Серверу переадресації %1$@ не вдалося з'єднатися з сервером призначення %2$@. Спробуйте пізніше. + alert message Forwarding server address is incompatible with network settings: %@. @@ -3639,6 +4062,11 @@ Error: %2$@ GIF-файли та наклейки No comment provided by engineer. + + Get notified when mentioned. + Отримуйте сповіщення, коли вас згадують. + No comment provided by engineer. + Good afternoon! Доброго дня! @@ -3662,7 +4090,7 @@ Error: %2$@ Group already exists! Група вже існує! - No comment provided by engineer. + new chat sheet title Group display name @@ -3729,6 +4157,11 @@ Error: %2$@ Профіль групи зберігається на пристроях учасників, а не на серверах. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + Профіль групи було змінено. Якщо ви збережете його, оновлений профіль буде надіслано учасникам групи. + alert message + Group welcome message Привітальне повідомлення групи @@ -3744,11 +4177,21 @@ Error: %2$@ Група буде видалена для вас - це не може бути скасовано! 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 Приховано @@ -3809,6 +4252,11 @@ Error: %2$@ Як це захищає приватність No comment provided by engineer. + + How it works + Як це працює + alert button + How to Як зробити @@ -3951,6 +4399,16 @@ More improvements are coming soon! Звуки вхідного дзвінка No comment provided by engineer. + + Inappropriate content + Невідповідний вміст + report reason + + + Inappropriate profile + Невідповідний профіль + report reason + Incognito Інкогніто @@ -4043,6 +4501,31 @@ More improvements are coming soon! Кольори інтерфейсу 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-код @@ -4061,7 +4544,7 @@ More improvements are coming soon! Invalid link Невірне посилання - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4105,6 +4588,7 @@ More improvements are coming soon! Invite to chat + Запросити в чат No comment provided by engineer. @@ -4173,37 +4657,32 @@ More improvements are coming soon! Приєднуйтесь swipe action + + Join as %@ + приєднатися як %@ + No comment provided by engineer. + Join group Приєднуйтесь до групи - No comment provided by engineer. + new chat sheet title Join group conversations Приєднуйтесь до групових розмов No comment provided by engineer. - - Join group? - Приєднатися до групи? - No comment provided by engineer. - Join incognito Приєднуйтесь інкогніто No comment provided by engineer. - - Join with current profile - Приєднатися з поточним профілем - No comment provided by engineer. - Join your group? This is your link for group %@! Приєднатися до групи? Це ваше посилання на групу %@! - No comment provided by engineer. + new chat action Joining group @@ -4230,6 +4709,11 @@ This is your link for group %@! Зберігати невикористані запрошення? alert title + + Keep your chats clean + Підтримуйте чистоту в чатах + No comment provided by engineer. + Keep your connections Зберігайте свої зв'язки @@ -4267,10 +4751,12 @@ This is your link for group %@! Leave chat + Вийти з чату No comment provided by engineer. Leave chat? + Залишити чат? No comment provided by engineer. @@ -4283,6 +4769,11 @@ This is your link for group %@! Покинути групу? No comment provided by engineer. + + Less traffic on mobile networks. + Менше трафіку в мобільних мережах. + No comment provided by engineer. + Let's talk in SimpleX Chat Поговоримо в чаті SimpleX @@ -4313,6 +4804,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! Живе повідомлення! @@ -4323,6 +4829,11 @@ This is your link for group %@! Живі повідомлення No comment provided by engineer. + + Loading profile… + Завантаження профілю… + in progress text + Local name Місцева назва @@ -4398,13 +4909,32 @@ This is your link for group %@! Учасник No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + Прийом членів + No comment provided by engineer. + Member inactive Користувач неактивний item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + Member reports + Повідомлення учасників + chat feature + Member role will be changed to "%@". All chat members will be notified. + Роль учасника буде змінено на "%@". Усі учасники чату отримають сповіщення. No comment provided by engineer. @@ -4419,6 +4949,7 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! + Учасника буде видалено з чату – це неможливо скасувати! No comment provided by engineer. @@ -4426,6 +4957,11 @@ This is your link for group %@! Учасник буде видалений з групи - це неможливо скасувати! No comment provided by engineer. + + Member will join the group, accept member? + Учасник приєднається до групи, прийняти учасника? + alert message + Members can add message reactions. Учасники групи можуть додавати реакції на повідомлення. @@ -4436,6 +4972,11 @@ This is your link for group %@! Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години) No comment provided by engineer. + + Members can report messsages to moderators. + Учасники можуть повідомляти повідомлення модераторам. + No comment provided by engineer. + Members can send SimpleX links. Учасники групи можуть надсилати посилання SimpleX. @@ -4461,6 +5002,11 @@ This is your link for group %@! Учасники групи можуть надсилати голосові повідомлення. No comment provided by engineer. + + Mention members 👋 + Згадуйте учасників 👋 + No comment provided by engineer. + Menus Меню @@ -4491,6 +5037,11 @@ This is your link for group %@! Повідомлення переслано item status text + + Message instantly once you tap Connect. + Миттєве повідомлення, щойно ви натиснете "Підключитися". + No comment provided by engineer. + Message may be delivered later if member becomes active. Повідомлення може бути доставлене пізніше, якщо користувач стане активним. @@ -4566,11 +5117,21 @@ This is your link for group %@! Повідомлення та файли No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + Повідомлення захищені **наскрізним шифруванням**. + No comment provided by engineer. + Messages from %@ will be shown! Повідомлення від %@ будуть показані! No comment provided by engineer. + + Messages in this chat will never be deleted. + Повідомлення в цьому чаті ніколи не будуть видалені. + alert message + Messages received Отримані повідомлення @@ -4671,6 +5232,11 @@ This is your link for group %@! Модерується за: %@ copied message info + + More + Більше + swipe action + More improvements are coming soon! Незабаром буде ще більше покращень! @@ -4699,7 +5265,12 @@ This is your link for group %@! Mute Вимкнути звук - swipe action + notification label action + + + Mute all + Вимкнути звук для всіх + notification label action Muted when inactive! @@ -4749,7 +5320,12 @@ This is your link for group %@! Network status Стан мережі - No comment provided by engineer. + alert title + + + New + Новий + token status text New Passcode @@ -4801,6 +5377,11 @@ This is your link for group %@! Нові події notification + + New group role: Moderator + Нова роль у групі: Модератор + No comment provided by engineer. + New in %@ Нове в %@ @@ -4816,6 +5397,11 @@ This is your link for group %@! Нова роль учасника No comment provided by engineer. + + New member wants to join the group. + Новий учасник хоче приєднатися до групи. + rcv group event chat item + New message Нове повідомлення @@ -4841,6 +5427,26 @@ 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 chats with members + Ніяких чатів з учасниками + No comment provided by engineer. + No contacts selected Не вибрано жодного контакту @@ -4891,6 +5497,11 @@ This is your link for group %@! Ніяких медіа та файлових серверів. servers error + + No message + Немає повідомлення + No comment provided by engineer. + No message servers. Ніяких серверів повідомлень. @@ -4916,6 +5527,11 @@ This is your link for group %@! Немає дозволу на запис голосового повідомлення No comment provided by engineer. + + No private routing session + Немає приватного сеансу маршрутизації + alert title + No push server Локально @@ -4946,6 +5562,16 @@ This is your link for group %@! Немає серверів для надсилання файлів. servers error + + No token! + Немає токена! + alert title + + + No unread chats + Немає непрочитаних чатів + No comment provided by engineer. + No user identifiers. Ніяких ідентифікаторів користувачів. @@ -4956,6 +5582,11 @@ This is your link for group %@! Не сумісні! No comment provided by engineer. + + Notes + Нотатки + No comment provided by engineer. + Nothing selected Нічого не вибрано @@ -4976,11 +5607,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. @@ -5003,7 +5644,9 @@ This is your link for group %@! Ok Гаразд - alert button + alert action +alert button +new chat action Old database @@ -5036,6 +5679,7 @@ Requires compatible VPN. Only chat owners can change preferences. + Лише власники чату можуть змінювати налаштування. No comment provided by engineer. @@ -5063,6 +5707,16 @@ Requires compatible VPN. Тільки власники груп можуть вмикати голосові повідомлення. 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. Тільки ви можете додавати реакції на повідомлення. @@ -5083,6 +5737,10 @@ Requires compatible VPN. Тільки ви можете надсилати зникаючі повідомлення. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Тільки ви можете надсилати голосові повідомлення. @@ -5108,6 +5766,10 @@ Requires compatible VPN. Тільки ваш контакт може надсилати зникаючі повідомлення. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Тільки ваш контакт може надсилати голосові повідомлення. @@ -5116,7 +5778,7 @@ Requires compatible VPN. Open Відкрито - No comment provided by engineer. + alert action Open Settings @@ -5131,28 +5793,70 @@ Requires compatible VPN. Open chat Відкритий чат - No comment provided by engineer. + new chat action Open chat console Відкрийте консоль чату authentication reason + + Open clean link + alert action + Open conditions Відкриті умови No comment provided by engineer. + + Open full link + alert action + Open group Відкрита група - No comment provided by engineer. + new chat action + + + Open link? + Відкрите посилання? + alert title Open migration to another device Відкрита міграція на інший пристрій authentication reason + + Open new chat + Відкрити новий чат + new chat action + + + Open new group + Відкрити нову групу + new chat action + + + Open to accept + Відкрити для прийняття + No comment provided by engineer. + + + Open to connect + Відкрито для підключення + No comment provided by engineer. + + + Open to join + Відкрито для приєднання + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… Відкриваємо програму… @@ -5170,6 +5874,7 @@ Requires compatible VPN. Or import archive file + Або імпортуйте архівний файл No comment provided by engineer. @@ -5197,6 +5902,11 @@ Requires compatible VPN. Або поділитися приватно No comment provided by engineer. + + Organize chats into lists + Організовуйте чати в списки + No comment provided by engineer. + Other Інше @@ -5254,11 +5964,6 @@ Requires compatible VPN. Показати пароль No comment provided by engineer. - - Past member %@ - Колишній учасник %@ - past/unknown group member - Paste desktop address Вставте адресу робочого столу @@ -5329,7 +6034,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. Будь ласка, перевірте підключення до мережі за допомогою %@ і спробуйте ще раз. - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5388,6 +6093,26 @@ Error: %@ Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите. No comment provided by engineer. + + Please try to disable and re-enable notfications. + Будь ласка, спробуйте вимкнути та знову увімкнути сповіщення. + token info + + + Please wait for group moderators to review your request to join the group. + Будь ласка, зачекайте, поки модератори групи розглянуть ваш запит на приєднання до групи. + snd group event chat item + + + Please wait for token activation to complete. + Будь ласка, дочекайтеся завершення активації токену. + token info + + + Please wait for token to be registered. + Будь ласка, зачекайте, поки токен буде зареєстровано. + token info + Polish interface Польський інтерфейс @@ -5398,11 +6123,6 @@ Error: %@ Порт No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Можливо, в адресі сервера неправильно вказано відбиток сертифіката - server test error - Preserve the last message draft, with attachments. Зберегти чернетку останнього повідомлення з вкладеннями. @@ -5435,6 +6155,12 @@ Error: %@ Privacy for your customers. + Конфіденційність для ваших клієнтів. + No comment provided by engineer. + + + Privacy policy and conditions of use. + Політика конфіденційності та умови використання. No comment provided by engineer. @@ -5442,11 +6168,21 @@ Error: %@ Конфіденційність переглянута 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 Маршрутизація приватних повідомлень @@ -5470,7 +6206,12 @@ Error: %@ Private routing error Помилка приватної маршрутизації - No comment provided by engineer. + alert title + + + Private routing timeout + Тайм-аут приватної маршрутизації + alert title Profile and server connections @@ -5522,6 +6263,11 @@ Error: %@ Заборонити реакції на повідомлення. No comment provided by engineer. + + Prohibit reporting messages to moderators. + Заборонити повідомлення модераторам. + No comment provided by engineer. + Prohibit sending SimpleX links. Заборонити надсилання посилань SimpleX. @@ -5569,6 +6315,11 @@ Enable in *Network & servers* settings. Захистіть свої профілі чату паролем! No comment provided by engineer. + + Protocol background timeout + Фоновий тайм-аут протоколу + No comment provided by engineer. + Protocol timeout Тайм-аут протоколу @@ -5674,11 +6425,6 @@ Enable in *Network & servers* settings. Отримано за: %@ copied message info - - Received file event - Подія отримання файлу - notification - Received message Отримано повідомлення @@ -5779,11 +6525,27 @@ Enable in *Network & servers* settings. Зменшення використання акумулятора 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5793,7 +6555,12 @@ Enable in *Network & servers* settings. Reject contact request Відхилити запит на контакт - No comment provided by engineer. + alert title + + + Reject member? + Відхилити учасника? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5820,6 +6587,10 @@ Enable in *Network & servers* settings. Видалити зображення No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Видалити учасника @@ -5835,6 +6606,11 @@ Enable in *Network & servers* settings. Видалити парольну фразу з брелока? No comment provided by engineer. + + Removes messages and blocks members. + Видаляє повідомлення та блокує користувачів. + No comment provided by engineer. + Renegotiate Переузгодьте @@ -5850,11 +6626,6 @@ Enable in *Network & servers* settings. Переузгодьте шифрування? No comment provided by engineer. - - Repeat connection request? - Повторити запит на підключення? - No comment provided by engineer. - Repeat download Повторити завантаження @@ -5865,11 +6636,6 @@ Enable in *Network & servers* settings. Повторний імпорт No comment provided by engineer. - - Repeat join request? - Повторити запит на приєднання? - No comment provided by engineer. - Repeat upload Повторне завантаження @@ -5880,6 +6646,61 @@ Enable in *Network & servers* settings. Відповісти 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 sent to moderators + Повідомлення надіслано модераторам + alert title + + + 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 Потрібно @@ -5958,7 +6779,7 @@ Enable in *Network & servers* settings. Retry Спробуйте ще раз - No comment provided by engineer. + alert action Reveal @@ -5970,11 +6791,21 @@ Enable in *Network & servers* settings. Умови перегляду No comment provided by engineer. - - Review later - Перегляньте пізніше + + Review group members + Учасники групи оглядів No comment provided by engineer. + + Review members + Схвалювати учасників + admission stage + + + Review members before admitting ("knocking"). + Перевірка учасників перед тим, як їх прийняти («стукіт»). + admission stage description + Revoke Відкликати @@ -6024,13 +6855,23 @@ Enable in *Network & servers* settings. Save Зберегти alert button - chat item action +chat item action Save (and notify contacts) Зберегти (і повідомити контактам) alert button + + Save (and notify members) + Зберегти (і повідомити учасникам) + alert button + + + Save admission settings? + Зберегти налаштування входу? + alert title + Save and notify contact Зберегти та повідомити контакт @@ -6056,6 +6897,16 @@ Enable in *Network & servers* settings. Зберегти профіль групи No comment provided by engineer. + + Save group profile? + Зберегти профіль групи? + alert title + + + Save list + Зберегти список + No comment provided by engineer. + Save passphrase and open chat Збережіть пароль і відкрийте чат @@ -6246,6 +7097,11 @@ Enable in *Network & servers* settings. Надішліть повідомлення в реальному часі - воно буде оновлюватися для одержувача (одержувачів), поки ви його вводите No comment provided by engineer. + + Send contact request? + Надіслати запит на контакт? + No comment provided by engineer. + Send delivery receipts to Надсилання звітів про доставку @@ -6296,6 +7152,11 @@ Enable in *Network & servers* settings. Надсилати сповіщення No comment provided by engineer. + + Send private reports + Надсилайте приватні звіти + No comment provided by engineer. + Send questions and ideas Надсилайте запитання та ідеї @@ -6306,6 +7167,16 @@ Enable in *Network & servers* settings. Надіслати підтвердження No comment provided by engineer. + + Send request + Надіслати запит + No comment provided by engineer. + + + Send request without message + Надіслати запит без повідомлення + No comment provided by engineer. + Send them from gallery or custom keyboards. Надсилайте їх із галереї чи власних клавіатур. @@ -6316,6 +7187,11 @@ Enable in *Network & servers* settings. Надішліть до 100 останніх повідомлень новим користувачам. No comment provided by engineer. + + Send your private feedback to groups. + Надсилайте свої приватні відгуки до груп. + No comment provided by engineer. + Sender cancelled file transfer. Відправник скасував передачу файлу. @@ -6381,11 +7257,6 @@ Enable in *Network & servers* settings. Відправлено напряму No comment provided by engineer. - - Sent file event - Подія надісланого файлу - notification - Sent message Надіслано повідомлення @@ -6456,14 +7327,14 @@ Enable in *Network & servers* settings. Протокол сервера змінено. alert title - - Server requires authorization to create queues, check password - Сервер вимагає авторизації для створення черг, перевірте пароль + + Server requires authorization to create queues, check password. + Сервер вимагає авторизації для створення черг, перевірте пароль. server test error - - Server requires authorization to upload, check password - Сервер вимагає авторизації для завантаження, перевірте пароль + + Server requires authorization to upload, check password. + Сервер вимагає авторизації для завантаження, перевірте пароль. server test error @@ -6511,6 +7382,11 @@ Enable in *Network & servers* settings. Встановити 1 день No comment provided by engineer. + + Set chat name… + Назвати чат… + No comment provided by engineer. + Set contact name… Встановити ім'я контакту… @@ -6531,6 +7407,16 @@ Enable in *Network & servers* settings. Встановіть його замість аутентифікації системи. No comment provided by engineer. + + Set member admission + Встановити прийом учасників + No comment provided by engineer. + + + Set message expiration in chats. + Встановлюйте термін придатності повідомлень у чатах. + No comment provided by engineer. + Set passcode Встановити пароль @@ -6546,6 +7432,11 @@ Enable in *Network & servers* settings. Встановити ключову фразу для експорту No comment provided by engineer. + + Set profile bio and welcome message. + Налаштуйте біографію профілю та вітальне повідомлення. + No comment provided by engineer. + Set the message shown to new members! Налаштуйте повідомлення, яке показуватиметься новим користувачам! @@ -6575,7 +7466,7 @@ Enable in *Network & servers* settings. Share Поділіться alert action - chat item action +chat item action Share 1-time link @@ -6617,6 +7508,16 @@ Enable in *Network & servers* settings. Поділіться посиланням No comment provided by engineer. + + Share old address + Поділіться старою адресою + alert button + + + Share old link + Поділіться старим посиланням + alert button + Share profile Поділіться профілем @@ -6637,6 +7538,26 @@ Enable in *Network & servers* settings. Поділіться з контактами No comment provided by engineer. + + Share your address + Поділіться своєю адресою + No comment provided by engineer. + + + Short SimpleX address + Коротка адреса SimpleX + No comment provided by engineer. + + + Short description + Короткий опис + No comment provided by engineer. + + + Short link + Коротке посилання + No comment provided by engineer. + Show QR code Показати QR-код @@ -6694,6 +7615,7 @@ Enable in *Network & servers* settings. SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + SimpleX Chat і Flux уклали угоду про включення серверів, керованих Flux, у додаток. No comment provided by engineer. @@ -6736,6 +7658,16 @@ Enable in *Network & servers* settings. SimpleX адреса або одноразове посилання? No comment provided by engineer. + + SimpleX address settings + Автоприйняття налаштувань + alert title + + + SimpleX channel link + Посилання на канал SimpleX + simplex link type + SimpleX contact address Контактна адреса SimpleX @@ -6776,6 +7708,10 @@ Enable in *Network & servers* settings. Протоколи SimpleX, розглянуті Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Спрощений режим інкогніто @@ -6838,6 +7774,12 @@ Enable in *Network & servers* settings. Хтось notification title + + Spam + Спам + blocking reason +report reason + Square, circle, or anything in between. Квадрат, коло або щось середнє між ними. @@ -6923,6 +7865,11 @@ Enable in *Network & servers* settings. Зупинка чату No comment provided by engineer. + + Storage + Зберігання + No comment provided by engineer. + Strong Сильний @@ -6978,11 +7925,21 @@ Enable in *Network & servers* settings. TCP-з'єднання No comment provided by engineer. + + TCP connection bg timeout + Таймаут TCP-з'єднання bg + 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 @@ -7008,8 +7965,28 @@ Enable in *Network & servers* settings. Сфотографуйте No comment provided by engineer. + + Tap Connect to chat + Натисніть Підключитися до чату + No comment provided by engineer. + + + Tap Connect to send request + Натисніть Підключитися, щоб відправити запит + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. + Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше. + No comment provided by engineer. + + + Tap Join group + Натисніть Приєднатися до групи No comment provided by engineer. @@ -7050,13 +8027,18 @@ Enable in *Network & servers* settings. Temporary file error Тимчасова помилка файлу - No comment provided by engineer. + file error alert title Test failed at step %@. Тест завершився невдало на кроці %@. server test failure + + Test notifications + Тестові сповіщення + No comment provided by engineer. + Test server Тестовий сервер @@ -7094,6 +8076,11 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + Адреса буде короткою, і ваш профіль буде доступний за цією адресою. + alert message + The app can notify you when you receive messages or contact requests - please open settings to enable. Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію. @@ -7154,6 +8141,11 @@ It can happen because of some bug or when the connection is compromised.Хеш попереднього повідомлення відрізняється. No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + Посилання буде коротким, а профіль групи буде поширюватися за посиланням. + alert message + The message will be deleted for all members. Повідомлення буде видалено для всіх учасників. @@ -7179,21 +8171,11 @@ 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. - Профіль доступний лише вашим контактам. - No comment provided by engineer. - The same conditions will apply to operator **%@**. Такі ж умови діятимуть і для оператора **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - Такі ж умови будуть застосовуватися до оператора(ів): **%@**. - No comment provided by engineer. - The second preset operator in the app! Другий попередньо встановлений оператор у застосунку! @@ -7207,7 +8189,7 @@ It can happen because of some bug or when the connection is compromised. The sender will NOT be notified Відправник НЕ буде повідомлений - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7259,6 +8241,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. Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені. @@ -7294,14 +8281,9 @@ It can happen because of some bug or when the connection is compromised.Цієї групи більше не існує. No comment provided by engineer. - - This is your own SimpleX address! - Це ваша власна SimpleX-адреса! - No comment provided by engineer. - - - This is your own one-time 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. @@ -7309,11 +8291,25 @@ It can happen because of some bug or when the connection is compromised.Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі. 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + Час зникнення встановлюється тільки для нових контактів. + No comment provided by engineer. + Title Заголовок @@ -7396,11 +8392,20 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб відправити No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + Щоб використовувати інший профіль після спроби з'єднання, видаліть чат і скористайтеся посиланням знову. + alert message + To use the servers of **%@**, accept conditions of use. Щоб користуватися серверами **%@**, прийміть умови використання. @@ -7421,6 +8426,11 @@ You will be prompted to complete authentication before this feature is enabled.< Увімкніть інкогніто при підключенні. No comment provided by engineer. + + Token status: %@. + Статус токена: %@. + token status + Toolbar opacity Непрозорість панелі інструментів @@ -7441,15 +8451,9 @@ You will be prompted to complete authentication before this feature is enabled.< Транспортні сесії 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. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7586,13 +8590,18 @@ To connect, please ask your contact to create another connection link and check Unmute Увімкнути звук - swipe action + 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 останніх повідомлень. @@ -7618,16 +8627,51 @@ To connect, please ask your contact to create another connection link and check Оновити налаштування? No comment provided by engineer. + + Updated conditions + Оновлені умови + No comment provided by engineer. + Updating settings will re-connect the client to all servers. Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів. No comment provided by engineer. + + Upgrade + Оновлення + alert button + + + Upgrade address + Адреса оновлення + No comment provided by engineer. + + + Upgrade address? + Змінити адресу? + alert message + Upgrade and open chat Оновлення та відкритий чат No comment provided by engineer. + + Upgrade group link? + Оновити посилання на групу? + alert message + + + Upgrade link + Посилання для оновлення + No comment provided by engineer. + + + Upgrade your address + Поновіть свою адресу + No comment provided by engineer. + Upload errors Помилки завантаження @@ -7678,6 +8722,16 @@ To connect, please ask your contact to create another connection link and check Використовувати сервери 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. + Використовуйте TCP порт 443 лише для попередньо налаштованих серверів. + No comment provided by engineer. + Use chat Використовуйте чат @@ -7686,7 +8740,7 @@ To connect, please ask your contact to create another connection link and check Use current profile Використовувати поточний профіль - No comment provided by engineer. + new chat action Use for files @@ -7713,10 +8767,15 @@ To connect, please ask your contact to create another connection link and check Використовуйте інтерфейс виклику iOS No comment provided by engineer. + + Use incognito profile + Використовуйте профіль інкогніто + No comment provided by engineer. + Use new incognito profile Використовуйте новий профіль інкогніто - No comment provided by engineer. + new chat action Use only local notifications? @@ -7753,6 +8812,11 @@ To connect, please ask your contact to create another connection link and check Використовуйте додаток однією рукою. No comment provided by engineer. + + Use web port + Використовувати веб-порт + No comment provided by engineer. + User selection Вибір користувача @@ -7943,6 +9007,11 @@ To connect, please ask your contact to create another connection link and check Привітальне повідомлення занадто довге No comment provided by engineer. + + Welcome your contacts 👋 + Вітаємо ваші контакти 👋 + No comment provided by engineer. + What's new Що нового @@ -8060,17 +9129,18 @@ To connect, please ask your contact to create another connection link and check You are already connected with %@. + Ви вже підключені до %@. No comment provided by engineer. You are already connecting to %@. Ви вже з'єднані з %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Ви вже підключаєтеся до %@.Ви вже підключаєтеся за цим одноразовим посиланням! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8080,35 +9150,33 @@ To connect, please ask your contact to create another connection link and check 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. + new chat sheet message You are already joining the group via this link. Ви вже приєдналися до групи за цим посиланням. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Ви вже приєдналися до групи! Повторити запит на приєднання? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту. - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group Запрошуємо вас до групи No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. Не підключені до цих серверів. Для доставлення повідомлень до них використовується приватна маршрутизація. @@ -8124,11 +9192,6 @@ Repeat join request? Ви можете змінити його в налаштуваннях зовнішнього вигляду. No comment provided by engineer. - - You can configure operators in Network & servers settings. - Ви можете налаштувати операторів у налаштуваннях Мережі та серверів. - No comment provided by engineer. - You can configure servers via settings. Ви можете налаштувати сервери за допомогою налаштувань. @@ -8219,10 +9282,15 @@ Repeat join request? Ви можете переглянути посилання на запрошення ще раз у деталях підключення. alert message + + You can view your reports in Chat with admins. + Ви можете переглянути свої звіти у чаті з адміністраторами. + alert message + You can't send messages! Ви не можете надсилати повідомлення! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8234,17 +9302,12 @@ Repeat join request? Ви вирішуєте, хто може під'єднатися. No comment provided by engineer. - - You have already requested connection via this address! - Ви вже надсилали запит на підключення за цією адресою! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Ви вже надіслали запит на підключення! Повторити запит на підключення? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8301,6 +9364,16 @@ Repeat connection request? Ви надіслали запрошення до групи No comment provided by engineer. + + You should receive notifications. + Ви повинні отримувати сповіщення. + token info + + + You will be able to send messages **only after your request is accepted**. + Ви зможете надсилати повідомлення **тільки після того, як ваш запит буде прийнято**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше! @@ -8326,11 +9399,6 @@ Repeat connection request? Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі. No comment provided by engineer. - - You will connect to all group members. - Ви з'єднаєтеся з усіма учасниками групи. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні. @@ -8338,6 +9406,7 @@ Repeat connection request? You will stop receiving messages from this chat. Chat history will be preserved. + Ви більше не будете отримувати повідомлення з цього чату. Історія чату буде збережена. No comment provided by engineer. @@ -8365,16 +9434,16 @@ Repeat connection request? Ваші сервери 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 business contact + Ваш діловий контакт + No comment provided by engineer. + Your calls Твої дзвінки @@ -8400,11 +9469,21 @@ Repeat connection request? Ваші профілі чату No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Ваш чат було переміщено на %@, але при перенаправленні на профіль сталася несподівана помилка. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка. No comment provided by engineer. + + Your contact + Ваш контакт + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@). @@ -8435,6 +9514,11 @@ Repeat connection request? Ваш поточний профіль No comment provided by engineer. + + Your group + Ваша група + No comment provided by engineer. + Your preferences Ваші уподобання @@ -8455,6 +9539,11 @@ Repeat connection request? Ваш профіль **%@** буде опублікований. No comment provided by engineer. + + Your profile is stored on your device and only shared with your contacts. + Профіль доступний лише вашим контактам. + No comment provided by engineer. + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль. @@ -8465,11 +9554,6 @@ Repeat connection request? Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам. alert message - - Your profile, contacts and delivered messages are stored on your device. - Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої. - No comment provided by engineer. - Your random profile Ваш випадковий профіль @@ -8520,6 +9604,11 @@ Repeat connection request? вище, а потім обирайте: No comment provided by engineer. + + accepted %@ + прийнято %@ + rcv group event chat item + accepted call прийнято виклик @@ -8527,8 +9616,14 @@ Repeat connection request? accepted invitation + прийняте запрошення chat list item title + + accepted you + прийняв(ла) вас + rcv group event chat item + admin адмін @@ -8549,6 +9644,11 @@ Repeat connection request? узгодження шифрування… chat item text + + all + усі + member criteria value + all members всі учасники @@ -8564,6 +9664,11 @@ Repeat connection request? та %lld інших подій No comment provided by engineer. + + archived report + архівование повідомлення + No comment provided by engineer. + attempts спроби @@ -8602,7 +9707,8 @@ Repeat connection request? blocked by admin заблоковано адміністратором - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8629,6 +9735,11 @@ Repeat connection request? дзвоніть… call status + + can't send messages + не можна надсилати + No comment provided by engineer. + cancelled %@ скасовано %@ @@ -8679,11 +9790,6 @@ Repeat connection request? з'єднаний No comment provided by engineer. - - connected directly - з'єднані безпосередньо - rcv group event chat item - connecting з'єднання @@ -8734,6 +9840,16 @@ Repeat connection request? контакт %1$@ змінено на %2$@ profile update event chat item + + contact deleted + контакт видалено + No comment provided by engineer. + + + contact disabled + контакт вимкнено + No comment provided by engineer. + contact has e2e encryption контакт має шифрування e2e @@ -8744,6 +9860,16 @@ Repeat connection request? контакт не має шифрування e2e No comment provided by engineer. + + contact not ready + контакт не готовий + No comment provided by engineer. + + + contact should accept… + контакт повинен прийняти… + No comment provided by engineer. + creator творець @@ -8772,7 +9898,8 @@ Repeat connection request? default (%@) за замовчуванням (%@) - pref value + delete after time +pref value default (no) @@ -8899,31 +10026,31 @@ Repeat connection request? помилка No comment provided by engineer. - - event happened - відбулася подія - No comment provided by engineer. - expired закінчився No comment provided by engineer. - - for better metadata privacy. - для кращої конфіденційності метаданих. - No comment provided by engineer. - forwarded переслано No comment provided by engineer. + + group + група + shown on group welcome message + group deleted групу видалено No comment provided by engineer. + + group is deleted + групу видалено + No comment provided by engineer. + group profile updated оновлено профіль групи @@ -9019,11 +10146,6 @@ Repeat connection request? курсив No comment provided by engineer. - - join as %@ - приєднатися як %@ - No comment provided by engineer. - left ліворуч @@ -9049,6 +10171,11 @@ Repeat connection request? з'єднаний rcv group event chat item + + member has old version + учасник використовує застарілу версію + No comment provided by engineer. + message повідомлення @@ -9079,20 +10206,20 @@ Repeat connection request? модерується %@ marked deleted chat item preview text + + moderator + модератор + member role + months місяців time unit - - mute - приглушити - No comment provided by engineer. - never ніколи - No comment provided by engineer. + delete after time new message @@ -9109,11 +10236,20 @@ Repeat connection request? без шифрування e2e No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text без тексту copied message info in history + + not synchronized + не синхронізовано + No comment provided by engineer. + observer спостерігач @@ -9123,8 +10259,9 @@ Repeat connection request? off вимкнено enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9166,6 +10303,21 @@ Repeat connection request? одноранговий No comment provided by engineer. + + pending + очікує + No comment provided by engineer. + + + pending approval + очікує на схвалення + No comment provided by engineer. + + + pending review + очікує на схвалення + No comment provided by engineer. + quantum resistant e2e encryption квантово-стійке шифрування e2e @@ -9181,6 +10333,11 @@ Repeat connection request? отримали підтвердження… No comment provided by engineer. + + rejected + відхилено + No comment provided by engineer. + rejected call відхилений виклик @@ -9201,6 +10358,11 @@ Repeat connection request? видалено контактну адресу profile update event chat item + + removed from group + видалено з групи + No comment provided by engineer. + removed profile picture видалено зображення профілю @@ -9211,10 +10373,39 @@ Repeat connection request? прибрали вас rcv group event chat item + + request is sent + запит відправлено + No comment provided by engineer. + + + request to join rejected + запит на приєднання відхилено + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect + запит на підключення chat list item title + + review + перегляд + No comment provided by engineer. + + + reviewed by admins + схвалено адміністраторами + No comment provided by engineer. + saved збережено @@ -9250,11 +10441,6 @@ Repeat connection request? змінено код безпеки chat item text - - send direct message - надіслати пряме повідомлення - No comment provided by engineer. - server queue info: %1$@ @@ -9314,11 +10500,6 @@ last received msg: %2$@ невідомий статус No comment provided by engineer. - - unmute - увімкнути звук - No comment provided by engineer. - unprotected незахищені @@ -9409,10 +10590,10 @@ last received msg: %2$@ ти No comment provided by engineer. - - you are invited to group - вас запрошують до групи - No comment provided by engineer. + + you accepted this member + ви прийняли цього учасника + snd group event chat item you are observer @@ -9483,7 +10664,7 @@ last received msg: %2$@
- +
@@ -9520,7 +10701,7 @@ last received msg: %2$@
- +
@@ -9542,7 +10723,7 @@ last received msg: %2$@
- +
@@ -9550,6 +10731,11 @@ last received msg: %2$@ %d нових подій notification body + + From %d chat(s) + З %d чату(ів) + notification body + From: %@ Від: %@ @@ -9565,16 +10751,11 @@ last received msg: %2$@ Нові повідомлення notification - - New messages in %d chats - Нові повідомлення в чатах %d - notification body -
- +
@@ -9596,7 +10777,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json index 38238e7802..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 d6e548c6be..e1ce65b5ce 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,21 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (可复制) @@ -114,10 +102,12 @@ %@ server + 服务器 No comment provided by engineer. %@ servers + 服务器 No comment provided by engineer. @@ -132,6 +122,7 @@ %1$@, %2$@ + %1$@, %2$@ format for date separator in chat @@ -156,18 +147,22 @@ %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 @@ -177,6 +172,7 @@ %d messages not forwarded + 未转发 %d 条消息 alert title @@ -194,9 +190,14 @@ %d 秒 time interval + + %d seconds(s) + %d 秒 + delete after time + %d skipped message(s) - %d 跳过消息 + 跳过的 %d 条消息 integrity error chat item @@ -264,11 +265,6 @@ %lld 种新的界面语言 No comment provided by engineer. - - %lld second(s) - %lld 秒 - No comment provided by engineer. - %lld seconds %lld 秒 @@ -319,11 +315,6 @@ 已跳过 %u 条消息。 No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (新) @@ -334,11 +325,6 @@ (此设备 v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接. @@ -376,6 +362,7 @@ **Scan / Paste link**: to connect via a link you received. + **扫描/粘贴链接**:用您收到的链接连接。 No comment provided by engineer. @@ -403,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). @@ -444,11 +426,6 @@ - 编辑消息历史。 No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 秒 @@ -462,7 +439,8 @@ 1 day 1天 - time interval + delete after time +time interval 1 hour @@ -477,19 +455,28 @@ 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. @@ -507,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> @@ -568,6 +550,7 @@ About operators + 关于运营方 No comment provided by engineer. @@ -579,11 +562,21 @@ Accept 接受 accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +alert action +swipe action + + + Accept as member + alert action + + + Accept as observer + alert action Accept conditions + 接受条款 No comment provided by engineer. @@ -591,6 +584,10 @@ 接受联系人? No comment provided by engineer. + + Accept contact request + alert title + Accept contact request from %@? 接受来自 %@ 的联系人请求? @@ -599,11 +596,16 @@ Accept incognito 接受隐身聊天 - accept contact request via notification - swipe action + alert action +swipe action + + + Accept member + alert title Accepted conditions + 已接受的条款 No comment provided by engineer. @@ -616,6 +618,11 @@ 确认错误 No comment provided by engineer. + + Active + 活跃 + token status text + Active connections 活动连接 @@ -628,8 +635,18 @@ Add friends + 添加好友 No comment provided by engineer. + + Add list + 添加列表 + No comment provided by engineer. + + + Add message + placeholder for sending contact request + Add profile 添加个人资料 @@ -647,6 +664,7 @@ Add team members + 添加团队成员 No comment provided by engineer. @@ -654,6 +672,11 @@ 添加另一设备 No comment provided by engineer. + + Add to list + 添加到列表 + No comment provided by engineer. + Add welcome message 添加欢迎信息 @@ -661,14 +684,17 @@ 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. @@ -698,10 +724,12 @@ Address or 1-time link? + 地址还是一次性链接? No comment provided by engineer. Address settings + 地址设置 No comment provided by engineer. @@ -724,6 +752,11 @@ 高级设置 No comment provided by engineer. + + All + 全部 + No comment provided by engineer. + All app data is deleted. 已删除所有应用程序数据。 @@ -734,6 +767,11 @@ 所有聊天记录和消息将被删除——这一行为无法撤销! 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. 所有数据在输入后将被删除。 @@ -751,6 +789,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + 所有消息和文件均通过**端到端加密**发送;私信以量子安全方式发送。 No comment provided by engineer. @@ -773,6 +812,16 @@ 所有配置文件 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. 所有联系人会保持连接。 @@ -813,6 +862,10 @@ 允许降级 No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) 仅有您的联系人许可后才允许不可撤回消息移除 @@ -848,6 +901,11 @@ 允许不可撤回地删除已发送消息 No comment provided by engineer. + + Allow to report messsages to moderators. + 允许向 moderators 举报消息。 + No comment provided by engineer. + Allow to send SimpleX links. 允许发送 SimpleX 链接。 @@ -893,6 +951,10 @@ 允许您的联系人发送限时消息。 No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. 允许您的联系人发送语音消息。 @@ -906,12 +968,12 @@ Already connecting! 已经在连接了! - No comment provided by engineer. + new chat sheet title Already joining the group! 已经加入了该群组! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -928,6 +990,11 @@ 已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。 No comment provided by engineer. + + Another reason + 另一个理由 + report reason + Answer call 接听来电 @@ -953,6 +1020,11 @@ 应用程序为新的本地文件(视频除外)加密。 No comment provided by engineer. + + App group: + 应用组: + No comment provided by engineer. + App icon 应用程序图标 @@ -970,6 +1042,7 @@ App session + 应用会话 No comment provided by engineer. @@ -997,6 +1070,21 @@ 应用于 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 存档和上传 @@ -1007,6 +1095,21 @@ 存档联系人以便稍后聊天. 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 已存档的联系人 @@ -1077,10 +1180,6 @@ 自动接受图片 No comment provided by engineer. - - Auto-accept settings - alert title - Back 返回 @@ -1108,6 +1207,7 @@ Better calls + 更佳的通话 No comment provided by engineer. @@ -1115,8 +1215,14 @@ 更佳的群组 No comment provided by engineer. + + Better groups performance + 更好的群性能 + No comment provided by engineer. + Better message dates. + 更好的消息日期。 No comment provided by engineer. @@ -1131,16 +1237,32 @@ 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. + + Bio + No comment provided by engineer. + + + Bio too large + alert title + Black 黑色 @@ -1191,6 +1313,10 @@ 模糊媒体 No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. 您和您的联系人都可以添加消息回应。 @@ -1211,6 +1337,10 @@ 您和您的联系人都可以发送限时消息。 No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. 您和您的联系人都可以发送语音消息。 @@ -1223,10 +1353,21 @@ Business address + 企业地址 No comment provided by engineer. Business chats + 企业聊天 + No comment provided by engineer. + + + Business connection + No comment provided by engineer. + + + Businesses + 企业 No comment provided by engineer. @@ -1234,6 +1375,15 @@ 通过聊天资料(默认)或者[通过连接](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! 通话已结束! @@ -1264,6 +1414,10 @@ 无法呼叫成员 No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! 无法邀请联系人! @@ -1283,7 +1437,8 @@ Cancel 取消 alert action - alert button +alert button +new chat action Cancel migration @@ -1320,8 +1475,14 @@ 更改 No comment provided by engineer. + + Change automatic message deletion? + 更改消息自动删除设置? + alert title + Change chat profiles + 更改聊天资料 authentication reason @@ -1368,19 +1529,22 @@ Change self-destruct passcode 更改自毁密码 authentication reason - set passcode view +set passcode view Chat + 聊天 No comment provided by engineer. Chat already exists + 聊天已存在 No comment provided by engineer. Chat already exists! - No comment provided by engineer. + 聊天已存在! + new chat sheet title Chat colors @@ -1444,6 +1608,7 @@ Chat preferences were changed. + 聊天偏好设置已修改。 alert message @@ -1458,10 +1623,24 @@ Chat will be deleted for all members - this cannot be undone! + 将为所有成员删除聊天 - 此操作无法撤销! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + 将为你删除聊天 - 此操作无法撤销! + No comment provided by engineer. + + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + + + Chat with members before they join. No comment provided by engineer. @@ -1469,12 +1648,18 @@ 聊天 No comment provided by engineer. + + Chats with members + 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. @@ -1532,6 +1717,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? 清除私密笔记? @@ -1552,6 +1747,11 @@ 颜色模式 No comment provided by engineer. + + Community guidelines violation + 违反社区指导方针 + report reason + Compare file 对比文件 @@ -1569,38 +1769,37 @@ 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 enabled operators after 30 days. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - No comment provided by engineer. + 使用条款 + alert button 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. @@ -1608,6 +1807,11 @@ 配置 ICE 服务器 No comment provided by engineer. + + Configure server operators + 配置服务器运营方 + No comment provided by engineer. + Confirm 确认 @@ -1658,6 +1862,11 @@ 确认上传 No comment provided by engineer. + + Confirmed + 已确定 + token status text + Connect 连接 @@ -1668,9 +1877,8 @@ 自动连接 No comment provided by engineer. - - Connect incognito - 在隐身状态下连接 + + Connect faster! 🚀 No comment provided by engineer. @@ -1683,44 +1891,39 @@ 更快地与您的朋友联系。 No comment provided by engineer. - - Connect to yourself? - 连接到你自己? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! 与自己建立联系? 这是您自己的 SimpleX 地址! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! 与自己建立联系? 这是您自己的一次性链接! - No comment provided by engineer. + new chat sheet title Connect via contact address 通过联系地址连接 - No comment provided by engineer. + new chat sheet title Connect via link 通过链接连接 - No comment provided by engineer. + new chat sheet title Connect via one-time link 通过一次性链接连接 - No comment provided by engineer. + new chat sheet title Connect with %@ 与 %@连接 - No comment provided by engineer. + new chat action Connected @@ -1777,16 +1980,32 @@ This is your own one-time link! 连接和服务器状态。 No comment provided by engineer. + + Connection blocked + 连接被阻止 + No comment provided by engineer. + Connection error 连接错误 - No comment provided by engineer. + alert title Connection error (AUTH) 连接错误(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 连接通知 @@ -1797,8 +2016,14 @@ This is your own one-time link! 已发送连接请求! No comment provided by engineer. + + Connection requires encryption renegotiation. + 连接需要加密重协商。 + No comment provided by engineer. + Connection security + 连接安全性 No comment provided by engineer. @@ -1809,7 +2034,7 @@ This is your own one-time link! Connection timeout 连接超时 - No comment provided by engineer. + alert title Connection with desktop stopped @@ -1861,6 +2086,10 @@ This is your own one-time link! 联系人偏好设置 No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! 联系人将被删除-这是无法撤消的! @@ -1876,6 +2105,11 @@ This is your own one-time link! 联系人可以将信息标记为删除;您将可以查看这些信息。 No comment provided by engineer. + + Content violates conditions of use + 内容违反使用条款 + blocking reason + Continue 继续 @@ -1903,6 +2137,7 @@ This is your own one-time link! Corner + 拐角 No comment provided by engineer. @@ -1917,6 +2152,7 @@ This is your own one-time link! Create 1-time link + 创建一次性链接 No comment provided by engineer. @@ -1949,6 +2185,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/)中创建新的个人资料。 💻 @@ -1964,9 +2205,8 @@ This is your own one-time link! 创建队列 server test step - - Create secret group - 创建私密群组 + + Create your address No comment provided by engineer. @@ -2006,6 +2246,7 @@ This is your own one-time link! Current conditions text couldn't be loaded, you can review conditions via this link: + 无法加载当前条款文本,你可以通过此链接审阅条款: No comment provided by engineer. @@ -2030,6 +2271,7 @@ This is your own one-time link! Customizable message shape. + 可自定义消息形状。 No comment provided by engineer. @@ -2164,8 +2406,7 @@ This is your own one-time link! Delete 删除 alert action - chat item action - swipe action +swipe action Delete %lld messages of members? @@ -2204,6 +2445,12 @@ This is your own one-time link! Delete chat + 删除聊天 + No comment provided by engineer. + + + Delete chat messages from your device. + 从你的设备删除聊天消息。 No comment provided by engineer. @@ -2216,8 +2463,13 @@ This is your own one-time link! 删除聊天资料? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? + 删除聊天? No comment provided by engineer. @@ -2295,6 +2547,11 @@ This is your own one-time link! 删除链接? No comment provided by engineer. + + Delete list? + 删除列表? + alert title + Delete member message? 删除成员消息? @@ -2308,7 +2565,7 @@ This is your own one-time link! Delete messages 删除消息 - No comment provided by engineer. + alert button Delete messages after @@ -2327,6 +2584,7 @@ This is your own one-time link! Delete or moderate up to 200 messages. + 允许自行删除或管理员移除最多200条消息。 No comment provided by engineer. @@ -2344,6 +2602,11 @@ This is your own one-time link! 删除队列 server test step + + Delete report + 删除举报 + No comment provided by engineer. + Delete up to 20 messages at once. 一次最多删除 20 条信息。 @@ -2381,6 +2644,7 @@ This is your own one-time link! Delivered even when Apple drops them. + 已送达,即使苹果已将其删除。 No comment provided by engineer. @@ -2398,11 +2662,19 @@ This is your own one-time link! 送达回执! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description 描述 No comment provided by engineer. + + Description too large + alert title + Desktop address 桌面地址 @@ -2465,7 +2737,7 @@ This is your own one-time link! Device authentication is disabled. Turning off SimpleX Lock. - 设备验证被禁用。关闭 SimpleX 锁定。 + 设备验证已禁用。 SimpleX 已解锁。 No comment provided by engineer. @@ -2485,11 +2757,12 @@ This is your own one-time link! 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. @@ -2502,6 +2775,16 @@ This is your own one-time link! 禁用 SimpleX 锁定 authentication reason + + Disable automatic message deletion? + 禁用消息自动销毁? + alert title + + + Disable delete messages + 停用消息删除 + alert button + Disable for all 全部禁用 @@ -2589,6 +2872,12 @@ This is your own one-time link! Do not use credentials with proxy. + 代理不使用身份验证凭据。 + No comment provided by engineer. + + + Documents: + 文档: No comment provided by engineer. @@ -2601,9 +2890,19 @@ This is your own one-time link! 不要启用 No comment provided by engineer. + + Don't miss important messages. + 不错过重要消息。 + No comment provided by engineer. + Don't show again 不再显示 + alert action + + + Done + 完成 No comment provided by engineer. @@ -2615,7 +2914,7 @@ This is your own one-time link! Download 下载 alert button - chat item action +chat item action Download errors @@ -2634,6 +2933,7 @@ This is your own one-time link! Download files + 下载文件 alert action @@ -2668,6 +2968,7 @@ This is your own one-time link! E2E encrypted notifications. + 端到端加密的通知。 No comment provided by engineer. @@ -2680,6 +2981,10 @@ This is your own one-time link! 编辑群组资料 No comment provided by engineer. + + Empty message! + No comment provided by engineer. + Enable 启用 @@ -2690,8 +2995,9 @@ This is your own one-time link! 启用(保持覆盖) No comment provided by engineer. - - Enable Flux + + Enable Flux in Network & servers settings for better metadata privacy. + 在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。 No comment provided by engineer. @@ -2707,13 +3013,17 @@ This is your own one-time link! Enable automatic message deletion? 启用自动删除消息? - No comment provided by engineer. + alert title Enable camera access 启用相机访问 No comment provided by engineer. + + Enable disappearing messages by default. + No comment provided by engineer. + Enable for all 全部启用 @@ -2834,6 +3144,11 @@ This is your own one-time link! 加密重协商失败了。 No comment provided by engineer. + + Encryption renegotiation in progress. + 正进行加密重协商。 + No comment provided by engineer. + Enter Passcode 输入密码 @@ -2901,6 +3216,7 @@ This is your own one-time link! Error accepting conditions + 接受条款出错 alert title @@ -2908,6 +3224,10 @@ This is your own one-time link! 接受联系人请求错误 No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) 添加成员错误 @@ -2915,15 +3235,25 @@ This is your own one-time link! Error adding server + 添加服务器出错 alert title + + Error adding short link + No comment provided by engineer. + Error changing address 更改地址错误 No comment provided by engineer. + + Error changing chat profile + alert title + Error changing connection profile + 更改连接资料出错 No comment provided by engineer. @@ -2934,16 +3264,25 @@ This is your own one-time link! Error changing setting 更改设置错误 - No comment provided by engineer. + alert title 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. + alert message + + + Error connecting to the server used to receive messages from this connection: %@ + subscription status explanation Error creating address @@ -2960,6 +3299,11 @@ This is your own one-time link! 创建群组链接错误 No comment provided by engineer. + + Error creating list + 创建列表出错 + alert title + Error creating member contact 创建成员联系人时出错 @@ -2975,20 +3319,29 @@ This is your own one-time link! 创建资料错误! No comment provided by engineer. + + Error creating report + 创建举报出错 + No comment provided by engineer. + Error decrypting file 解密文件时出错 No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database 删除聊天数据库错误 - No comment provided by engineer. + alert title Error deleting chat! 删除聊天错误! - No comment provided by engineer. + alert title Error deleting connection @@ -2998,12 +3351,12 @@ This is your own one-time link! Error deleting database 删除数据库错误 - No comment provided by engineer. + alert title Error deleting old database 删除旧数据库错误 - No comment provided by engineer. + alert title Error deleting token @@ -3038,7 +3391,7 @@ This is your own one-time link! Error exporting chat database 导出聊天数据库错误 - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3048,7 +3401,7 @@ This is your own one-time link! Error importing chat database 导入聊天数据库错误 - No comment provided by engineer. + alert title Error joining group @@ -3057,10 +3410,12 @@ This is your own one-time link! Error loading servers + 加载服务器出错 alert title Error migrating settings + 迁移设置出错 No comment provided by engineer. @@ -3068,6 +3423,10 @@ This is your own one-time link! 打开聊天时出错 No comment provided by engineer. + + Error opening group + No comment provided by engineer. + Error receiving file 接收文件错误 @@ -3083,10 +3442,24 @@ This is your own one-time link! 重新连接服务器时出错 No comment provided by engineer. + + Error registering for notifications + 注册消息推送出错 + alert title + + + Error rejecting contact request + alert title + Error removing member 删除成员错误 - No comment provided by engineer. + alert title + + + Error reordering lists + 重排列表出错 + alert title Error resetting statistics @@ -3098,6 +3471,11 @@ This is your own one-time link! 保存 ICE 服务器错误 No comment provided by engineer. + + Error saving chat list + 保存聊天列表出错 + alert title + Error saving group profile 保存群组资料错误 @@ -3115,6 +3493,7 @@ This is your own one-time link! Error saving servers + 保存服务器出错 alert title @@ -3147,6 +3526,10 @@ This is your own one-time link! 发送消息错误 No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! 设置送达回执出错! @@ -3164,7 +3547,8 @@ This is your own one-time link! Error switching profile - No comment provided by engineer. + 切换配置文件出错 + alert title Error switching profile! @@ -3176,6 +3560,11 @@ This is your own one-time link! 同步连接错误 No comment provided by engineer. + + Error testing server connection + 检验服务器连接出错 + No comment provided by engineer. + Error updating group link 更新群组链接错误 @@ -3188,6 +3577,7 @@ This is your own one-time link! Error updating server + 更新服务器出错 alert title @@ -3218,7 +3608,13 @@ This is your own one-time link! Error: %@ 错误: %@ - alert message + alert message +file error text +snd error text + + + Error: %@. + server test error Error: URL is invalid @@ -3237,6 +3633,7 @@ This is your own one-time link! Errors in servers configuration. + 服务器配置有错误。 servers error @@ -3254,6 +3651,11 @@ This is your own one-time link! 展开 chat item action + + Expired + 已过期 + token status text + Export database 导出数据库 @@ -3294,26 +3696,50 @@ This is your own one-time link! 快速且无需等待发件人在线! 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 文件错误 - No comment provided by engineer. + 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. 找不到文件 - 很可能文件已被删除或取消。 @@ -3369,6 +3795,10 @@ This is your own one-time link! 文件和媒体 chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. 此群组中禁止文件和媒体。 @@ -3409,6 +3839,23 @@ This is your own one-time link! 更快地查找聊天记录 No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + 服务器地址中的证书指纹可能不正确 + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix 修复 @@ -3439,8 +3886,14 @@ This is your own one-time link! 修复群组成员不支持的问题 No comment provided by engineer. + + For all moderators + 所有 moderators + No comment provided by engineer. + For chat profile %@: + 为聊天资料 %@: servers error @@ -3450,14 +3903,22 @@ This is your own one-time link! 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. @@ -3467,6 +3928,7 @@ This is your own one-time link! Forward %d message(s)? + 转发 %d 条消息? alert title @@ -3476,14 +3938,17 @@ This is your own one-time link! Forward messages + 已转发的消息 alert action Forward messages without files? + 仅转发消息不转发文件? alert message Forward up to 20 messages at once. + 一次转发最多20条消息。 No comment provided by engineer. @@ -3498,12 +3963,13 @@ This is your own one-time link! 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 %1$@ failed to connect to destination server %2$@. Please try later. + 转发服务器 %1$@ 无法连接到目标服务器 %2$@。请稍后尝试。 + alert message Forwarding server address is incompatible with network settings: %@. @@ -3569,6 +4035,11 @@ Error: %2$@ GIF 和贴纸 No comment provided by engineer. + + Get notified when mentioned. + 被提及时收到通知。 + No comment provided by engineer. + Good afternoon! 下午好! @@ -3592,7 +4063,7 @@ Error: %2$@ Group already exists! 群已存在! - No comment provided by engineer. + new chat sheet title Group display name @@ -3659,6 +4130,10 @@ Error: %2$@ 群组资料存储在成员的设备上,而不是服务器上。 No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message 群欢迎词 @@ -3674,11 +4149,21 @@ Error: %2$@ 将为您删除群组——此操作无法撤消! 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 隐藏 @@ -3731,12 +4216,19 @@ Error: %2$@ How it affects privacy + 它如何影响隐私 No comment provided by engineer. How it helps privacy + 它如何帮助隐私 No comment provided by engineer. + + How it works + 工作原理 + alert button + How to 如何 @@ -3764,6 +4256,7 @@ Error: %2$@ IP address + IP 地址 No comment provided by engineer. @@ -3844,6 +4337,8 @@ Error: %2$@ Improved delivery, reduced traffic usage. More improvements are coming soon! + 改善传送,降低流量使用。 +更多改进即将推出! No comment provided by engineer. @@ -3876,6 +4371,16 @@ More improvements are coming soon! 通话声音 No comment provided by engineer. + + Inappropriate content + 不当内容 + report reason + + + Inappropriate profile + 不当个人资料 + report reason + Incognito 隐身聊天 @@ -3968,6 +4473,31 @@ More improvements are coming soon! 界面颜色 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 无效的二维码 @@ -3986,7 +4516,7 @@ More improvements are coming soon! Invalid link 无效链接 - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4030,6 +4560,7 @@ More improvements are coming soon! Invite to chat + 邀请加入聊天 No comment provided by engineer. @@ -4098,37 +4629,32 @@ More improvements are coming soon! 加入 swipe action + + Join as %@ + 以 %@ 身份加入 + No comment provided by engineer. + Join group 加入群组 - No comment provided by engineer. + new chat sheet title Join group conversations 加入群对话 No comment provided by engineer. - - Join group? - 加入群组? - No comment provided by engineer. - Join incognito 加入隐身聊天 No comment provided by engineer. - - Join with current profile - 使用当前档案加入 - No comment provided by engineer. - Join your group? This is your link for group %@! 加入您的群组? 这是您组 %@ 的链接! - No comment provided by engineer. + new chat action Joining group @@ -4155,6 +4681,10 @@ This is your link for group %@! 保留未使用的邀请吗? alert title + + Keep your chats clean + No comment provided by engineer. + Keep your connections 保持连接 @@ -4192,10 +4722,12 @@ This is your link for group %@! Leave chat + 离开聊天 No comment provided by engineer. Leave chat? + 离开聊天? No comment provided by engineer. @@ -4208,6 +4740,10 @@ This is your link for group %@! 离开群组? No comment provided by engineer. + + Less traffic on mobile networks. + No comment provided by engineer. + Let's talk in SimpleX Chat 让我们一起在 SimpleX Chat 里聊天 @@ -4238,6 +4774,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! 实时消息! @@ -4248,6 +4799,10 @@ This is your link for group %@! 实时消息 No comment provided by engineer. + + Loading profile… + in progress text + Local name 本地名称 @@ -4323,13 +4878,31 @@ This is your link for group %@! 成员 No comment provided by engineer. + + Member %@ + past/unknown group member + + + Member admission + No comment provided by engineer. + Member inactive 成员不活跃 item status text + + Member is deleted - can't accept request + No comment provided by engineer. + + + Member reports + 成员举报 + chat feature + Member role will be changed to "%@". All chat members will be notified. + 将变更成员角色为“%@”。所有成员都会收到通知。 No comment provided by engineer. @@ -4344,6 +4917,7 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! + 将从聊天中删除成员 - 此操作无法撤销! No comment provided by engineer. @@ -4351,6 +4925,10 @@ This is your link for group %@! 成员将被移出群组——此操作无法撤消! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. 群组成员可以添加信息回应。 @@ -4361,6 +4939,11 @@ This is your link for group %@! 群组成员可以不可撤回地删除已发送的消息 No comment provided by engineer. + + Members can report messsages to moderators. + 成员可以向 moderators 举报消息。 + No comment provided by engineer. + Members can send SimpleX links. 群成员可发送 SimpleX 链接。 @@ -4386,6 +4969,11 @@ This is your link for group %@! 群组成员可以发送语音消息。 No comment provided by engineer. + + Mention members 👋 + 提及成员👋 + No comment provided by engineer. + Menus 菜单 @@ -4416,6 +5004,10 @@ This is your link for group %@! 消息已转发 item status text + + Message instantly once you tap Connect. + No comment provided by engineer. + Message may be delivered later if member becomes active. 如果 member 变为活动状态,则稍后可能会发送消息。 @@ -4453,6 +5045,7 @@ This is your link for group %@! Message shape + 消息形状 No comment provided by engineer. @@ -4490,11 +5083,20 @@ This is your link for group %@! 消息 No comment provided by engineer. + + Messages are protected by **end-to-end encryption**. + No comment provided by engineer. + Messages from %@ will be shown! 将显示来自 %@ 的消息! No comment provided by engineer. + + Messages in this chat will never be deleted. + 此聊天中的消息永远不会被删除。 + alert message + Messages received 收到的消息 @@ -4507,6 +5109,7 @@ This is your link for group %@! Messages were deleted after you selected them. + 在你选中消息后这些消息已被删除。 alert message @@ -4594,6 +5197,11 @@ This is your link for group %@! 已被管理员移除于:%@ copied message info + + More + 更多 + swipe action + More improvements are coming soon! 更多改进即将推出! @@ -4606,6 +5214,7 @@ This is your link for group %@! More reliable notifications + 更可靠的通知 No comment provided by engineer. @@ -4621,7 +5230,12 @@ This is your link for group %@! Mute 静音 - swipe action + notification label action + + + Mute all + 全部静音 + notification label action Muted when inactive! @@ -4645,6 +5259,7 @@ This is your link for group %@! Network decentralization + 网络去中心化 No comment provided by engineer. @@ -4659,6 +5274,7 @@ This is your link for group %@! Network operator + 网络运营方 No comment provided by engineer. @@ -4669,7 +5285,12 @@ This is your link for group %@! Network status 网络状态 - No comment provided by engineer. + alert title + + + New + + token status text New Passcode @@ -4678,10 +5299,12 @@ This is your link for group %@! 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. @@ -4716,8 +5339,13 @@ This is your link for group %@! New events + 新事件 notification + + New group role: Moderator + No comment provided by engineer. + New in %@ %@ 的新内容 @@ -4733,6 +5361,10 @@ This is your link for group %@! 新成员角色 No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message 新消息 @@ -4745,6 +5377,7 @@ This is your link for group %@! New server + 新服务器 No comment provided by engineer. @@ -4757,6 +5390,25 @@ 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 chats with members + No comment provided by engineer. + No contacts selected 未选择联系人 @@ -4804,10 +5456,17 @@ This is your link for group %@! No media & file servers. + 无媒体和文件服务器。 servers error + + No message + 无消息 + No comment provided by engineer. + No message servers. + 无消息服务器。 servers error @@ -4817,10 +5476,12 @@ This is your link for group %@! No permission to record speech + 无录音权限 No comment provided by engineer. No permission to record video + 无录像权限 No comment provided by engineer. @@ -4828,6 +5489,10 @@ This is your link for group %@! 没有录制语音消息的权限 No comment provided by engineer. + + No private routing session + alert title + No push server 本地 @@ -4840,20 +5505,34 @@ This is your link for group %@! 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. 没有用户标识符。 @@ -4864,6 +5543,11 @@ This is your link for group %@! 不兼容! No comment provided by engineer. + + Notes + 附注 + No comment provided by engineer. + Nothing selected 未选中任何内容 @@ -4871,6 +5555,7 @@ This is your link for group %@! Nothing to forward! + 无可转发! alert title @@ -4883,10 +5568,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. @@ -4909,7 +5605,9 @@ This is your link for group %@! Ok 好的 - alert button + alert action +alert button +new chat action Old database @@ -4942,6 +5640,7 @@ Requires compatible VPN. Only chat owners can change preferences. + 仅聊天所有人可更改首选项。 No comment provided by engineer. @@ -4969,6 +5668,16 @@ Requires compatible VPN. 只有群主可以启用语音信息。 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. 只有您可以添加消息回应。 @@ -4989,6 +5698,10 @@ Requires compatible VPN. 只有您可以发送限时消息。 No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. 只有您可以发送语音消息。 @@ -5014,6 +5727,10 @@ Requires compatible VPN. 只有您的联系人才可以发送限时消息。 No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. 只有您的联系人可以发送语音消息。 @@ -5022,7 +5739,7 @@ Requires compatible VPN. Open 打开 - No comment provided by engineer. + alert action Open Settings @@ -5031,32 +5748,70 @@ Requires compatible VPN. Open changes + 打开更改 No comment provided by engineer. Open chat 打开聊天 - No comment provided by engineer. + new chat action Open chat console 打开聊天控制台 authentication reason + + Open clean link + alert action + Open conditions + 打开条款 No comment provided by engineer. + + Open full link + alert action + Open group 打开群 - No comment provided by engineer. + new chat action + + + Open link? + alert title Open migration to another device 打开迁移到另一台设备 authentication reason + + Open new chat + new chat action + + + Open new group + new chat action + + + Open to accept + No comment provided by engineer. + + + Open to connect + No comment provided by engineer. + + + Open to join + No comment provided by engineer. + + + Open to use bot + No comment provided by engineer. + Opening app… 正在打开应用程序… @@ -5064,14 +5819,17 @@ Requires compatible VPN. Operator + 运营方 No comment provided by engineer. Operator server + 运营方服务器 alert title Or import archive file + 或者导入或者导入压缩文件 No comment provided by engineer. @@ -5096,6 +5854,12 @@ Requires compatible VPN. Or to share privately + 或者私下分享 + No comment provided by engineer. + + + Organize chats into lists + 将聊天组织到列表 No comment provided by engineer. @@ -5145,6 +5909,7 @@ Requires compatible VPN. Password + 密码 No comment provided by engineer. @@ -5152,11 +5917,6 @@ Requires compatible VPN. 显示密码 No comment provided by engineer. - - Past member %@ - 前任成员 %@ - past/unknown group member - Paste desktop address 粘贴桌面地址 @@ -5227,7 +5987,7 @@ Please share any other issues with the developers. Please check your network connection with %@ and try again. 请检查您与%@的网络连接,然后重试。 - No comment provided by engineer. + alert message Please check yours and your contact preferences. @@ -5286,6 +6046,22 @@ Error: %@ 请安全地保存密码,如果您丢失了密码,您将无法更改它。 No comment provided by engineer. + + Please try to disable and re-enable notfications. + token info + + + Please wait for group moderators to review your request to join the group. + snd group event chat item + + + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + token info + Polish interface 波兰语界面 @@ -5295,11 +6071,6 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - 服务器地址中的证书指纹可能不正确 - server test error - Preserve the last message draft, with attachments. 保留最后的消息草稿及其附件。 @@ -5333,16 +6104,30 @@ Error: %@ 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 私有消息路由 @@ -5366,7 +6151,11 @@ Error: %@ Private routing error 专用路由错误 - No comment provided by engineer. + alert title + + + Private routing timeout + alert title Profile and server connections @@ -5418,6 +6207,10 @@ Error: %@ 禁止消息回应。 No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. 禁止发送 SimpleX 链接。 @@ -5465,6 +6258,10 @@ Enable in *Network & servers* settings. 使用密码保护您的聊天资料! No comment provided by engineer. + + Protocol background timeout + No comment provided by engineer. + Protocol timeout 协议超时 @@ -5569,11 +6366,6 @@ Enable in *Network & servers* settings. 已收到于:%@ copied message info - - Received file event - 收到文件项目 - notification - Received message 收到的信息 @@ -5674,11 +6466,24 @@ Enable in *Network & servers* settings. 减少电池使用量 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 - swipe action + alert action +reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -5688,7 +6493,11 @@ Enable in *Network & servers* settings. Reject contact request 拒绝联系人请求 - No comment provided by engineer. + alert title + + + Reject member? + alert title Relay server is only used if necessary. Another party can observe your IP address. @@ -5714,6 +6523,10 @@ Enable in *Network & servers* settings. 移除图片 No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member 删除成员 @@ -5729,6 +6542,10 @@ Enable in *Network & servers* settings. 从钥匙串中删除密码? No comment provided by engineer. + + Removes messages and blocks members. + No comment provided by engineer. + Renegotiate 重新协商 @@ -5744,11 +6561,6 @@ Enable in *Network & servers* settings. 重新协商加密? No comment provided by engineer. - - Repeat connection request? - 重复连接请求吗? - No comment provided by engineer. - Repeat download 重复下载 @@ -5759,11 +6571,6 @@ Enable in *Network & servers* settings. 重复导入 No comment provided by engineer. - - Repeat join request? - 重复加入请求吗? - No comment provided by engineer. - Repeat upload 重复上传 @@ -5774,6 +6581,50 @@ Enable in *Network & servers* settings. 回复 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 sent to moderators + alert title + + + 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 必须 @@ -5852,7 +6703,7 @@ Enable in *Network & servers* settings. Retry 重试 - No comment provided by engineer. + alert action Reveal @@ -5861,25 +6712,34 @@ Enable in *Network & servers* settings. Review conditions + 审阅条款 No comment provided by engineer. - - Review later + + Review group members No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke - 撤销 + 吊销 No comment provided by engineer. Revoke file - 撤销文件 + 吊销文件 cancel file action Revoke file? - 撤销文件? + 吊销文件? No comment provided by engineer. @@ -5889,7 +6749,7 @@ Enable in *Network & servers* settings. Run chat - 运行聊天程序 + 运行聊天 No comment provided by engineer. @@ -5915,13 +6775,21 @@ Enable in *Network & servers* settings. Save 保存 alert button - chat item action +chat item action Save (and notify contacts) 保存(并通知联系人) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact 保存并通知联系人 @@ -5947,6 +6815,15 @@ Enable in *Network & servers* settings. 保存群组资料 No comment provided by engineer. + + Save group profile? + alert title + + + Save list + 保存列表 + No comment provided by engineer. + Save passphrase and open chat 保存密码并打开聊天 @@ -5984,6 +6861,7 @@ Enable in *Network & servers* settings. Save your profile? + 保存您的个人资料? alert title @@ -6008,6 +6886,7 @@ Enable in *Network & servers* settings. Saving %lld messages + 正在保存 %lld 条消息 No comment provided by engineer. @@ -6134,6 +7013,10 @@ Enable in *Network & servers* settings. 发送实时消息——它会在您键入时为收件人更新 No comment provided by engineer. + + Send contact request? + No comment provided by engineer. + Send delivery receipts to 将送达回执发送给 @@ -6184,6 +7067,10 @@ Enable in *Network & servers* settings. 发送通知 No comment provided by engineer. + + Send private reports + No comment provided by engineer. + Send questions and ideas 发送问题和想法 @@ -6194,6 +7081,14 @@ Enable in *Network & servers* settings. 发送回执 No comment provided by engineer. + + Send request + No comment provided by engineer. + + + Send request without message + No comment provided by engineer. + Send them from gallery or custom keyboards. 发送它们来自图库或自定义键盘。 @@ -6204,6 +7099,10 @@ Enable in *Network & servers* settings. 给新成员发送最多 100 条历史消息。 No comment provided by engineer. + + Send your private feedback to groups. + No comment provided by engineer. + Sender cancelled file transfer. 发送人已取消文件传输。 @@ -6269,11 +7168,6 @@ Enable in *Network & servers* settings. 直接发送 No comment provided by engineer. - - Sent file event - 已发送文件项目 - notification - Sent message 已发信息 @@ -6339,13 +7233,13 @@ Enable in *Network & servers* settings. Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. 服务器需要授权才能创建队列,检查密码 server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. 服务器需要授权来上传,检查密码 server test error @@ -6394,6 +7288,10 @@ Enable in *Network & servers* settings. 设定1天 No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… 设置联系人姓名…… @@ -6414,6 +7312,14 @@ Enable in *Network & servers* settings. 设置它以代替系统身份验证。 No comment provided by engineer. + + Set member admission + No comment provided by engineer. + + + Set message expiration in chats. + No comment provided by engineer. + Set passcode 设置密码 @@ -6429,6 +7335,10 @@ Enable in *Network & servers* settings. 设置密码来导出 No comment provided by engineer. + + Set profile bio and welcome message. + No comment provided by engineer. + Set the message shown to new members! 设置向新成员显示的消息! @@ -6457,7 +7367,7 @@ Enable in *Network & servers* settings. Share 分享 alert action - chat item action +chat item action Share 1-time link @@ -6496,6 +7406,14 @@ Enable in *Network & servers* settings. 分享链接 No comment provided by engineer. + + Share old address + alert button + + + Share old link + alert button + Share profile No comment provided by engineer. @@ -6515,6 +7433,22 @@ Enable in *Network & servers* settings. 与联系人分享 No comment provided by engineer. + + Share your address + No comment provided by engineer. + + + Short SimpleX address + No comment provided by engineer. + + + Short description + No comment provided by engineer. + + + Short link + No comment provided by engineer. + Show QR code 显示二维码 @@ -6572,6 +7506,7 @@ Enable in *Network & servers* settings. 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. @@ -6606,12 +7541,24 @@ Enable in *Network & servers* settings. 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 address settings + 自动接受设置 + alert title + + + SimpleX channel link + SimpleX 频道链接 + simplex link type + SimpleX contact address SimpleX 联系地址 @@ -6619,12 +7566,12 @@ Enable in *Network & servers* settings. SimpleX encrypted message or connection event - SimpleX 加密消息或连接项目 + SimpleX 加密的消息或连接事件 notification SimpleX group link - SimpleX 群组链接 + SimpleX 群链接 simplex link type @@ -6649,8 +7596,13 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. + SimpleX 协议由 Trail of Bits 审阅。 No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode 简化的隐身模式 @@ -6683,6 +7635,7 @@ Enable in *Network & servers* settings. Some app settings were not migrated. + 部分应用设置未被迁移。 No comment provided by engineer. @@ -6710,6 +7663,11 @@ Enable in *Network & servers* settings. 某人 notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. 方形、圆形、或两者之间的任意形状. @@ -6795,6 +7753,10 @@ Enable in *Network & servers* settings. 正在停止聊天 No comment provided by engineer. + + Storage + No comment provided by engineer. + Strong 加粗 @@ -6848,11 +7810,19 @@ Enable in *Network & servers* settings. TCP 连接 No comment provided by engineer. + + TCP connection bg timeout + 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 @@ -6877,10 +7847,26 @@ Enable in *Network & servers* settings. 拍照 No comment provided by engineer. + + Tap Connect to chat + No comment provided by engineer. + + + Tap Connect to send request + No comment provided by engineer. + + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join group + No comment provided by engineer. + Tap button 点击按钮 @@ -6919,13 +7905,17 @@ Enable in *Network & servers* settings. Temporary file error 临时文件错误 - No comment provided by engineer. + file error alert title Test failed at step %@. 在步骤 %@ 上测试失败。 server test failure + + Test notifications + No comment provided by engineer. + Test server 测试服务器 @@ -6963,6 +7953,10 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The address will be short, and your profile will be shared via the address. + alert message + The app can notify you when you receive messages or contact requests - please open settings to enable. 该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。 @@ -7021,6 +8015,10 @@ It can happen because of some bug or when the connection is compromised.上一条消息的散列不同。 No comment provided by engineer. + + The link will be short, and group profile will be shared via the link. + alert message + The message will be deleted for all members. 将为所有成员删除该消息。 @@ -7046,19 +8044,10 @@ 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. - 该资料仅与您的联系人共享。 - No comment provided by engineer. - The same conditions will apply to operator **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - The second preset operator in the app! No comment provided by engineer. @@ -7071,7 +8060,7 @@ It can happen because of some bug or when the connection is compromised. The sender will NOT be notified 发送者将不会收到通知 - No comment provided by engineer. + alert message The servers for new connections of your current chat profile **%@**. @@ -7120,6 +8109,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. 此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。 @@ -7155,14 +8148,8 @@ It can happen because of some bug or when the connection is compromised.该群组已不存在。 No comment provided by engineer. - - This is your own SimpleX address! - 这是你自己的 SimpleX 地址! - No comment provided by engineer. - - - This is your own one-time 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. @@ -7170,11 +8157,23 @@ It can happen because of some bug or when the connection is compromised.此链接已在其他移动设备上使用,请在桌面上创建新链接。 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. + + This setting is for your current profile **%@**. + No comment provided by engineer. + + + Time to disappear is set only for new contacts. + No comment provided by engineer. + Title 标题 @@ -7252,11 +8251,19 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. 为了支持即时推送通知,聊天数据库必须被迁移。 No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. @@ -7276,6 +8283,10 @@ You will be prompted to complete authentication before this feature is enabled.< 在连接时切换隐身模式。 No comment provided by engineer. + + Token status: %@. + token status + Toolbar opacity 工具栏不透明度 @@ -7296,15 +8307,9 @@ You will be prompted to complete authentication before this feature is enabled.< 传输会话 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. + + Trying to connect to the server used to receive messages from this connection. + subscription status explanation Turkish interface @@ -7440,13 +8445,17 @@ To connect, please ask your contact to create another connection link and check Unmute 取消静音 - swipe action + 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 条历史消息。 @@ -7472,16 +8481,44 @@ To connect, please ask your contact to create another connection link and check 更新设置? No comment provided by engineer. + + Updated conditions + No comment provided by engineer. + Updating settings will re-connect the client to all servers. 更新设置会将客户端重新连接到所有服务器。 No comment provided by engineer. + + Upgrade + alert button + + + Upgrade address + No comment provided by engineer. + + + Upgrade address? + alert message + Upgrade and open chat 升级并打开聊天 No comment provided by engineer. + + Upgrade group link? + alert message + + + Upgrade link + No comment provided by engineer. + + + Upgrade your address + No comment provided by engineer. + Upload errors 上传错误 @@ -7530,6 +8567,14 @@ To connect, please ask your contact to create another connection link and check 使用 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 使用聊天 @@ -7538,7 +8583,7 @@ To connect, please ask your contact to create another connection link and check Use current profile 使用当前配置文件 - No comment provided by engineer. + new chat action Use for files @@ -7563,10 +8608,14 @@ To connect, please ask your contact to create another connection link and check 使用 iOS 通话界面 No comment provided by engineer. + + Use incognito profile + No comment provided by engineer. + Use new incognito profile 使用新的隐身配置文件 - No comment provided by engineer. + new chat action Use only local notifications? @@ -7602,6 +8651,10 @@ To connect, please ask your contact to create another connection link and check 用一只手使用应用程序。 No comment provided by engineer. + + Use web port + No comment provided by engineer. + User selection 用户选择 @@ -7789,6 +8842,10 @@ To connect, please ask your contact to create another connection link and check 欢迎消息太大了 No comment provided by engineer. + + Welcome your contacts 👋 + No comment provided by engineer. + What's new 更新内容 @@ -7910,12 +8967,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. 您已连接到 %@。 - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! 你已经在通过这个一次性链接进行连接! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7925,35 +8982,33 @@ To connect, please ask your contact to create another connection link and check 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. + new chat sheet message You are already joining the group via this link. 你已经在通过此链接加入该群。 - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? 您已经加入了这个群组! 重复加入请求? - No comment provided by engineer. + new chat sheet title - - You are connected to the server used to receive messages from this contact. - 您已连接到用于接收该联系人消息的服务器。 - No comment provided by engineer. + + You are connected to the server used to receive messages from this connection. + subscription status explanation You are invited to group 您被邀请加入群组 No comment provided by engineer. + + You are not connected to the server used to receive messages from this connection (no subscription). + subscription status explanation + You are not connected to these servers. Private routing is used to deliver messages to them. 您未连接到这些服务器。私有路由用于向他们发送消息。 @@ -7969,10 +9024,6 @@ Repeat join request? 您可以在外观设置中更改它。 No comment provided by engineer. - - You can configure operators in Network & servers settings. - No comment provided by engineer. - You can configure servers via settings. No comment provided by engineer. @@ -8061,10 +9112,14 @@ Repeat join request? 您可以在连接详情中再次查看邀请链接。 alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! 您无法发送消息! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -8076,17 +9131,12 @@ Repeat join request? 你决定谁可以连接。 No comment provided by engineer. - - You have already requested connection via this address! - 你已经请求通过此地址进行连接! - No comment provided by engineer. - You have already requested connection! Repeat connection request? 您已经请求连接了! 重复连接请求? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8143,6 +9193,14 @@ Repeat connection request? 您发送了群组邀请 No comment provided by engineer. + + You should receive notifications. + token info + + + You will be able to send messages **only after your request is accepted**. + No comment provided by engineer. + You will be connected to group when the group host's device is online, please wait or check later! 您将在组主设备上线时连接到该群组,请稍等或稍后再检查! @@ -8168,11 +9226,6 @@ Repeat connection request? 当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。 No comment provided by engineer. - - You will connect to all group members. - 你将连接到所有群成员。 - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. 当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。 @@ -8207,16 +9260,15 @@ Repeat connection request? 您的 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 business contact + No comment provided by engineer. + Your calls 您的通话 @@ -8241,8 +9293,16 @@ Repeat connection request? 您的聊天资料 No comment provided by engineer. - - Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + alert message + + + Your connection was moved to %@ but an error happened when switching profile. + No comment provided by engineer. + + + Your contact No comment provided by engineer. @@ -8274,6 +9334,10 @@ Repeat connection request? 您当前的资料 No comment provided by engineer. + + Your group + No comment provided by engineer. + Your preferences 您的偏好设置 @@ -8294,6 +9358,11 @@ Repeat connection request? 您的个人资料 **%@** 将被共享。 No comment provided by engineer. + + Your profile is stored on your device and only shared with your contacts. + 该资料仅与您的联系人共享。 + No comment provided by engineer. + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. 您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。 @@ -8303,11 +9372,6 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message - - Your profile, contacts and delivered messages are stored on your device. - 您的资料、联系人和发送的消息存储在您的设备上。 - No comment provided by engineer. - Your random profile 您的随机资料 @@ -8357,6 +9421,10 @@ Repeat connection request? 上面,然后选择: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call 已接受通话 @@ -8366,6 +9434,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin 管理员 @@ -8386,6 +9458,10 @@ Repeat connection request? 同意加密… chat item text + + all + member criteria value + all members 所有成员 @@ -8401,6 +9477,10 @@ Repeat connection request? 和 %lld 其他事件 No comment provided by engineer. + + archived report + No comment provided by engineer. + attempts 尝试 @@ -8439,7 +9519,8 @@ Repeat connection request? blocked by admin 由管理员封禁 - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8466,6 +9547,10 @@ Repeat connection request? 呼叫中…… call status + + can't send messages + No comment provided by engineer. + cancelled %@ 已取消 %@ @@ -8516,11 +9601,6 @@ Repeat connection request? 已连接 No comment provided by engineer. - - connected directly - 已直连 - rcv group event chat item - connecting 连接中 @@ -8571,6 +9651,14 @@ Repeat connection request? 联系人 %1$@ 已更改为 %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption 联系人具有端到端加密 @@ -8581,6 +9669,14 @@ Repeat connection request? 联系人没有端到端加密 No comment provided by engineer. + + contact not ready + No comment provided by engineer. + + + contact should accept… + No comment provided by engineer. + creator 创建者 @@ -8609,7 +9705,8 @@ Repeat connection request? default (%@) 默认 (%@) - pref value + delete after time +pref value default (no) @@ -8658,7 +9755,7 @@ Repeat connection request? duplicates - 复本 + 副本 No comment provided by engineer. @@ -8736,30 +9833,29 @@ Repeat connection request? 错误 No comment provided by engineer. - - event happened - 发生的事 - No comment provided by engineer. - expired 过期 No comment provided by engineer. - - for better metadata privacy. - No comment provided by engineer. - forwarded 已转发 No comment provided by engineer. + + group + shown on group welcome message + group deleted 群组已删除 No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated 群组资料已更新 @@ -8855,11 +9951,6 @@ Repeat connection request? 斜体 No comment provided by engineer. - - join as %@ - 以 %@ 身份加入 - No comment provided by engineer. - left 已离开 @@ -8885,6 +9976,10 @@ Repeat connection request? 已连接 rcv group event chat item + + member has old version + No comment provided by engineer. + message 消息 @@ -8915,20 +10010,19 @@ Repeat connection request? 由 %@ 审核 marked deleted chat item preview text + + moderator + member role + months time unit - - mute - 静音 - No comment provided by engineer. - never 从不 - No comment provided by engineer. + delete after time new message @@ -8945,11 +10039,19 @@ Repeat connection request? 无端到端加密 No comment provided by engineer. + + no subscription + No comment provided by engineer. + no text 无文本 copied message info in history + + not synchronized + No comment provided by engineer. + observer 观察者 @@ -8959,8 +10061,9 @@ Repeat connection request? off 关闭 enabled status - group pref value - time to disappear +group pref value +member criteria value +time to disappear offered %@ @@ -9002,6 +10105,18 @@ Repeat connection request? 点对点 No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + + + pending review + No comment provided by engineer. + quantum resistant e2e encryption 抗量子端到端加密 @@ -9017,6 +10132,10 @@ Repeat connection request? 已受到确认…… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call 拒接来电 @@ -9037,6 +10156,10 @@ Repeat connection request? 删除了联系地址 profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture 删除了资料图片 @@ -9047,10 +10170,34 @@ Repeat connection request? 已将您移除 rcv group event chat item + + request is sent + No comment provided by engineer. + + + request to join rejected + No comment provided by engineer. + + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved 已保存 @@ -9086,11 +10233,6 @@ Repeat connection request? 安全密码已更改 chat item text - - send direct message - 发送私信 - No comment provided by engineer. - server queue info: %1$@ @@ -9150,11 +10292,6 @@ last received msg: %2$@ 未知状态 No comment provided by engineer. - - unmute - 取消静音 - No comment provided by engineer. - unprotected 未受保护 @@ -9245,10 +10382,9 @@ last received msg: %2$@ No comment provided by engineer. - - you are invited to group - 您被邀请加入群组 - No comment provided by engineer. + + you accepted this member + snd group event chat item you are observer @@ -9319,7 +10455,7 @@ last received msg: %2$@
- +
@@ -9356,7 +10492,7 @@ last received msg: %2$@
- +
@@ -9378,13 +10514,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9397,15 +10537,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9427,7 +10563,7 @@ last received msg: %2$@
- +
diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json index 6416a2d8fa..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" : "15F31d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.4" + "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 93b9725131..0e4e383b52 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. @@ -224,7 +224,7 @@ **Warning**: Instant push notifications require passphrase saved in Keychain. - **警告**:即時推送訊息通知需要數據庫的密碼儲存在資料庫中。 + **警告**:即時推送訊息通知需要將數據庫的密碼儲存在資料庫中。 No comment provided by engineer. @@ -2367,8 +2367,8 @@ We will be adding server redundancy to prevent lost messages. 請放置你的密碼於安全的地方,如果你遺失了密碼,將不可能修改你的密碼。 No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. 伺服器地址的憑證指紋可能不正確 server test error @@ -2704,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. @@ -2724,7 +2724,7 @@ We will be adding server redundancy to prevent lost messages. Send questions and ideas - 傳送問題和想法給開發者 + 給開發者提問題和想法 No comment provided by engineer. @@ -2757,8 +2757,8 @@ We will be adding server redundancy to prevent lost messages. 已傳送的訊息將在設定的時間後被刪除。 No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. 伺服器需要授權才能建立佇列,請檢查密碼 server test error @@ -2774,7 +2774,7 @@ We will be adding server redundancy to prevent lost messages. Set 1 day - 設定為1天 + 設定為 1 天 No comment provided by engineer. @@ -3022,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. @@ -3054,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. @@ -3071,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. @@ -3263,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. @@ -3283,7 +3283,7 @@ To connect, please ask your contact to create another connection link and check Verify connection security - 驗證連接安全性 + 驗證連線安全性 No comment provided by engineer. @@ -4000,7 +4000,7 @@ SimpleX 伺服器並不會看到你的個人檔案。 斜體 No comment provided by engineer. - + join as %@ 以 %@ 身份加入 No comment provided by engineer. @@ -4163,7 +4163,7 @@ SimpleX 伺服器並不會看到你的個人檔案。 via contact address link - 透過聯絡人的邀請連結連接 + 透過聯絡人的邀請連結連線 chat list item description @@ -4173,7 +4173,7 @@ SimpleX 伺服器並不會看到你的個人檔案。 via one-time link - 透過一次性連結連接 + 透過一次性連結連線 chat list item description @@ -4206,8 +4206,8 @@ SimpleX 伺服器並不會看到你的個人檔案。 pref value - - you are invited to group + + You are invited to group 你被邀請加入至群組 No comment provided by engineer. @@ -4702,7 +4702,7 @@ Available in v5.1 %u messages failed to decrypt. - %u 訊息解密失敗。 + %u 則訊息解密失敗。 No comment provided by engineer. @@ -4783,7 +4783,7 @@ Available in v5.1 Migrations: - 遷移:%@ + 遷移: No comment provided by engineer. @@ -5130,8 +5130,8 @@ Available in v5.1 儲存歡迎訊息? No comment provided by engineer. - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. 伺服器需要認證後才能上傳,檢查密碼 server test error @@ -5152,7 +5152,7 @@ Available in v5.1 Tap to activate profile. - 點擊以激活配置檔案。 + 點擊以激活設定檔。 No comment provided by engineer. @@ -5225,7 +5225,7 @@ SimpleX Lock must be enabled. <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> - <p>你好!</p> + <p>你好!</p> <p><a href="%@">來連接我透過SimpleX Chat</a></p> email text @@ -6034,7 +6034,7 @@ It can happen because of some bug or when the connection is compromised. %lld messages marked deleted - %lld 條訊息已刪除 + %lld 則訊息已標記為刪除 Already connecting! @@ -6046,7 +6046,7 @@ It can happen because of some bug or when the connection is compromised. (new) - (新) + (新) %@, %@ and %lld other members connected @@ -6112,6 +6112,374 @@ It can happen because of some bug or when the connection is compromised.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 ce80adf38f..5d619ac130 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -22,12 +22,6 @@ let nseSuspendSchedule: SuspendSchedule = (2, 4) let fastNSESuspendSchedule: SuspendSchedule = (1, 1) -enum NSENotification { - case nse(UNMutableNotificationContent) - case callkit(RcvCallInvitation) - case empty -} - public enum NSENotificationData { case connectionEvent(_ user: User, _ connEntity: ConnectionEntity) case contactConnected(_ user: any UserLike, _ contact: Contact) @@ -37,6 +31,7 @@ public enum NSENotificationData { case msgInfo(NtfMsgAckInfo) case noNtf + @inline(__always) var callInvitation: RcvCallInvitation? { switch self { case let .callInvitation(invitation): invitation @@ -56,8 +51,9 @@ public enum NSENotificationData { } } + @inline(__always) var notificationEvent: NSENotificationData? { - return switch self { + switch self { case .connectionEvent: self case .contactConnected: self case .contactRequest: self @@ -68,9 +64,10 @@ public enum NSENotificationData { } } - var newMsgData: (any UserLike, ChatInfo)? { - return switch self { - case let .messageReceived(user, cInfo, _): (user, cInfo) + @inline(__always) + var newMsgNtf: NSENotificationData? { + switch self { + case .messageReceived: self default: nil } } @@ -81,20 +78,25 @@ public enum NSENotificationData { // or when background notification is received. class NSEThreads { static let shared = NSEThreads() - 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 = [] - var activeThreads: [(UUID, NotificationService)] = [] - var droppedNotifications: [(ChatId, NSENotificationData)] = [] + 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 { @@ -103,24 +105,111 @@ class NSEThreads { } } + // atomically: + // - checks that passed NSE instance can start processing passed notification entity, + // - adds it to the passed NSE instance, + // - marks as started, if no other NSE instance is processing it. + // Making all these steps atomic prevents a race condition between threads when both will be added and none will be started + @inline(__always) + func startEntity(_ nse: NotificationService, _ ntfEntity: NotificationEntity) -> Bool { + queue.sync { + // checking that none of activeThreads with another NSE instance processes the same entity and is not ready + let canStart = !activeThreads.contains(where: { (tId, otherNSE) in + tId != nse.threadId + && otherNSE.notificationEntities.contains(where: { (id, otherEntity) in + id == ntfEntity.entityId + && otherEntity.expectedMsg != nil + }) + }) + // atomically add entity to passed NSE instance + let id = ntfEntity.entityId + nse.notificationEntities[id] = ntfEntity + if canStart { + // and set as started, so it cannot be chosen to start by another NSE entity in nextThread + nse.notificationEntities[id]?.startedProcessingNewMsgs = true + } + return canStart + } + } + + @inline(__always) + func addDroppedNtf(_ id: ChatId, _ ntf: NSENotificationData) { + queue.sync { droppedNotifications.append((id, ntf)) } + } + + // atomically remove and return first dropped notification for the passed entity + @inline(__always) + func takeDroppedNtf(_ ntfEntity: NotificationEntity) -> (entityId: ChatId, ntf: NSENotificationData)? { + queue.sync { + if droppedNotifications.isEmpty { + nil + } else if let i = droppedNotifications.firstIndex(where: { (id, _) in id == ntfEntity.entityId }) { + droppedNotifications.remove(at: i) + } else { + nil + } + } + } + + // passes notification for processing to NSE instance chosen by rcvEntityThread + @inline(__always) func processNotification(_ id: ChatId, _ ntf: NSENotificationData) async -> Void { - if let (_, nse) = rcvEntityThread(id), - nse.expectedMessages[id]?.shouldProcessNtf ?? false { - nse.processReceivedNtf(id, ntf, signalReady: true) + 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) + } } } - private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? { - NSEThreads.queue.sync { + // 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 in nse.expectedMessages - // 2) has not completed processing messages for this connection (not ready) - activeThreads.first(where: { (_, nse) in nse.expectedMessages[id]?.ready == false }) + // 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 { @@ -137,21 +226,53 @@ class NSEThreads { } } + @inline(__always) var noThreads: Bool { allThreads.isEmpty } } -struct ExpectedMessage { - var ntfConn: UserNtfConn - var receiveConnId: String? - var expectedMsgId: String? - var allowedGetNextAttempts: Int - var msgBestAttemptNtf: NSENotificationData? - var ready: Bool - var shouldProcessNtf: Bool - var startedProcessingNewMsgs: Bool - var semaphore: DispatchSemaphore +// 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. @@ -161,36 +282,46 @@ struct ExpectedMessage { class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? // served as notification if no message attempts (msgBestAttemptNtf) could be produced - var serviceBestAttemptNtf: NSENotification? + 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 expectedMessages: Dictionary = [:] // key is receiveEntityId + 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() } - setServiceBestAttemptNtf(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() - setServiceBestAttemptNtf(createAppStoppedNtf(badgeCount)) - deliverBestAttemptNtf() + contentHandler(createAppStoppedNtf(badgeCount)) case .suspended: - receiveNtfMessages(request, contentHandler) + setExpirationTimer() + receiveNtfMessages(request) case .suspending: + // 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()) @@ -206,119 +337,179 @@ 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) } } - case .active: contentHandler(UNMutableNotificationContent()) - case .activating: contentHandler(UNMutableNotificationContent()) - case .bgRefresh: contentHandler(UNMutableNotificationContent()) + 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 ntfConns = apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo) { + 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 }))") - for ntfConn in ntfConns { - addExpectedMessage(ntfConn: ntfConn) - } + // 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) - let connIdsToGet = expectedMessages.compactMap { (id, _) in - let started = NSEThreads.queue.sync { - let canStart = checkCanStart(id) - if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: can start: \(canStart)") } - if canStart { - processDroppedNotifications(id) - expectedMessages[id]?.startedProcessingNewMsgs = true - expectedMessages[id]?.shouldProcessNtf = true - } - return canStart - } - if started { - return expectedMessages[id]?.receiveConnId - } else { - if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) waiting on semaphore") } - expectedMessages[id]?.semaphore.wait() - if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) proceeding after semaphore") } - Task { - NSEThreads.queue.sync { - processDroppedNotifications(id) - expectedMessages[id]?.startedProcessingNewMsgs = true - expectedMessages[id]?.shouldProcessNtf = true + // 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 } - if let connId = expectedMessages[id]?.receiveConnId { - let _ = getConnNtfMessage(connId: connId) + } 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 } } - if !connIdsToGet.isEmpty { - if let r = apiGetConnNtfMessages(connIds: connIdsToGet) { - logger.debug("NotificationService: receiveNtfMessages: apiGetConnNtfMessages count = \(r.count)") + // Request messages for all connection entities that were not used by other instances. + // It triggers event delivery to receiveMessages loop (see above). + if !msgReqs.isEmpty, + let rcvMsgs = apiGetConnNtfMessages(connMsgReqs: msgReqs.map { $0.connMsgReq }) { + for i in 0 ..< min(msgReqs.count, rcvMsgs.count) { // a sanity check, API always returns the same size + if rcvMsgs[i].noMsg { + // mark entity as ready if there are no message on the server (or on error) + finalizeEntity(msgReqs[i].chatId) + } } - return } } else if let dbStatus = dbStatus { setServiceBestAttemptNtf(createErrorNtf(dbStatus, badgeCount)) } } + // try to deliver the best attempt before exiting deliverBestAttemptNtf() } - func addExpectedMessage(ntfConn: UserNtfConn) { - if let connEntity = ntfConn.connEntity_, - let receiveEntityId = connEntity.id, ntfConn.expectedMsg_ != nil { - let expectedMsgId = ntfConn.expectedMsg_?.msgId - logger.debug("NotificationService: addExpectedMessage: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private)") - expectedMessages[receiveEntityId] = ExpectedMessage( + @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, - receiveConnId: connEntity.conn.agentConnId, - expectedMsgId: expectedMsgId, - allowedGetNextAttempts: 3, - msgBestAttemptNtf: ntfConn.defaultBestAttemptNtf, - ready: false, - shouldProcessNtf: false, - startedProcessingNewMsgs: false, - semaphore: DispatchSemaphore(value: 0) + entityId: rcvEntityId, + expectedMsg: expectedMsg, + msgBestAttemptNtf: defaultBestAttemptNtf(ntfConn) ) } + return nil } - func checkCanStart(_ entityId: String) -> Bool { - return !NSEThreads.shared.activeThreads.contains(where: { - (tId, nse) in tId != threadId && nse.expectedMessages.contains(where: { $0.key == entityId }) - }) - } - - func processDroppedNotifications(_ entityId: String) { - if !NSEThreads.shared.droppedNotifications.isEmpty { - let messagesToProcess = NSEThreads.shared.droppedNotifications.filter { (eId, _) in eId == entityId } - NSEThreads.shared.droppedNotifications.removeAll(where: { (eId, _) in eId == entityId }) - for (index, (_, ntf)) in messagesToProcess.enumerated() { - if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entity \(entityId, privacy: .private): processing dropped notification \(index, privacy: .private)") } - processReceivedNtf(entityId, ntf, signalReady: false) + // 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() { @@ -326,73 +517,70 @@ class NotificationService: UNNotificationServiceExtension { deliverBestAttemptNtf(urgent: true) } + @inline(__always) var expectingMoreMessages: Bool { - !expectedMessages.allSatisfy { $0.value.ready } + notificationEntities.contains { $0.value.expectedMsg != nil } } - func processReceivedNtf(_ id: ChatId, _ ntf: NSENotificationData, signalReady: Bool) { - guard let expectedMessage = expectedMessages[id] else { - return - } - guard let expectedMsgTs = expectedMessage.ntfConn.expectedMsg_?.msgTs else { - NSEThreads.shared.droppedNotifications.append((id, ntf)) - if signalReady { entityReady(id) } - return - } + // processReceivedNtf returns "completed" - true when no more messages for the passed entity should be processed by the current NSE instance. + // This is used to call finalizeEntity(id) and by processDroppedNotifications to decide if further processing is needed. + func processReceivedNtf(_ ntfEntity: NotificationEntity, _ expectedMsg: NtfMsgInfo, _ ntf: NSENotificationData) -> Bool { + let id = ntfEntity.entityId if case let .msgInfo(info) = ntf { - if info.msgId == expectedMessage.expectedMsgId { + 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") - expectedMessages[id]?.expectedMsgId = nil - if signalReady { entityReady(id) } - self.deliverBestAttemptNtf() - } else if let msgTs = info.msgTs_, msgTs > expectedMsgTs { + return true + } else if let msgTs = info.msgTs_, msgTs > expectedMsg.msgTs { + // Otherwise check timestamp - if it is after the currently expected timestamp, preserve .msgInfo marker for the next instance. logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, let other instance to process it, stopping this one") - NSEThreads.shared.droppedNotifications.append((id, ntf)) - if signalReady { entityReady(id) } - self.deliverBestAttemptNtf() - } else if (expectedMessages[id]?.allowedGetNextAttempts ?? 0) > 0, let receiveConnId = expectedMessages[id]?.receiveConnId { + 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") - expectedMessages[id]?.allowedGetNextAttempts -= 1 - if let receivedMsg = getConnNtfMessage(connId: receiveConnId) { - logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)") + 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") - NSEThreads.shared.droppedNotifications.append((id, ntf)) - if signalReady { entityReady(id) } - self.deliverBestAttemptNtf() + return true } } else { + // Current instance needs more messages, but ran out of attempts - deliver what we have. logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unknown message, let other instance to process it") - NSEThreads.shared.droppedNotifications.append((id, ntf)) - if signalReady { entityReady(id) } - self.deliverBestAttemptNtf() + return true } - } else if expectedMessage.ntfConn.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") if ntf.notificationEvent != nil { setBadgeCount() } - let prevBestAttempt = expectedMessages[id]?.msgBestAttemptNtf - if prevBestAttempt?.callInvitation != nil { - if ntf.callInvitation != nil { // replace with newer call - expectedMessages[id]?.msgBestAttemptNtf = ntf - } // otherwise keep call as best attempt - } else { - expectedMessages[id]?.msgBestAttemptNtf = ntf - } + // 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 { - NSEThreads.shared.droppedNotifications.append((id, ntf)) - if signalReady { entityReady(id) } + // We should not get to this branch, as notifications are not delivered for muted users. + return true } } - func entityReady(_ entityId: ChatId) { - if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: entity \(entityId, privacy: .private)") } - expectedMessages[entityId]?.ready = true - if let (tNext, nse) = NSEThreads.shared.activeThreads.first(where: { (_, nse) in nse.expectedMessages[entityId]?.startedProcessingNewMsgs == false }) { - if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: signal next thread \(tNext, privacy: .private) for entity \(entityId, privacy: .private)") } - nse.expectedMessages[entityId]?.semaphore.signal() - } + func finalizeEntity(_ entityId: ChatId) { + if let t = threadId { logger.debug("NotificationService thread \(t): entityReady: entity \(entityId)") } + NSEThreads.shared.signalNextThread(self, entityId) + deliverBestAttemptNtf() } func setBadgeCount() { @@ -400,17 +588,22 @@ class NotificationService: UNNotificationServiceExtension { ntfBadgeCountGroupDefault.set(badgeCount) } + @inline(__always) func setServiceBestAttemptNtf(_ ntf: UNMutableNotificationContent) { logger.debug("NotificationService.setServiceBestAttemptNtf") - serviceBestAttemptNtf = .nse(ntf) + serviceBestAttemptNtf = ntf } private func deliverBestAttemptNtf(urgent: Bool = false) { - if (urgent || !expectingMoreMessages) { + 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 expectedMessages { - expectedMessages[key]?.shouldProcessNtf = false + for (key, _) in notificationEntities { + notificationEntities[key]?.shouldProcessNtf = false } let suspend: Bool @@ -420,26 +613,28 @@ class NotificationService: UNNotificationServiceExtension { } else { suspend = false } - deliverCallkitOrNotification(urgent: urgent, suspend: suspend) + deliverCallkitOrNotification(urgent: urgent, suspend: suspend, handler: handler) } } - private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) { - if useCallKit() && expectedMessages.contains(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }) { + @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) } } } @@ -458,66 +653,71 @@ class NotificationService: UNNotificationServiceExtension { } } } - deliverNotification() + deliverNotification(handler, callInv) } } - private func deliverNotification() { - if let handler = contentHandler, let ntf = prepareNotification() { - contentHandler = nil + private func deliverNotification(_ handler: @escaping (UNNotificationContent) -> Void, _ callInv: RcvCallInvitation?) { + if let serviceNtf = serviceBestAttemptNtf { serviceBestAttemptNtf = nil - switch ntf { - case let .nse(content): - content.badge = badgeCount as NSNumber - handler(content) - case let .callkit(invitation): - logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)") - CXProvider.reportNewIncomingVoIPPushPayload([ - "displayName": invitation.contact.displayName, - "contactId": invitation.contact.id, - "callUUID": invitation.callUUID ?? "", - "media": invitation.callType.media.rawValue, - "callTs": invitation.callTs.timeIntervalSince1970 - ]) { error in - logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") - handler(error == nil ? UNMutableNotificationContent() : createCallInvitationNtf(invitation, self.badgeCount)) + contentHandler = nil + if let callInv { + if useCallKit() { + logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(callInv.contact.id)") + CXProvider.reportNewIncomingVoIPPushPayload([ + "displayName": callInv.contact.displayName, + "contactId": callInv.contact.id, + "callUUID": callInv.callUUID ?? "", + "media": callInv.callType.media.rawValue, + "callTs": callInv.callTs.timeIntervalSince1970 + ]) { error in + logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") + handler(error == nil ? UNMutableNotificationContent() : createCallInvitationNtf(callInv, self.badgeCount)) + } + } else { + handler(createCallInvitationNtf(callInv, badgeCount)) } - case .empty: - handler(UNMutableNotificationContent()) // used to mute notifications that did not unsubscribe yet - } - } - } - - private func prepareNotification() -> NSENotification? { - if expectedMessages.isEmpty { - return serviceBestAttemptNtf - } else if let callNtfKV = expectedMessages.first(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }), - let callInv = callNtfKV.value.msgBestAttemptNtf?.callInvitation, - let callNtf = callNtfKV.value.msgBestAttemptNtf { - return useCallKit() ? .callkit(callInv) : .nse(callNtf.notificationContent(badgeCount)) - } else { - let ntfEvents = expectedMessages.compactMap { $0.value.msgBestAttemptNtf?.notificationEvent } - if ntfEvents.isEmpty { - return .empty - } else if let ntfEvent = ntfEvents.count == 1 ? ntfEvents.first : nil { - return .nse(ntfEvent.notificationContent(badgeCount)) + } else if notificationEntities.isEmpty { + handler(serviceNtf) } else { - return .nse(createJointNtf(ntfEvents)) + handler(prepareNotification()) } } } - private func createJointNtf(_ ntfEvents: [NSENotificationData]) -> UNMutableNotificationContent { + @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() - let newMsgsData: [(any UserLike, ChatInfo)] = ntfEvents.compactMap { $0.newMsgData } - if !newMsgsData.isEmpty, let userId = newMsgsData.first?.0.userId { - let newMsgsChats: [ChatInfo] = newMsgsData.map { $0.1 } - let uniqueChatsNames = uniqueNewMsgsChatsNames(newMsgsChats) - var body: String - if previewMode == .hidden { - body = String.localizedStringWithFormat(NSLocalizedString("New messages in %d chats", comment: "notification body"), uniqueChatsNames.count) + 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 { - body = String.localizedStringWithFormat(NSLocalizedString("From: %@", comment: "notification body"), newMsgsChatsNamesStr(uniqueChatsNames)) + String.localizedStringWithFormat(NSLocalizedString("From: %@", comment: "notification body"), newMsgsChatsNamesStr(chatsNames)) } return createNotification( categoryIdentifier: ntfCategoryManyEvents, @@ -530,24 +730,32 @@ class NotificationService: UNNotificationServiceExtension { return createNotification( categoryIdentifier: ntfCategoryManyEvents, title: NSLocalizedString("New events", comment: "notification"), - body: String.localizedStringWithFormat(NSLocalizedString("%d new events", comment: "notification body"), ntfEvents.count), + body: String.localizedStringWithFormat(NSLocalizedString("%d new events", comment: "notification body"), ntfs.count), badgeCount: badgeCount ) } } - private func uniqueNewMsgsChatsNames(_ newMsgsChats: [ChatInfo]) -> [String] { + @inline(__always) + private func newMsgsChatsNames(_ ntfs: [NSENotificationData]) -> (Int64?, [String]) { var seenChatIds = Set() - var uniqueChatsNames: [String] = [] - for chat in newMsgsChats { - if !seenChatIds.contains(chat.id) { - seenChatIds.insert(chat.id) - uniqueChatsNames.append(chat.chatViewName) + var chatsNames: [String] = [] + var userId: Int64? + for ntf in ntfs { + switch ntf { + case let .messageReceived(user, chat, _): + if seenChatIds.isEmpty { userId = user.userId } + if !seenChatIds.contains(chat.id) { + seenChatIds.insert(chat.id) + chatsNames.append(chat.chatViewName) + } + default: () } } - return uniqueChatsNames + return (userId, chatsNames) } + @inline(__always) private func newMsgsChatsNamesStr(_ names: [String]) -> String { return switch names.count { case 1: names[0] @@ -566,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) @@ -576,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) @@ -624,7 +831,7 @@ func startChat() -> DBMigrationResult? { startLock.wait() defer { startLock.signal() } - + if hasChatCtrl() { return switch NSEChatState.shared.value { case .created: doStartChat() @@ -654,7 +861,7 @@ 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 apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path) @@ -747,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: () } } @@ -762,17 +975,20 @@ 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, NSENotificationData)? { +@inline(__always) +func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)? { logger.debug("NotificationService receivedMsgNtf: \(res.responseType)") switch res { case let .contactConnected(user, contact, _): @@ -780,7 +996,11 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotificationData)? // case let .contactConnecting(contact): // TODO profile update case let .receivedContactRequest(user, contactRequest): - return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest)) + if let userContactLinkId = contactRequest.userContactLinkId_ { + return (UserContact(userContactLinkId: userContactLinkId).id, .contactRequest(user, contactRequest)) + } else { + return nil + } case let .newChatItems(user, chatItems): // Received items are created one at a time if let chatItem = chatItems.first { @@ -789,8 +1009,13 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotificationData)? if let file = cItem.autoReceiveFile() { cItem = autoReceiveFile(file) ?? cItem } - let ntf: NSENotificationData = (cInfo.ntfsEnabled && cItem.showNotification) ? .messageReceived(user, cInfo, cItem) : .noNtf - return (chatItem.chatId, ntf) + 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 } @@ -813,15 +1038,10 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotificationData)? 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 { @@ -836,14 +1056,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: @@ -853,101 +1073,93 @@ func apiGetActiveUser() -> User? { } func apiStartChat() throws -> Bool { - let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: 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 apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { - let r = sendSimpleXCmd(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) - if case .cmdOk = r { return } - throw r + 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 apiGetNtfConns(nonce: String, encNtfInfo: String) -> [UserNtfConn]? { +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(.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo)) - if case let .ntfConns(ntfConns) = r { - logger.debug("apiGetNtfConns response ntfConns: \(ntfConns.count)") - return ntfConns.compactMap { toUserNtfConn($0) } - } 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 toUserNtfConn(_ ntfConn: NtfConn) -> UserNtfConn? { - if let user = ntfConn.user_ { - return UserNtfConn(user: user, connEntity_: ntfConn.connEntity_, expectedMsg_: ntfConn.expectedMsg_) - } else { - return nil - } -} - -func apiGetConnNtfMessages(connIds: [String]) -> [NtfMsgInfo?]? { - guard apiGetActiveUser() != nil else { - logger.debug("no active user") - return nil - } - let r = sendSimpleXCmd(.apiGetConnNtfMessages(connIds: connIds)) - if case let .connNtfMessages(receivedMsgs) = r { - logger.debug("apiGetConnNtfMessages response receivedMsgs: \(receivedMsgs.count)") - return receivedMsgs - } - logger.debug("apiGetConnNtfMessages error: \(responseError(r))") - return nil -} - -func getConnNtfMessage(connId: String) -> NtfMsgInfo? { - let r_ = apiGetConnNtfMessages(connIds: [connId]) - if let r = r_, let receivedMsg = r.count == 1 ? r.first : nil { - return receivedMsg - } - 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 userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() - let r = sendSimpleXCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline)) - if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } - logger.error("receiveFile error: \(responseError(r))") + 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 userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() - let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted)) - if case .cmdOk = r { return } - logger.error("setFileToReceive error: \(responseError(r))") + 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? { @@ -964,38 +1176,30 @@ 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 UserNtfConn { - var user: User - var connEntity_: ConnectionEntity? - var expectedMsg_: NtfMsgInfo? - - var defaultBestAttemptNtf: NSENotificationData { - return if !user.showNotifications { - .noNtf - } else if let connEntity = connEntity_ { - switch 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 - } - } else { - .noNtf +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 let .userContactConnection(_, userContact): + userContact.groupId == nil + ? .connectionEvent(user, connEntity) + : .noNtf } } } diff --git a/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings index 6cc768efe1..2ea2a332d4 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 © 2024 SimpleX Chat. All rights reserved."; +"NSHumanReadableCopyright" = "Copyright © 2025 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 index f9779c6e05..ec502c53c6 100644 --- a/apps/ios/SimpleX NSE/de.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "%d neue Ereignisse"; +/* notification body */ +"From %d chat(s)" = "Von %d Chat(s)"; + /* notification body */ "From: %@" = "Von: %@"; @@ -10,6 +13,3 @@ /* notification */ "New messages" = "Neue Nachrichten"; -/* notification body */ -"New messages in %d chats" = "Neue Nachrichten in %d Chats"; - diff --git a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings index fb190400e1..685eb3d93d 100644 --- a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "%d evento(s) nuevo(s)"; +/* notification body */ +"From %d chat(s)" = "De %d chat(s)"; + /* notification body */ "From: %@" = "De: %@"; @@ -10,6 +13,3 @@ /* notification */ "New messages" = "Mensajes nuevos"; -/* notification body */ -"New messages in %d chats" = "Mensajes nuevos en %d chat(s)"; - diff --git a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings index 5ef592ec70..999bb3608f 100644 --- a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings @@ -1,7 +1,12 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d nouveaux événements"; + +/* notification body */ +"From: %@" = "De : %@"; + +/* notification */ +"New events" = "Nouveaux événements"; + +/* notification */ +"New messages" = "Nouveaux messages"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings index e64c98df9e..a6330b93db 100644 --- a/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "%d új esemény"; +/* notification body */ +"From %d chat(s)" = "%d csevegésből"; + /* notification body */ "From: %@" = "Tőle: %@"; @@ -10,6 +13,3 @@ /* notification */ "New messages" = "Új üzenetek"; -/* notification body */ -"New messages in %d chats" = "Új üzenetek %d csevegésben"; - diff --git a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings index 31f463eb5b..a6c1ec215b 100644 --- a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "%d nuovi eventi"; +/* notification body */ +"From %d chat(s)" = "Da %d chat"; + /* notification body */ "From: %@" = "Da: %@"; @@ -10,6 +13,3 @@ /* notification */ "New messages" = "Nuovi messaggi"; -/* notification body */ -"New messages in %d chats" = "Nuovi messaggi in %d chat"; - diff --git a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings index 4cf91689b5..83d42926d1 100644 --- a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "‐%d nieuwe gebeurtenissen"; +/* notification body */ +"From %d chat(s)" = "Van %d chat(s)"; + /* notification body */ "From: %@" = "Van: %@"; @@ -10,6 +13,3 @@ /* notification */ "New messages" = "Nieuwe berichten"; -/* notification body */ -"New messages in %d chats" = "Nieuwe berichten in %d chats"; - diff --git a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings index 5ef592ec70..3a577620a0 100644 --- a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings @@ -1,7 +1,3 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"New messages in %d chats" = "Nowe wiadomości w %d czatach"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings index 6ba39ccc63..cf082a166d 100644 --- a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "%d новых сообщений"; +/* notification body */ +"From %d chat(s)" = "Из %d чатов"; + /* notification body */ "From: %@" = "От: %@"; @@ -10,6 +13,3 @@ /* notification */ "New messages" = "Новые сообщения"; -/* notification body */ -"New messages in %d chats" = "Новые сообщения в %d разговоре(ах)"; - diff --git a/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings index 5ef592ec70..e43538b856 100644 --- a/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d yeni olaylar"; + +/* notification body */ +"From %d chat(s)" = "%d 'dan sohbetler"; + +/* notification body */ +"From: %@" = "Şuradan: %@@"; + +/* notification */ +"New events" = "Yeni etkinlikler"; + +/* notification */ +"New messages" = "Yeni mesajlar"; - 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 index 69cc53bff1..6dd5248aeb 100644 --- a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "%d нових подій"; +/* notification body */ +"From %d chat(s)" = "З %d чату(ів)"; + /* notification body */ "From: %@" = "Від: %@"; @@ -10,6 +13,3 @@ /* notification */ "New messages" = "Нові повідомлення"; -/* notification body */ -"New messages in %d chats" = "Нові повідомлення в чатах %d"; - diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift index fcb78c64b1..6495d09b03 100644 --- a/apps/ios/SimpleX SE/ShareAPI.swift +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -13,91 +13,93 @@ import SimpleXChat let logger = Logger() func apiGetActiveUser() throws -> User? { - let r = sendSimpleXCmd(.showActiveUser) + let r: APIResult = sendSimpleXCmd(SEChatCommand.showActiveUser) 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 apiStartChat() throws -> Bool { - let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: true)) + let r: APIResult = sendSimpleXCmd(SEChatCommand.startChat(mainApp: false, enableSndFiles: true)) 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 apiSetNetworkConfig(_ cfg: NetCfg) throws { - let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg)) - if case .cmdOk = r { return } - throw r + 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 = sendSimpleXCmd(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) - if case .cmdOk = r { return } - throw r + 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 = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable)) - if case .cmdOk = r { return } - throw r + 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 = sendSimpleXCmd(.apiGetChats(userId: userId)) - if case let .apiChats(user: _, chats: chats) = r { return chats } - throw r +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 = sendSimpleXCmd( + let r: APIResult = sendSimpleXCmd( chatInfo.chatType == .local - ? .apiCreateChatItems( + ? SEChatCommand.apiCreateChatItems( noteFolderId: chatInfo.apiId, composedMessages: composedMessages ) - : .apiSendMessages( + : SEChatCommand.apiSendMessages( type: chatInfo.chatType, id: chatInfo.apiId, + scope: chatInfo.groupChatScope(), live: false, ttl: nil, composedMessages: composedMessages ) ) - if case let .newChatItems(_, chatItems) = r { + if case let .result(.newChatItems(_, chatItems)) = r { return chatItems } else { for composedMessage in composedMessages { if let filePath = composedMessage.fileSource?.filePath { removeFile(filePath) } } - throw r + throw r.unexpected } } func apiActivateChat() throws { chatReopenStore() - let r = sendSimpleXCmd(.apiActivateChat(restoreChat: false)) - if case .cmdOk = r { return } - throw r + let r: APIResult = sendSimpleXCmd(SEChatCommand.apiActivateChat(restoreChat: false)) + if case .result(.cmdOk) = r { return } + throw r.unexpected } func apiSuspendChat(expired: Bool) { - let r = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000)) + 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 .cmdOk = r, !expired { + if case .result(.cmdOk) = r, !expired { let startTime = CFAbsoluteTimeGetCurrent() while CFAbsoluteTimeGetCurrent() - startTime < 3 { - switch recvSimpleXMsg(messageTimeout: 3_500000) { - case .chatSuspended: + let msg: APIResult? = recvSimpleXMsg(messageTimeout: 3_500000) + switch msg { + case .result(.chatSuspended): suspended = false break default: continue @@ -105,9 +107,167 @@ func apiSuspendChat(expired: Bool) { } } if !suspended { - _ = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: 0)) + 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, scope: GroupChatScope?, 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, scope, live, ttl, composedMessages): + let msgs = encodeJSON(composedMessages) + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + } + } + + func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String { + "\(type.rawValue)\(id)\(scopeRef(scope: scope))" + } + + func scopeRef(scope: GroupChatScope?) -> String { + switch (scope) { + case .none: "" + case let .memberSupport(groupMemberId_): + if let groupMemberId = groupMemberId_ { + "(_support:\(groupMemberId))" + } else { + "(_support)" + } + case .reports: + "(reports, prohibited)" // can't use surrogate Reports scope + } + } +} + +enum SEChatResponse: Decodable, ChatAPIResult { + case activeUser(user: User) + case chatStarted + case chatRunning + case apiChats(user: UserRef, chats: [SEChatData]) + 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 = seParseApiChats(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))") + } + } +} + +public struct SEChatData: Decodable, Identifiable, Hashable, ChatLike { + public var chatInfo: ChatInfo + + public var id: ChatId { get { chatInfo.id } } + + public init(chatInfo: ChatInfo) { + self.chatInfo = chatInfo + } + + public static func invalidJSON(_ json: Data?) -> SEChatData { + SEChatData(chatInfo: .invalidJSON(json: json)) + } +} + +public func seParseApiChats(_ jResp: NSDictionary) -> (user: UserRef, chats: [SEChatData])? { + if let jApiChats = jResp["apiChats"] as? NSDictionary, + let user: UserRef = try? decodeObject(jApiChats["user"] as Any), + let jChats = jApiChats["chats"] as? NSArray { + let chats: [SEChatData] = jChats.map { jChat in + if let jChatDict = jChat as? NSDictionary, + let jChatInfo = jChatDict["chatInfo"], + let chatInfo: ChatInfo = try? decodeObject(jChatInfo) { + return SEChatData(chatInfo: chatInfo) + } + return SEChatData.invalidJSON(serializeJSON(jChat, options: .prettyPrinted)) + } + return (user, chats) + } else { + return nil + } +} diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index f75624702f..3d756957b6 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -19,11 +19,11 @@ private let MAX_DOWNSAMPLE_SIZE: Int64 = 2000 class ShareModel: ObservableObject { @Published var sharedContent: SharedContent? - @Published var chats: [ChatData] = [] + @Published var chats: [SEChatData] = [] @Published var profileImages: [ChatInfo.ID: UIImage] = [:] @Published var search = "" @Published var comment = "" - @Published var selected: ChatData? + @Published var selected: SEChatData? @Published var isLoaded = false @Published var bottomBar: BottomBar = .loadingSpinner @Published var errorAlert: ErrorAlert? @@ -60,13 +60,13 @@ class ShareModel: ObservableObject { } } - func isProhibited(_ chat: ChatData?) -> Bool { + func isProhibited(_ chat: SEChatData?) -> Bool { if let chat, let sharedContent { sharedContent.prohibited(in: chat, hasSimplexLink: hasSimplexLink) } else { false } } - var filteredChats: [ChatData] { + var filteredChats: [SEChatData] { search.isEmpty ? filterChatsToForwardTo(chats: chats) : filterChatsToForwardTo(chats: chats) @@ -179,7 +179,7 @@ class ShareModel: ObservableObject { resetChatCtrl() // Clears retained migration result registerGroupDefaults() haskell_init_se() - let (_, result) = chatMigrateInit(dbKey, confirmMigrations: defaultMigrationConfirmation()) + let (_, result) = chatMigrateInit(dbKey, confirmMigrations: defaultMigrationConfirmation(), backgroundMode: false) if let e = migrationError(result) { return e } try apiSetAppFilePaths( filesFolder: getAppFilesDirectory().path, @@ -253,7 +253,7 @@ class ShareModel: ObservableObject { } } - private func fetchChats() -> Result, ErrorAlert> { + private func fetchChats() -> Result, ErrorAlert> { do { guard let user = try apiGetActiveUser() else { return .failure( @@ -303,8 +303,9 @@ class ShareModel: ObservableObject { } } } - switch recvSimpleXMsg(messageTimeout: 1_000_000) { - case let .sndFileProgressXFTP(_, ci, _, sentSize, totalSize): + 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 { @@ -313,14 +314,14 @@ class ShareModel: ObservableObject { bottomBar = .loadingBar(progress: progress) } } - case let .sndFileCompleteXFTP(_, ci, _): + 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 .chatItemsStatusesUpdated(_, chatItems): + 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 { @@ -342,17 +343,15 @@ class ShareModel: ObservableObject { } } } - case let .sndFileError(_, ci, _, errorMessage): + 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 .sndFileWarning(_, 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 .chatError(_, chatError): - return ErrorAlert(chatError) - case let .chatCmdError(_, chatError): + case let .error(chatError): return ErrorAlert(chatError) default: continue } @@ -391,13 +390,13 @@ enum SharedContent { switch self { case let .image(preview, _): .image(text: comment, image: preview) case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration) - case let .url(preview): .link(text: preview.uri.absoluteString + (comment == "" ? "" : "\n" + comment), preview: preview) + case let .url(preview): .link(text: preview.uri + (comment == "" ? "" : "\n" + comment), preview: preview) case .text: .text(comment) case .data: .file(comment) } } - func prohibited(in chatData: ChatData, hasSimplexLink: Bool) -> Bool { + func prohibited(in chatData: SEChatData, hasSimplexLink: Bool) -> Bool { chatData.prohibitedByPref( hasSimplexLink: hasSimplexLink, isMediaOrFileAttachment: cryptoFile != nil, @@ -465,12 +464,13 @@ fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result some View { + private func linkPreview(_ linkPreview: LinkPreview) -> some View { previewArea { HStack(alignment: .center, spacing: 8) { if let uiImage = imageFromBase64(linkPreview.image) { @@ -172,7 +172,7 @@ struct ShareView: View { VStack(alignment: .center, spacing: 4) { Text(linkPreview.title) .lineLimit(1) - Text(linkPreview.uri.absoluteString) + Text(linkPreview.uri) .font(.caption) .lineLimit(1) .foregroundColor(.secondary) diff --git a/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings index 48f774742e..4a387a4361 100644 --- a/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings @@ -5,5 +5,5 @@ "CFBundleName" = "SimpleX SE"; /* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Alle Rechte vorbehalten."; +"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 index 081d7f8c66..ed96f44a15 100644 --- a/apps/ios/SimpleX SE/de.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/de.lproj/Localizable.strings @@ -17,13 +17,13 @@ "Comment" = "Kommentieren"; /* No comment provided by engineer. */ -"Currently maximum supported file size is %@." = "Die maximale erlaubte Dateigröße beträgt aktuell %@."; +"Currently maximum supported file size is %@." = "Die maximal erlaubte Dateigröße beträgt aktuell %@."; /* No comment provided by engineer. */ -"Database downgrade required" = "Datenbank-Herabstufung erforderlich"; +"Database downgrade required" = "Datenbank-Herunterstufung ist erforderlich"; /* No comment provided by engineer. */ -"Database encrypted!" = "Datenbank verschlüsselt!"; +"Database encrypted!" = "Datenbank ist verschlüsselt!"; /* No comment provided by engineer. */ "Database error" = "Datenbankfehler"; @@ -32,7 +32,7 @@ "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." = "Ein Datenbank-Passwort ist erforderlich, um den Chat zu öffnen."; +"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"; @@ -47,7 +47,7 @@ "Error: %@" = "Fehler: %@"; /* No comment provided by engineer. */ -"File error" = "Dateifehler"; +"File error" = "Datei-Fehler"; /* No comment provided by engineer. */ "Incompatible database version" = "Datenbank-Version nicht kompatibel"; @@ -68,19 +68,19 @@ "Ok" = "OK"; /* No comment provided by engineer. */ -"Open the app to downgrade the database." = "Öffne die App, um die Datenbank herabzustufen."; +"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." = "Öffne die App, um die Datenbank zu aktualisieren."; +"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 erstelle ein Profil in der SimpleX-App"; +"Please create a profile in the SimpleX app" = "Bitte erstellen Sie in der SimpleX-App ein Profil"; /* No comment provided by engineer. */ -"Selected chat preferences prohibit this message." = "Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt."; +"Selected chat preferences prohibit this message." = "Die gewählten Chat-Einstellungen erlauben diese Nachricht nicht."; /* No comment provided by engineer. */ "Sending a message takes longer than expected." = "Das Senden einer Nachricht dauert länger als erwartet."; @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Falsches Datenbank-Passwort"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Du kannst das Teilen in den Einstellungen zu Datenschutz & Sicherheit - SimpleX-Sperre erlauben."; +"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/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings index 94f18db853..2fedf0e6f1 100644 --- a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -2,7 +2,7 @@ "%@" = "%@"; /* No comment provided by engineer. */ -"App is locked!" = "Az alkalmazás zárolva!"; +"App is locked!" = "Az alkalmazás zárolva van!"; /* No comment provided by engineer. */ "Cancel" = "Mégse"; @@ -17,7 +17,7 @@ "Comment" = "Hozzászólás"; /* No comment provided by engineer. */ -"Currently maximum supported file size is %@." = "Jelenleg a maximálisan támogatott fájlméret: %@."; +"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"; @@ -29,19 +29,19 @@ "Database error" = "Adatbázishiba"; /* No comment provided by engineer. */ -"Database passphrase is different from saved in the keychain." = "Az adatbázis jelmondata eltér a kulcstartóban lévőtő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 required" = "Adatbázis fejlesztése szükséges"; /* No comment provided by engineer. */ -"Error preparing file" = "Hiba a fájl előkészítésekor"; +"Error preparing file" = "Hiba történt a fájl előkészítésekor"; /* No comment provided by engineer. */ -"Error preparing message" = "Hiba az üzenet előkészítésekor"; +"Error preparing message" = "Hiba történt az üzenet előkészítésekor"; /* No comment provided by engineer. */ "Error: %@" = "Hiba: %@"; @@ -80,7 +80,7 @@ "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 kiválasztott csevegési beállítások tiltják ezt az üzenetet."; +"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."; @@ -92,7 +92,7 @@ "Share" = "Megosztás"; /* No comment provided by engineer. */ -"Slow network?" = "Lassú internetkapcsolat?"; +"Slow network?" = "Lassú a hálózata?"; /* No comment provided by engineer. */ "Unknown database error: %@" = "Ismeretlen adatbázishiba: %@"; @@ -104,7 +104,7 @@ "Wait" = "Várjon"; /* No comment provided by engineer. */ -"Wrong database passphrase" = "Hibás adatbázis-jelmondat"; +"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--iOS--Info.plist b/apps/ios/SimpleX--iOS--Info.plist index 6f7d6c2395..72bd9b0dc3 100644 --- a/apps/ios/SimpleX--iOS--Info.plist +++ b/apps/ios/SimpleX--iOS--Info.plist @@ -56,5 +56,7 @@ remote-notification voip + UIDesignRequiresCompatibility + diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index e4fdd2b556..2d08d1159b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -145,7 +145,6 @@ 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 */; }; @@ -166,23 +165,31 @@ 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; }; 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; - 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D82CFE07CF00536B68 /* libffi.a */; }; - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j-ghc9.6.3.a */; }; - 649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DA2CFE07CF00536B68 /* libgmpxx.a */; }; - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j.a */; }; - 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DC2CFE07CF00536B68 /* libgmp.a */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; + 64A779F62DBFB9F200FDEF2F /* MemberAdmissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */; }; + 64A779F82DBFDBF200FDEF2F /* MemberSupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */; }; + 64A779FC2DC1040000FDEF2F /* SecondaryChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */; }; + 64A779FE2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */; }; + 64A77A022DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; }; 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; + 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; + 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.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 */; }; + 64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */; }; + 64E5E3672DFC16A900A4D530 /* ContextProfilePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.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 */; }; + 64FC8F9D2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.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 */; }; @@ -198,13 +205,19 @@ 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 */; }; @@ -224,7 +237,6 @@ 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 */; }; - CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA861C2C2ABCB50084E1EA /* ReverseList.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 */; }; @@ -233,10 +245,13 @@ D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; + E559A0A12E3F77EE00B26F74 /* CommandsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */; }; E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; }; 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 */ @@ -496,7 +511,6 @@ 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 = ""; }; @@ -518,24 +532,32 @@ 648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = ""; }; 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - 649B28D82CFE07CF00536B68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j-ghc9.6.3.a"; sourceTree = ""; }; - 649B28DA2CFE07CF00536B68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j.a"; sourceTree = ""; }; - 649B28DC2CFE07CF00536B68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; + 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAdmissionView.swift; sourceTree = ""; }; + 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberSupportView.swift; sourceTree = ""; }; + 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryChatView.swift; sourceTree = ""; }; + 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberSupportChatToolbar.swift; sourceTree = ""; }; + 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextPendingMemberActionsView.swift; sourceTree = ""; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; }; 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.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.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 = ""; }; + 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContactRequestActionsView.swift; sourceTree = ""; }; + 64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextProfilePickerView.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 = ""; }; + 64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMemberContactActionsView.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 = ""; }; @@ -550,12 +572,18 @@ 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 = ""; }; @@ -574,13 +602,13 @@ 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 = ""; }; - CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseList.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 = ""; }; + E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandsMenuView.swift; sourceTree = ""; }; E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; @@ -633,6 +661,8 @@ 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 */ @@ -673,14 +703,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j.a in Frameworks */, + 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, + 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, + 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j-ghc9.6.3.a in Frameworks */, - 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -735,21 +765,26 @@ 5C5F4AC227A5E9AF00B51EF1 /* Chat */ = { isa = PBXGroup; children = ( + 8CC317452D4FEBA800292A20 /* ScrollViewCells.swift */, + 8CC317432D4FEB9B00292A20 /* EndlessScrollView.swift */, 6440CA01288AEC770062C672 /* Group */, 5CE4407427ADB657007B033A /* ChatItem */, 5CEACCE527DE977C000BD591 /* ComposeMessage */, 5C2E260E27A30FDC00F70299 /* ChatView.swift */, 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */, 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */, + E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */, 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */, 5CE4407127ADB1D0007B033A /* Emoji.swift */, 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */, 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */, - CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */, 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */, 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */, 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */, + 8CB15E9F2CFDA30600C28209 /* ChatItemsMerger.swift */, + 8CAEF14F2D11A6A000240F00 /* ChatItemsLoader.swift */, + 8CAD466E2D15A8100078D18F /* ChatScrollHelpers.swift */, ); path = Chat; sourceTree = ""; @@ -757,11 +792,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 649B28D82CFE07CF00536B68 /* libffi.a */, - 649B28DC2CFE07CF00536B68 /* libgmp.a */, - 649B28DA2CFE07CF00536B68 /* libgmpxx.a */, - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j-ghc9.6.3.a */, - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.1-ClRYkrcEf1740ljH8FNP2j.a */, + 64C829992D54AEEE006B9E89 /* libffi.a */, + 64C829982D54AEED006B9E89 /* libgmp.a */, + 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a */, ); path = Libraries; sourceTree = ""; @@ -781,6 +816,7 @@ 5C764E87279CBC8E000C6508 /* Model */ = { isa = PBXGroup; children = ( + E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */, 5C764E88279CBCB3000C6508 /* ChatModel.swift */, 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, @@ -976,6 +1012,7 @@ isa = PBXGroup; children = ( 5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */, + E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */, 5CDCAD472818589900503DA2 /* NotificationService.swift */, 5CDCAD492818589900503DA2 /* Info.plist */, 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */, @@ -992,9 +1029,9 @@ 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */, 5CDCAD7428188D2900503DA2 /* APITypes.swift */, 5C5E5D3C282447AB00B0488A /* CallTypes.swift */, + 5CDCAD7D2818941F00503DA2 /* API.swift */, CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */, 5C9FD96A27A56D4D0075386C /* JSON.swift */, - 5CDCAD7D2818941F00503DA2 /* API.swift */, 5CDCAD80281A7E2700503DA2 /* Notifications.swift */, 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */, @@ -1055,7 +1092,10 @@ 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */, 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */, D72A9087294BD7A70047C86D /* NativeTextEditor.swift */, - 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */, + 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */, + 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */, + 64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */, + 64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */, ); path = ComposeMessage; sourceTree = ""; @@ -1096,6 +1136,11 @@ 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */, 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */, 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */, + B70CE9E52D4BE5930080F36D /* GroupMentions.swift */, + 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */, + 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */, + 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */, + 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */, ); path = Group; sourceTree = ""; @@ -1405,13 +1450,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */, 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 */, + 64A779FE2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */, + 64A779F82DBFDBF200FDEF2F /* MemberSupportView.swift in Sources */, 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, B79ADAFF2CE4EF930083DFFD /* AddressCreationCard.swift in Sources */, @@ -1424,10 +1471,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 */, 8C74C3EC2C1B92A900039E77 /* Theme.swift in Sources */, - 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */, 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, @@ -1445,6 +1492,7 @@ 5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */, 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */, 5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */, + 64A779FC2DC1040000FDEF2F /* SecondaryChatView.swift in Sources */, 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, @@ -1452,6 +1500,7 @@ B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */, 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */, D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */, + B70CE9E62D4BE5930080F36D /* GroupMentions.swift in Sources */, CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */, 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, @@ -1466,6 +1515,7 @@ 8CBC14862D357CDB00BBD901 /* StorageView.swift in Sources */, 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */, 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */, + E559A0A12E3F77EE00B26F74 /* CommandsMenuView.swift in Sources */, 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */, 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, @@ -1473,10 +1523,12 @@ 5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */, 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */, 5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */, + 64E5E3672DFC16A900A4D530 /* ContextProfilePickerView.swift in Sources */, 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */, 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, + 64FC8F9D2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift in Sources */, 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */, 640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */, 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */, @@ -1486,6 +1538,7 @@ 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */, 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, + 64A77A022DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift in Sources */, 8C74C3EA2C1B90AF00039E77 /* ThemeManager.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, @@ -1512,10 +1565,12 @@ 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 */, @@ -1523,6 +1578,7 @@ 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 */, @@ -1535,6 +1591,7 @@ 5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */, B70A39732D24090D00E80A5F /* TagListView.swift in Sources */, 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */, + 64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */, 6440CA00288857A10062C672 /* CIEventView.swift in Sources */, 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */, 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, @@ -1561,7 +1618,9 @@ 1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */, 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */, 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */, + 64A779F62DBFB9F200FDEF2F /* MemberAdmissionView.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 */, @@ -1586,6 +1645,7 @@ buildActionMask = 2147483647; files = ( 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */, + E5DDBE702DC4217900A0EFF0 /* NSEAPITypes.swift in Sources */, 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1943,7 +2003,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1968,7 +2028,8 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; + OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1992,7 +2053,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2017,7 +2078,8 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; + OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -2033,11 +2095,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2053,11 +2115,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2078,7 +2140,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2093,7 +2155,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2115,7 +2177,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2130,7 +2192,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2152,7 +2214,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2178,7 +2240,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2203,7 +2265,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2229,7 +2291,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2254,7 +2316,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2269,7 +2331,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2288,7 +2350,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 260; + CURRENT_PROJECT_VERSION = 315; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2303,7 +2365,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 987f7f3d41..40cee93faf 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, retryNum: Int32 = 0) -> APIResult { + if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl, retryNum: retryNum) { + decodeAPIResult(d) + } else { + APIResult.error(.invalidJSON(json: nil)) + } +} + +@inline(__always) +public func sendSimpleXCmdStr(_ cmd: String, _ ctrl: chat_ctrl? = nil, retryNum: Int32) -> Data? { + var c = cmd.cString(using: .utf8)! + return if let cjson = chat_send_cmd_retry(ctrl ?? getChatCtrl(), &c, retryNum) { + dataFromCString(cjson) + } else { + nil + } } // in microseconds public let MESSAGE_TIMEOUT: Int32 = 15_000_000 -public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> ChatResponse? { - if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout) { - 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,58 @@ struct ParsedServerAddress: Decodable { var parseError: String } +public func parseSanitizeUri(_ s: String, safe: Bool) -> ParsedUri? { + var c = s.cString(using: .utf8)! + if let cjson = chat_parse_uri(&c, safe ? 1 : 0) { + if let d = dataFromCString(cjson) { + do { + return try jsonDecoder.decode(ParsedUri.self, from: d) + } catch { + logger.error("parseSanitizeUri jsonDecoder.decode error: \(error.localizedDescription)") + } + } + } + return nil +} + +public struct ParsedUri: Decodable { + public var uriInfo: UriInfo? + public var parseError: String +} + +public struct UriInfo: Decodable { + public var scheme: String + public var sanitized: String? +} + +@inline(__always) public func fromCString(_ c: UnsafeMutablePointer) -> String { let s = String.init(cString: c) 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(serializeJSON(jChat, options: .prettyPrinted) ?? "") - } - 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: errorJson(jError) ?? "")) - } - } else if type == "chatError" { - if let jError = jResp["chatError"] as? NSDictionary { - return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? "")) - } - } - } - json = serializeJSON(j, options: .prettyPrinted) - } - 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,7 +245,7 @@ private func decodeUser_(_ jDict: NSDictionary) -> UserRef? { } } -private func errorJson(_ jDict: NSDictionary) -> String? { +public func errorJson(_ jDict: NSDictionary) -> Data? { if let chatError = jDict["chatError"] { serializeJSON(chatError) } else { @@ -247,10 +253,15 @@ private func errorJson(_ jDict: NSDictionary) -> String? { } } -func parseChatData(_ jChat: Any) throws -> ChatData { +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) { @@ -259,16 +270,18 @@ func parseChatData(_ jChat: Any) throws -> ChatData { return ChatItem.invalidJSON( chatDir: decodeProperty(jCI, "chatDir"), meta: decodeProperty(jCI, "meta"), - json: serializeJSON(jCI, options: .prettyPrinted) ?? "" + 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) @@ -276,28 +289,52 @@ func decodeProperty(_ obj: Any, _ prop: NSString) -> T? { return nil } -func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> String? { - if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) { - 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 { @@ -336,15 +373,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 4ae9bda0f2..fce0f100f2 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -13,1687 +13,183 @@ import Network public let jsonDecoder = getJSONDecoder() public let jsonEncoder = getJSONEncoder() -public enum ChatCommand { - 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(type: ChatType, id: Int64, 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, msg: MsgContent, live: Bool) - case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) - case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64]) - 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 apiDeleteToken(token: DeviceToken) - case apiGetNtfConns(nonce: String, encNtfInfo: String) - case apiGetConnNtfMessages(connIds: [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 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, incognito: Bool) - case apiSetConnectionIncognito(connId: Int64, incognito: Bool) - case apiChangeConnectionUser(connId: Int64, userId: Int64) - 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, 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) - 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) - - public 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(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 .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, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.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 .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 .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" - case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)" - case let .apiGetConnNtfMessages(connIds): return "/_ntf conn messages \(connIds.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 .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 .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, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" - case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" - case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" - case let .apiConnectPlan(userId, 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, 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): 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): 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 - } - } - } - - 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 .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 .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 .apiDeleteToken: return "apiDeleteToken" - case .apiGetNtfConns: return "apiGetNtfConns" - case .apiGetConnNtfMessages: return "apiGetConnNtfMessages" - 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 .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" - } - } - - public 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) - } +public protocol ChatCmdProtocol { + var cmdString: String { get } } -private func onOff(_ b: Bool) -> String { +@inline(__always) +public func onOff(_ b: Bool) -> String { b ? "on" : "off" } -public struct APIResponse: Decodable { - var resp: ChatResponse -} - -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 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 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 tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64]) - case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection) - case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) - case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) - 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 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 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, connReqContact: String) - case userContactLinkDeleted(user: User) - 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 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 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 chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) - 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]) - // 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 businessLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, fromContact: Contact) - case userDeletedMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) - case leftMemberUser(user: UserRef, groupInfo: GroupInfo) - case groupMembers(user: UserRef, group: Group) - case receivedGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole) - case groupDeletedUser(user: UserRef, groupInfo: GroupInfo) - case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) - case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) - case memberRoleUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) - case 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?, 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 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, 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 callInvitations(callInvitations: [RcvCallInvitation]) - case ntfTokenStatus(status: NtfTknStatus) - case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String) - case ntfConns(ntfConns: [NtfConn]) - case connNtfMessages(receivedMsgs: [NtfMsgInfo?]) - case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) - case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) - case contactDisabled(user: UserRef, contact: Contact) - // 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 agentSubsTotal(user: UserRef, subsTotal: SMPServerSubs, hasSession: Bool) - case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary) - case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs) - case chatCmdError(user_: UserRef?, chatError: ChatError) - case chatError(user_: UserRef?, chatError: ChatError) - case archiveExported(archiveErrors: [ArchiveError]) - 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 .chatTags: return "chatTags" - case .chatItemInfo: return "chatItemInfo" - case .serverTestResult: return "serverTestResult" - case .serverOperatorConditions: return "serverOperators" - case .userServers: return "userServers" - case .userServersValidation: return "userServersValidation" - case .usageConditions: return "usageConditions" - case .chatItemTTL: return "chatItemTTL" - case .networkConfig: return "networkConfig" - case .contactInfo: return "contactInfo" - case .groupMemberInfo: return "groupMemberInfo" - case .queueInfo: return "queueInfo" - 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 .tagsUpdated: return "tagsUpdated" - case .invitation: return "invitation" - case .connectionIncognitoUpdated: return "connectionIncognitoUpdated" - case .connectionUserChanged: return "connectionUserChanged" - case .connectionPlan: return "connectionPlan" - case .sentConfirmation: return "sentConfirmation" - case .sentInvitation: return "sentInvitation" - case .sentInvitationToContact: return "sentInvitationToContact" - case .contactAlreadyExists: return "contactAlreadyExists" - 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 .groupAliasUpdated: return "groupAliasUpdated" - 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 .contactSndReady: return "contactSndReady" - 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 .newChatItems: return "newChatItems" - case .groupChatItemsDeleted: return "groupChatItemsDeleted" - case .forwardPlan: return "forwardPlan" - case .chatItemsStatusesUpdated: return "chatItemsStatusesUpdated" - case .chatItemUpdated: return "chatItemUpdated" - case .chatItemNotChanged: return "chatItemNotChanged" - case .chatItemReaction: return "chatItemReaction" - case .reactionMembers: return "reactionMembers" - case .chatItemsDeleted: return "chatItemsDeleted" - case .contactsList: return "contactsList" - case .groupCreated: return "groupCreated" - case .sentGroupInvitation: return "sentGroupInvitation" - case .userAcceptedGroupSent: return "userAcceptedGroupSent" - case .groupLinkConnecting: return "groupLinkConnecting" - case .businessLinkConnecting: return "businessLinkConnecting" - 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 .rcvFileWarning: return "rcvFileWarning" - 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 .sndFileWarning: return "sndFileWarning" - 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 .ntfConns: return "ntfConns" - case .connNtfMessages: return "connNtfMessages" - case .ntfMessage: return "ntfMessage" - case .contactConnectionDeleted: return "contactConnectionDeleted" - case .contactDisabled: return "contactDisabled" - 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 .agentSubsTotal: return "agentSubsTotal" - case .agentServersSummary: return "agentServersSummary" - case .agentSubsSummary: return "agentSubsSummary" - case .chatCmdError: return "chatCmdError" - case .chatError: return "chatError" - case .archiveExported: return "archiveExported" - 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 .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 .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 .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))") - 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 .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\newUserId: \(String(describing: newUser.userId))") - 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 .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 .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, 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 .contactSndReady(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 .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 .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 .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 .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 .businessLinkConnecting(u, groupInfo, hostMember, fromContact): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))\nfromContact: \(String(describing: fromContact))") - 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, 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 .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, _, 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 .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 .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 .contactDisabled(u, contact): return withUser(u, String(describing: contact)) - 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 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)") - 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 .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) - case let .chatError(u, chatError): return withUser(u, String(describing: chatError)) - case let .archiveExported(archiveErrors): return String(describing: archiveErrors) - 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 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" - } - } - - public var isEntity: Bool { - switch self { - case .entity: return true - default: return false - } - } -} - -public enum ConnectionPlan: Decodable, Hashable { - case invitationLink(invitationLinkPlan: InvitationLinkPlan) - case contactAddress(contactAddressPlan: ContactAddressPlan) - case groupLink(groupLinkPlan: GroupLinkPlan) -} - -public enum InvitationLinkPlan: Decodable, Hashable { - case ok - case ownLink - case connecting(contact_: Contact?) - case known(contact: Contact) -} - -public enum ContactAddressPlan: Decodable, Hashable { - case ok - case ownLink - case connectingConfirmReconnect - case connectingProhibit(contact: Contact) - case known(contact: Contact) - case contactViaAddress(contact: Contact) -} - -public enum GroupLinkPlan: Decodable, Hashable { - case ok - case ownLink(groupInfo: GroupInfo) - case connectingConfirmReconnect - case connectingProhibit(groupInfo_: GroupInfo?) - case known(groupInfo: GroupInfo) -} - -struct NewUser: Encodable { - var profile: Profile? - var pastTimestamp: Bool -} - -public enum ChatPagination { - case last(count: Int) - case after(chatItemId: Int64, count: Int) - case before(chatItemId: Int64, count: Int) - case around(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)" - case let .around(chatItemId, count): return "around=\(chatItemId) count=\(count)" - } - } -} - -public struct ChatTagData: Encodable { - public var emoji: String? - public var text: String +public enum APIResult: Decodable where R: Decodable, R: ChatAPIResult { + case result(R) + case error(ChatError) + case invalid(type: String, json: Data) - public init(emoji: String?, text: String) { - self.emoji = emoji - self.text = text + public var responseType: String { + switch 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)") + } + } + + 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 + } +} + +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 + } + + public func simplexChatUri(short: Bool = true) -> String { + short ? (connShortLink ?? simplexChatLink(connFullLink)) : simplexChatLink(connFullLink) + } +} + +public func simplexChatLink(_ uri: String) -> String { + uri.starts(with: "simplex:/") + ? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") + : uri } public struct ComposedMessage: Encodable { public var fileSource: CryptoFile? var quotedItemId: Int64? public var msgContent: MsgContent + public var mentions: [String: Int64] - public init(fileSource: CryptoFile? = nil, quotedItemId: Int64? = nil, msgContent: MsgContent) { + 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 ArchiveConfig: Encodable { - var archivePath: String - var disableCompression: Bool? - - public init(archivePath: String, disableCompression: Bool? = nil) { - self.archivePath = archivePath - self.disableCompression = disableCompression - } -} - -public struct DBEncryptionConfig: Codable { - public init(currentKey: String, newKey: String) { - self.currentKey = currentKey - self.newKey = newKey - } - - public var currentKey: String - public var newKey: String -} - public enum ServerProtocol: String, Decodable { case smp case xftp } -public enum OperatorTag: String, Codable { - case simplex = "simplex" - case flux = "flux" -} - -public struct ServerOperatorInfo { - public var description: [String] - public var website: URL - public var selfhost: (text: String, link: URL)? = nil - public var logo: String - public var largeLogo: String - public var logoDarkMode: String - public var largeLogoDarkMode: String -} - -public 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" - ), -] - -public struct UsageConditions: Decodable { - public var conditionsId: Int64 - public var conditionsCommit: String - public var notifiedAt: Date? - public var createdAt: Date - - public static var sampleData = UsageConditions( - conditionsId: 1, - conditionsCommit: "11a44dc1fd461a93079f897048b46998db55da5c", - notifiedAt: nil, - createdAt: Date.now - ) -} - -public enum UsageConditionsAction: Decodable { - case review(operators: [ServerOperator], deadline: Date?, showNotice: Bool) - case accepted(operators: [ServerOperator]) - - public var showNotice: Bool { - switch self { - case let .review(_, _, showNotice): showNotice - case .accepted: false - } - } -} - -public struct ServerOperatorConditions: Decodable { - public var serverOperators: [ServerOperator] - public var currentConditions: UsageConditions - public var conditionsAction: UsageConditionsAction? - - public static var empty = ServerOperatorConditions( - serverOperators: [], - currentConditions: UsageConditions(conditionsId: 0, conditionsCommit: "empty", notifiedAt: nil, createdAt: .now), - conditionsAction: nil - ) -} - -public 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?) - - public var conditionsAccepted: Bool { - switch self { - case .accepted: true - case .required: false - } - } - - public var usageAllowed: Bool { - switch self { - case .accepted: true - case let .required(deadline): deadline != nil - } - } -} - -public struct ServerOperator: Identifiable, Equatable, Codable { - public var operatorId: Int64 - public var operatorTag: OperatorTag? - public var tradeName: String - public var legalName: String? - public var serverDomains: [String] - public var conditionsAcceptance: ConditionsAcceptance - public var enabled: Bool - public var smpRoles: ServerRoles - public var xftpRoles: ServerRoles - - public var id: Int64 { operatorId } - - public 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 - } - - public var legalName_: String { - legalName ?? tradeName - } - - public var info: ServerOperatorInfo { - return if let operatorTag = operatorTag { - operatorsInfo[operatorTag] ?? ServerOperator.dummyOperatorInfo - } else { - ServerOperator.dummyOperatorInfo - } - } - - public static let dummyOperatorInfo = ServerOperatorInfo( - description: ["Default"], - website: URL(string: "https://simplex.chat")!, - logo: "decentralized", - largeLogo: "logo", - logoDarkMode: "decentralized-light", - largeLogoDarkMode: "logo-light" - ) - - public func logo(_ colorScheme: ColorScheme) -> String { - colorScheme == .light ? info.logo : info.logoDarkMode - } - - public func largeLogo(_ colorScheme: ColorScheme) -> String { - colorScheme == .light ? info.largeLogo : info.largeLogoDarkMode - } - - public 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) - ) -} - -public struct ServerRoles: Equatable, Codable { - public var storage: Bool - public var proxy: Bool -} - -public struct UserOperatorServers: Identifiable, Equatable, Codable { - public var `operator`: ServerOperator? - public var smpServers: [UserServer] - public var xftpServers: [UserServer] - - public var id: String { - if let op = self.operator { - "\(op.operatorId)" - } else { - "nil operator" - } - } - - public 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 } - } - - public static var sampleData1 = UserOperatorServers( - operator: ServerOperator.sampleData1, - smpServers: [UserServer.sampleData.preset], - xftpServers: [UserServer.sampleData.xftpPreset] - ) - - public static var sampleDataNilOperator = UserOperatorServers( - operator: nil, - smpServers: [UserServer.sampleData.preset], - xftpServers: [UserServer.sampleData.xftpPreset] - ) -} - -public 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) - - public 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 - } - } - - public 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 - } - } - - public 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) - } -} - -public struct UserServer: Identifiable, Equatable, Codable, Hashable { - public var serverId: Int64? - public var server: String - public var preset: Bool - public var tested: Bool? - public var enabled: Bool - public var deleted: Bool - var createdAt = Date() - - public init(serverId: Int64?, server: String, preset: Bool, tested: Bool?, enabled: Bool, deleted: Bool) { - self.serverId = serverId - self.server = server - self.preset = preset - self.tested = tested - self.enabled = enabled - self.deleted = deleted - } - - public 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 - } - - public var id: String { "\(server) \(createdAt)" } - - public static var empty = UserServer(serverId: nil, server: "", preset: false, tested: nil, enabled: false, deleted: false) - - public var isEmpty: Bool { - server.trimmingCharacters(in: .whitespaces) == "" - } - - public struct SampleData { - public var preset: UserServer - public var custom: UserServer - public var untested: UserServer - public var xftpPreset: UserServer - } - - public 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 - } -} - -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 { - public var testStep: ProtocolTestStep - public 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] @@ -1744,9 +240,9 @@ public struct NetCfg: Codable, Equatable { public var sessionMode = TransportSessionMode.user public var smpProxyMode: SMPProxyMode = .always public var smpProxyFallback: SMPProxyFallback = .allowProtected - public var smpWebPort = false - public var tcpConnectTimeout: Int // microseconds - public var tcpTimeout: Int // microseconds + public var smpWebPortServers: SMPWebPortServers = .preset + public var tcpConnectTimeout: NetworkTimeout + public var tcpTimeout: NetworkTimeout public var tcpTimeoutPerKb: Int // microseconds public var rcvConcurrency: Int // pool size public var tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults @@ -1755,21 +251,21 @@ public struct NetCfg: Codable, Equatable { public var logTLSErrors: Bool = false public static let defaults: NetCfg = NetCfg( - tcpConnectTimeout: 25_000_000, - tcpTimeout: 15_000_000, + tcpConnectTimeout: NetworkTimeout(backgroundTimeout: 45_000_000, interactiveTimeout: 15_000_000), + tcpTimeout: NetworkTimeout(backgroundTimeout: 30_000_000, interactiveTimeout: 10_000_000), tcpTimeoutPerKb: 10_000, rcvConcurrency: 12, smpPingInterval: 1200_000_000 ) static let proxyDefaults: NetCfg = NetCfg( - tcpConnectTimeout: 35_000_000, - tcpTimeout: 20_000_000, + tcpConnectTimeout: NetworkTimeout(backgroundTimeout: 60_000_000, interactiveTimeout: 30_000_000), + tcpTimeout: NetworkTimeout(backgroundTimeout: 40_000_000, interactiveTimeout: 20_000_000), tcpTimeoutPerKb: 15_000, rcvConcurrency: 8, smpPingInterval: 1200_000_000 ) - + public var withProxyTimeouts: NetCfg { var cfg = self cfg.tcpConnectTimeout = NetCfg.proxyDefaults.tcpConnectTimeout @@ -1779,7 +275,7 @@ public struct NetCfg: Codable, Equatable { cfg.smpPingInterval = NetCfg.proxyDefaults.smpPingInterval return cfg } - + public var hasProxyTimeouts: Bool { tcpConnectTimeout == NetCfg.proxyDefaults.tcpConnectTimeout && tcpTimeout == NetCfg.proxyDefaults.tcpTimeout && @@ -1791,6 +287,11 @@ public struct NetCfg: Codable, Equatable { public var enableKeepAlive: Bool { tcpKeepAlive != nil } } +public struct NetworkTimeout: Codable, Equatable { + public var backgroundTimeout: Int // microseconds + public var interactiveTimeout: Int // microseconds +} + public enum HostMode: String, Codable { case onionViaSocks case onionHost = "onion" @@ -1840,6 +341,20 @@ public enum SMPProxyFallback: String, Codable, SelectableItem { 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 @@ -1912,7 +427,7 @@ public struct NetworkProxy: Equatable, Codable { public static var def: NetworkProxy { NetworkProxy() } - + public var valid: Bool { let hostOk = switch NWEndpoint.Host(host) { case .ipv4: true @@ -1923,7 +438,7 @@ public struct NetworkProxy: Equatable, Codable { port > 0 && port <= 65535 && NetworkProxy.validCredential(username) && NetworkProxy.validCredential(password) } - + public static func validCredential(_ s: String) -> Bool { !s.contains(":") && !s.contains("@") } @@ -1959,56 +474,6 @@ public enum NetworkProxyAuth: String, Codable { case isolate } -public enum NetworkStatus: Decodable, Equatable { - case unknown - case connected - case disconnected - case error(connectionError: String) - - public var statusString: LocalizedStringKey { - get { - switch self { - case .connected: return "connected" - case .error: return "error" - default: return "connecting" - } - } - } - - 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 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 enum ForwardConfirmation: Decodable, Hashable { - case filesNotAccepted(fileIds: [Int64]) - case filesInProgress(filesCount: Int) - case filesMissing(filesCount: Int) - case filesFailed(filesCount: Int) -} - -public struct ConnNetworkStatus: Decodable { - public var agentConnId: String - public var networkStatus: NetworkStatus -} - public struct ChatSettings: Codable, Hashable { public var enableNtfs: MsgFilter public var sendRcpts: Bool? @@ -2023,19 +488,54 @@ public struct ChatSettings: Codable, Hashable { public static let defaults: ChatSettings = ChatSettings(enableNtfs: .all, sendRcpts: nil, favorite: false) } +public struct NavigationInfo: Decodable { + public var afterUnread: Int = 0 + public var afterTotal: Int = 0 + + public init(afterUnread: Int = 0, afterTotal: Int = 0) { + self.afterUnread = afterUnread + self.afterTotal = afterTotal + } +} + public enum MsgFilter: String, Codable, Hashable { case none case all case mentions -} - -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 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" + } } } @@ -2045,6 +545,7 @@ public struct ConnectionStats: Decodable, Hashable { public var sndQueuesInfo: [SndQueueInfo] public var ratchetSyncState: RatchetSyncState public var ratchetSyncSupported: Bool + public var subStatus: SubscriptionStatus? public var ratchetSyncAllowed: Bool { ratchetSyncSupported && [.allowed, .required].contains(ratchetSyncState) @@ -2059,25 +560,28 @@ public struct ConnectionStats: Decodable, Hashable { } } -public struct RcvQueueInfo: Codable, Hashable { +public struct RcvQueueInfo: Decodable, Hashable { public var rcvServer: String + public var status: QueueStatus public var rcvSwitchStatus: RcvSwitchStatus? public var canAbortSwitch: Bool + public var subStatus: SubscriptionStatus } -public enum RcvSwitchStatus: String, Codable, Hashable { +public enum RcvSwitchStatus: String, Decodable, Hashable { case switchStarted = "switch_started" case sendingQADD = "sending_qadd" case sendingQUSE = "sending_quse" case receivedMessage = "received_message" } -public struct SndQueueInfo: Codable, Hashable { +public struct SndQueueInfo: Decodable, Hashable { public var sndServer: String + public var status: QueueStatus public var sndSwitchStatus: SndSwitchStatus? } -public enum SndSwitchStatus: String, Codable, Hashable { +public enum SndSwitchStatus: String, Decodable, Hashable { case sendingQKEY = "sending_qkey" case sendingQTEST = "sending_qtest" } @@ -2106,41 +610,45 @@ public enum RatchetSyncState: String, Decodable { case agreed } -public struct UserContactLink: Decodable, Hashable { - 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 enum QueueStatus: String, Decodable, Hashable { + case new + case confirmed + case secured + case active + case disabled } -public struct AutoAccept: Codable, Hashable { - public var businessAddress: Bool - public var acceptIncognito: Bool - public var autoReply: MsgContent? +public enum SubscriptionStatus: Decodable, Hashable { + case active + case pending + case removed(subError: String) + case noSub - public init(businessAddress: Bool, acceptIncognito: Bool, autoReply: MsgContent? = nil) { - self.businessAddress = businessAddress - self.acceptIncognito = acceptIncognito - self.autoReply = autoReply + public var statusString: LocalizedStringKey { + switch self { + case .active: "connected" + case .pending: "connecting" + case .removed: "error" + case .noSub: "no subscription" + } } - 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" + public var statusExplanation: String { + switch self { + case .active: NSLocalizedString("You are connected to the server used to receive messages from this connection.", comment: "subscription status explanation") + case .pending: NSLocalizedString("Trying to connect to the server used to receive messages from this connection.", comment: "subscription status explanation") + case let .removed(err): String.localizedStringWithFormat(NSLocalizedString("Error connecting to the server used to receive messages from this connection: %@", comment: "subscription status explanation"), err) + case .noSub: NSLocalizedString("You are not connected to the server used to receive messages from this connection (no subscription).", comment: "subscription status explanation") + } + } + + public var imageName: String { + switch self { + case .active: "circle.fill" + case .pending: "ellipsis.circle.fill" + case .removed: "exclamationmark.circle.fill" + case .noSub: "circle.dotted" } - guard let msg = autoAccept.autoReply else { return s } - return s + " " + msg.cmdString } } @@ -2149,65 +657,6 @@ public protocol SelectableItem: Identifiable, Equatable { 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: "No push server" - case .periodic: "Periodic" - case .instant: "Instant" - } - } - - public var icon: String { - switch self { - case .off: return "arrow.clockwise" - case .periodic: return "timer" - case .instant: return "bolt" - } - } - - public var id: String { self.rawValue } - - public static var values: [NotificationsMode] = [.instant, .periodic, .off] -} - public enum NotificationPreviewMode: String, SelectableItem, Codable { case hidden case contact @@ -2226,63 +675,6 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable { public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden] } -public enum PrivacyChatListOpenLinksMode: String, CaseIterable, Codable, RawRepresentable, Identifiable { - case yes - case no - case ask - - public var id: Self { self } - - public var text: LocalizedStringKey { - switch self { - case .yes: return "Yes" - case .no: return "No" - case .ask: return "Ask" - } - } -} - -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) @@ -2299,13 +691,26 @@ private func encodeCJSON(_ value: T) -> [CChar] { encodeJSON(value).cString(using: .utf8)! } -public enum ChatError: Decodable, Hashable { +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, Hashable { @@ -2328,9 +733,10 @@ public enum ChatErrorType: Decodable, Hashable { case chatNotStarted case chatNotStopped case chatStoreChanged - case connectionPlan(connectionPlan: ConnectionPlan) case invalidConnReq + case unsupportedConnReq case invalidChatMessage(connection: Connection, message: String) + case connReqMessageProhibited case contactNotReady(contact: Contact) case contactNotActive(contact: Contact) case contactDisabled(contact: Contact) @@ -2354,7 +760,6 @@ public enum ChatErrorType: Decodable, Hashable { case fileCancelled(message: String) case fileCancel(fileId: Int64, message: String) case fileAlreadyExists(filePath: String) - case fileRead(filePath: String, message: String) case fileWrite(filePath: String, message: String) case fileSend(fileId: Int64, agentError: String) case fileRcvChunk(message: String) @@ -2403,10 +808,13 @@ public enum StoreError: Decodable, Hashable { case userContactLinkNotFound case contactRequestNotFound(contactRequestId: Int64) case contactRequestNotFoundByName(contactName: ContactName) + case invalidContactRequestEntity(contactRequestId: Int64) + case invalidBusinessChatContactRequest case groupNotFound(groupId: Int64) case groupNotFoundByName(groupName: GroupName) case groupMemberNameNotFound(groupId: Int64, groupMemberName: ContactName) case groupMemberNotFound(groupMemberId: Int64) + case groupHostMemberNotFound(groupId: Int64) case groupMemberNotFoundByMemberId(memberId: String) case memberContactGroupMemberNotFound(contactId: Int64) case groupWithoutUser @@ -2445,6 +853,7 @@ public enum StoreError: Decodable, Hashable { case hostMemberIdNotFound(groupId: Int64) case contactNotFoundByFileId(fileId: Int64) case noGroupSndStatus(itemId: Int64, groupMemberId: Int64) + case dBException(message: String) } public enum DatabaseError: Decodable, Hashable { @@ -2462,7 +871,7 @@ public enum SQLiteError: Decodable, Hashable { public enum AgentErrorType: Decodable, Hashable { case CMD(cmdErr: CommandErrorType, errContext: String) - case CONN(connErr: ConnectionErrorType) + case CONN(connErr: ConnectionErrorType, errContext: String) case SMP(serverAddress: String, smpErr: ProtocolErrorType) case NTF(ntfErr: ProtocolErrorType) case XFTP(xftpErr: XFTPErrorType) @@ -2470,6 +879,7 @@ public enum AgentErrorType: Decodable, Hashable { case RCP(rcpErr: RCErrorType) case BROKER(brokerAddress: String, brokerErr: BrokerErrorType) case AGENT(agentErr: SMPAgentError) + case NOTICE(server: String, preset: Bool, expiresAt: Date?) case INTERNAL(internalErr: String) case CRITICAL(offerRestart: Bool, criticalErr: String) case INACTIVE @@ -2494,7 +904,7 @@ public enum ConnectionErrorType: Decodable, Hashable { public enum BrokerErrorType: Decodable, Hashable { case RESPONSE(smpErr: String) case UNEXPECTED - case NETWORK + case NETWORK(networkError: NetworkError) case HOST case TRANSPORT(transportErr: ProtocolTransportError) case TIMEOUT @@ -2590,6 +1000,15 @@ public enum ProtocolCommandError: Decodable, Hashable { case NO_ENTITY } +public enum NetworkError: Decodable, Hashable { + case connectError(connectError: String) + case tLSError(tlsError: String) + case unknownCAError + case failedError + case timeoutError + case subscribeError(subscribeError: String) +} + public enum ProtocolTransportError: Decodable, Hashable { case badBlock case version @@ -2634,425 +1053,14 @@ public enum RemoteCtrlError: Decodable, Hashable { case protocolError } -public struct MigrationFileLinkData: Codable { - let networkConfig: NetworkConfig? - - public init(networkConfig: NetworkConfig) { - self.networkConfig = networkConfig - } - - public struct NetworkConfig: Codable { - let socksProxy: String? - let networkProxy: NetworkProxy? - let hostMode: HostMode? - let requiredHostMode: Bool? - - public init(socksProxy: String?, networkProxy: NetworkProxy?, hostMode: HostMode?, requiredHostMode: Bool?) { - self.socksProxy = socksProxy - self.networkProxy = networkProxy - self.hostMode = hostMode - self.requiredHostMode = requiredHostMode - } - - public func transformToPlatformSupported() -> NetworkConfig { - return if let hostMode, let requiredHostMode { - NetworkConfig( - socksProxy: nil, - networkProxy: 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 networkProxy: NetworkProxy? = nil - public var privacyEncryptLocalFiles: Bool? = nil - public var privacyAskToApproveRelays: Bool? = nil - public var privacyAcceptImages: Bool? = nil - public var privacyLinkPreviews: Bool? = nil - public var privacyChatListOpenLinks: PrivacyChatListOpenLinksMode? = nil - public var privacyShowChatPreviews: Bool? = nil - public var privacySaveLastDraft: Bool? = nil - public var privacyProtectScreen: Bool? = nil - public var privacyMediaBlurRadius: Int? = 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 var uiProfileImageCornerRadius: Double? = nil - public var uiChatItemRoundness: Double? = nil - public var uiChatItemTail: Bool? = nil - public var uiColorScheme: String? = nil - public var uiDarkColorScheme: String? = nil - public var uiCurrentThemeIds: [String: String]? = nil - public var uiThemes: [ThemeOverrides]? = nil - public var oneHandUI: Bool? = nil - public var chatBottomBar: Bool? = nil - - public 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 privacyChatListOpenLinks != def.privacyChatListOpenLinks { empty.privacyChatListOpenLinks = privacyChatListOpenLinks } - if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } - if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft } - if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen } - if privacyMediaBlurRadius != def.privacyMediaBlurRadius { empty.privacyMediaBlurRadius = privacyMediaBlurRadius } - if notificationMode != def.notificationMode { empty.notificationMode = notificationMode } - if notificationPreviewMode != def.notificationPreviewMode { empty.notificationPreviewMode = notificationPreviewMode } - if webrtcPolicyRelay != def.webrtcPolicyRelay { empty.webrtcPolicyRelay = webrtcPolicyRelay } - if webrtcICEServers != def.webrtcICEServers { empty.webrtcICEServers = webrtcICEServers } - if confirmRemoteSessions != def.confirmRemoteSessions { empty.confirmRemoteSessions = confirmRemoteSessions } - if connectRemoteViaMulticast != def.connectRemoteViaMulticast {empty.connectRemoteViaMulticast = connectRemoteViaMulticast } - if connectRemoteViaMulticastAuto != def.connectRemoteViaMulticastAuto { empty.connectRemoteViaMulticastAuto = connectRemoteViaMulticastAuto } - if developerTools != def.developerTools { empty.developerTools = developerTools } - if confirmDBUpgrades != def.confirmDBUpgrades { empty.confirmDBUpgrades = confirmDBUpgrades } - if androidCallOnLockScreen != def.androidCallOnLockScreen { empty.androidCallOnLockScreen = androidCallOnLockScreen } - if iosCallKitEnabled != def.iosCallKitEnabled { empty.iosCallKitEnabled = iosCallKitEnabled } - if iosCallKitCallsInRecents != def.iosCallKitCallsInRecents { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } - if uiProfileImageCornerRadius != def.uiProfileImageCornerRadius { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius } - if uiChatItemRoundness != def.uiChatItemRoundness { empty.uiChatItemRoundness = uiChatItemRoundness } - if uiChatItemTail != def.uiChatItemTail { empty.uiChatItemTail = uiChatItemTail } - if uiColorScheme != def.uiColorScheme { empty.uiColorScheme = uiColorScheme } - if uiDarkColorScheme != def.uiDarkColorScheme { empty.uiDarkColorScheme = uiDarkColorScheme } - if uiCurrentThemeIds != def.uiCurrentThemeIds { empty.uiCurrentThemeIds = uiCurrentThemeIds } - if uiThemes != def.uiThemes { empty.uiThemes = uiThemes } - if oneHandUI != def.oneHandUI { empty.oneHandUI = oneHandUI } - if chatBottomBar != def.chatBottomBar { empty.chatBottomBar = chatBottomBar } - return empty - } - - public static var defaults: AppSettings { - AppSettings ( - networkConfig: NetCfg.defaults, - networkProxy: NetworkProxy.def, - privacyEncryptLocalFiles: true, - privacyAskToApproveRelays: true, - privacyAcceptImages: true, - privacyLinkPreviews: true, - privacyChatListOpenLinks: .ask, - 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 - ) - } -} - -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 RcvMsgInfo: Codable { - var msgId: Int64 - var msgDeliveryId: Int64 - var msgDeliveryStatus: String - var agentMsgId: Int64 - var agentMsgMeta: String -} - -public struct ServerQueueInfo: Codable { - var server: String - var rcvId: String - var sndId: String - var ntfId: String? - var status: String - var info: QueueInfo -} - -public struct QueueInfo: Codable { - var qiSnd: Bool - var qiNtf: Bool - var qiSub: QSub? - var qiSize: Int - var qiMsg: MsgInfo? -} - -public struct QSub: Codable { - var qSubThread: QSubThread - var qDelivered: String? -} - -public enum QSubThread: String, Codable { - case noSub - case subPending - case subThread - case prohibitSub -} - -public struct MsgInfo: Codable { - var msgId: String - var msgTs: Date - var msgType: MsgType -} - -public enum MsgType: String, Codable { - case message - case quota -} - public struct AppFilePaths: Encodable { public let appFilesFolder: String public let appTempFolder: String public let appAssetsFolder: String -} - -public struct PresentedServersSummary: Codable { - public var statsStartedAt: Date - public var allUsersSMP: SMPServersSummary - public var allUsersXFTP: XFTPServersSummary - public var currentUserSMP: SMPServersSummary - public var currentUserXFTP: XFTPServersSummary -} - -public struct SMPServersSummary: Codable { - public var smpTotals: SMPTotals - public var currentlyUsedSMPServers: [SMPServerSummary] - public var previouslyUsedSMPServers: [SMPServerSummary] - public var onlyProxiedSMPServers: [SMPServerSummary] -} - -public struct SMPTotals: Codable { - public var sessions: ServerSessions - public var subs: SMPServerSubs - public var stats: AgentSMPServerStatsData -} - -public struct SMPServerSummary: Codable, Identifiable { - public var smpServer: String - public var known: Bool? - public var sessions: ServerSessions? - public var subs: SMPServerSubs? - public var stats: AgentSMPServerStatsData? - - public var id: String { smpServer } - - public var hasSubs: Bool { subs != nil } - - public var sessionsOrNew: ServerSessions { sessions ?? ServerSessions.newServerSessions } - - public var subsOrNew: SMPServerSubs { subs ?? SMPServerSubs.newSMPServerSubs } -} - -public struct ServerSessions: Codable { - public var ssConnected: Int - public var ssErrors: Int - public var ssConnecting: Int - - static public var newServerSessions = ServerSessions( - ssConnected: 0, - ssErrors: 0, - ssConnecting: 0 - ) - - public var hasSess: Bool { ssConnected > 0 } -} - -public struct SMPServerSubs: Codable { - public var ssActive: Int - public var ssPending: Int - - public init(ssActive: Int, ssPending: Int) { - self.ssActive = ssActive - self.ssPending = ssPending - } - - static public var newSMPServerSubs = SMPServerSubs( - ssActive: 0, - ssPending: 0 - ) - - public var total: Int { ssActive + ssPending } - - public var shareOfActive: Double { - guard total != 0 else { return 0.0 } - return Double(ssActive) / Double(total) + + public init(appFilesFolder: String, appTempFolder: String, appAssetsFolder: String) { + self.appFilesFolder = appFilesFolder + self.appTempFolder = appTempFolder + self.appAssetsFolder = appAssetsFolder } } - -public struct AgentSMPServerStatsData: Codable { - public var _sentDirect: Int - public var _sentViaProxy: Int - public var _sentProxied: Int - public var _sentDirectAttempts: Int - public var _sentViaProxyAttempts: Int - public var _sentProxiedAttempts: Int - public var _sentAuthErrs: Int - public var _sentQuotaErrs: Int - public var _sentExpiredErrs: Int - public var _sentOtherErrs: Int - public var _recvMsgs: Int - public var _recvDuplicates: Int - public var _recvCryptoErrs: Int - public var _recvErrs: Int - public var _ackMsgs: Int - public var _ackAttempts: Int - public var _ackNoMsgErrs: Int - public var _ackOtherErrs: Int - public var _connCreated: Int - public var _connSecured: Int - public var _connCompleted: Int - public var _connDeleted: Int - public var _connDelAttempts: Int - public var _connDelErrs: Int - public var _connSubscribed: Int - public var _connSubAttempts: Int - public var _connSubIgnored: Int - public var _connSubErrs: Int - public var _ntfKey: Int - public var _ntfKeyAttempts: Int - public var _ntfKeyDeleted: Int - public var _ntfKeyDeleteAttempts: Int -} - -public struct XFTPServersSummary: Codable { - public var xftpTotals: XFTPTotals - public var currentlyUsedXFTPServers: [XFTPServerSummary] - public var previouslyUsedXFTPServers: [XFTPServerSummary] -} - -public struct XFTPTotals: Codable { - public var sessions: ServerSessions - public var stats: AgentXFTPServerStatsData -} - -public struct XFTPServerSummary: Codable, Identifiable { - public var xftpServer: String - public var known: Bool? - public var sessions: ServerSessions? - public var stats: AgentXFTPServerStatsData? - public var rcvInProgress: Bool - public var sndInProgress: Bool - public var delInProgress: Bool - - public var id: String { xftpServer } -} - -public struct AgentXFTPServerStatsData: Codable { - public var _uploads: Int - public var _uploadsSize: Int64 - public var _uploadAttempts: Int - public var _uploadErrs: Int - public var _downloads: Int - public var _downloadsSize: Int64 - public var _downloadAttempts: Int - public var _downloadAuthErrs: Int - public var _downloadErrs: Int - public var _deletions: Int - public var _deleteAttempts: Int - public var _deleteErrs: Int -} - -public struct AgentNtfServerStatsData: Codable { - public var _ntfCreated: Int - public var _ntfCreateAttempts: Int - public var _ntfChecked: Int - public var _ntfCheckAttempts: Int - public var _ntfDeleted: Int - public var _ntfDelAttempts: Int -} diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index c754f0740d..77fff873ea 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -27,6 +27,8 @@ let GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED = "appLocalAuthEnabled" public let GROUP_DEFAULT_ALLOW_SHARE_EXTENSION = "allowShareExtension" // replaces DEFAULT_PRIVACY_LINK_PREVIEWS let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" +public let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "privacyLinkPreviewsShowAlert" +public let GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS = "privacySanitizeLinks" // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used @@ -40,8 +42,13 @@ 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_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" -let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" +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_CONNECT_TIMEOUT_BACKGROUND = "networkTCPConnectTimeoutBackground" +let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE = "networkTCPConnectTimeoutInteractive" +let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND = "networkTCPTimeoutInteractive" +let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE = "networkTCPTimeoutBackground" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" let GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval" @@ -71,8 +78,11 @@ public func registerGroupDefaults() { 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_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, - GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, + GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS: SMPWebPortServers.preset.rawValue, + GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND: NetCfg.defaults.tcpConnectTimeout.backgroundTimeout, + GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE: NetCfg.defaults.tcpConnectTimeout.interactiveTimeout, + GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND: NetCfg.defaults.tcpTimeout.backgroundTimeout, + GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE: NetCfg.defaults.tcpTimeout.interactiveTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY: NetCfg.defaults.rcvConcurrency, GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval, @@ -87,6 +97,8 @@ public func registerGroupDefaults() { GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED: true, GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false, GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: true, + GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT: true, + GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS: false, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, @@ -214,6 +226,8 @@ public let allowShareExtensionGroupDefault = BoolDefault(defaults: groupDefaults public let privacyLinkPreviewsGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS) +public let privacyLinkPreviewsShowAlertGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT) + // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES) @@ -249,6 +263,12 @@ public let networkSMPProxyFallbackGroupDefault = EnumDefault( 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) public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE) @@ -303,12 +323,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) } @@ -318,11 +340,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() @@ -336,8 +360,15 @@ public func getNetCfg() -> NetCfg { let sessionMode = networkSessionModeGroupDefault.get() let smpProxyMode = networkSMPProxyModeGroupDefault.get() let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get() - let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) - let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) + let smpWebPortServers = networkSMPWebPortServersDefault.get() + let tcpConnectTimeout = NetworkTimeout( + backgroundTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND), + interactiveTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE) + ) + let tcpTimeout = NetworkTimeout( + backgroundTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND), + interactiveTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE) + ) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) let rcvConcurrency = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) @@ -359,7 +390,7 @@ public func getNetCfg() -> NetCfg { sessionMode: sessionMode, smpProxyMode: smpProxyMode, smpProxyFallback: smpProxyFallback, - smpWebPort: false, + smpWebPortServers: smpWebPortServers, tcpConnectTimeout: tcpConnectTimeout, tcpTimeout: tcpTimeout, tcpTimeoutPerKb: tcpTimeoutPerKb, @@ -378,8 +409,11 @@ public func setNetCfg(_ cfg: NetCfg, networkProxy: NetworkProxy?) { networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback) let socksProxy = networkProxy?.toProxyString() groupDefaults.set(socksProxy, forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) - groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) - groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) + networkSMPWebPortServersDefault.set(cfg.smpWebPortServers) + groupDefaults.set(cfg.tcpConnectTimeout.backgroundTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND) + groupDefaults.set(cfg.tcpConnectTimeout.interactiveTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE) + groupDefaults.set(cfg.tcpTimeout.backgroundTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND) + groupDefaults.set(cfg.tcpTimeout.interactiveTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) groupDefaults.set(cfg.rcvConcurrency, forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index ae49ee3f3f..5d1d5b4302 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -15,6 +15,9 @@ public let CREATE_MEMBER_CONTACT_VERSION = 2 // version to receive reports (MCReport) public let REPORTS_VERSION = 12 +// support group knocking (MsgScope) +public let GROUP_KNOCKING_VERSION = 15 + 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 { @@ -29,12 +32,14 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } + public var shortDescr: String? { profile.shortDescr } public var image: String? { get { profile.image } } public var localAlias: String { get { "" } } public var showNtfs: Bool public var sendRcptsContacts: Bool public var sendRcptsSmallGroups: Bool + public var autoAcceptMemberContacts: Bool public var viewPwdHash: UserPwdHash? public var uiThemes: ThemeModeOverrides? @@ -61,7 +66,8 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { activeOrder: 0, showNtfs: true, sendRcptsContacts: true, - sendRcptsSmallGroups: false + sendRcptsSmallGroups: false, + autoAcceptMemberContacts: false ) } @@ -106,12 +112,15 @@ public struct Profile: Codable, NamedChat, Hashable { public init( displayName: String, fullName: String, + shortDescr: String? = nil, image: String? = nil, contactLink: String? = nil, - preferences: Preferences? = nil + preferences: Preferences? = nil, + peerType: ChatPeerType? = nil ) { self.displayName = displayName self.fullName = fullName + self.shortDescr = shortDescr self.image = image self.contactLink = contactLink self.preferences = preferences @@ -119,9 +128,11 @@ public struct Profile: Codable, NamedChat, Hashable { public var displayName: String public var fullName: String + public var shortDescr: String? public var image: String? public var contactLink: String? public var preferences: Preferences? + public var peerType: ChatPeerType? public var localAlias: String { get { "" } } var profileViewName: String { @@ -139,26 +150,32 @@ public struct LocalProfile: Codable, NamedChat, Hashable { profileId: Int64, displayName: String, fullName: String, + shortDescr: String? = nil, image: String? = nil, contactLink: String? = nil, preferences: Preferences? = nil, + peerType: ChatPeerType? = nil, localAlias: String ) { self.profileId = profileId self.displayName = displayName self.fullName = fullName + self.shortDescr = shortDescr self.image = image self.contactLink = contactLink self.preferences = preferences + self.peerType = peerType self.localAlias = localAlias } public var profileId: Int64 public var displayName: String public var fullName: String + public var shortDescr: String? public var image: String? public var contactLink: String? public var preferences: Preferences? + public var peerType: ChatPeerType? public var localAlias: String var profileViewName: String { @@ -176,12 +193,35 @@ public struct LocalProfile: Codable, NamedChat, Hashable { ) } +public enum ChatPeerType: String, Codable { + case human + case bot +} + public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias: String) -> LocalProfile { - LocalProfile(profileId: profileId, displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences, localAlias: localAlias) + LocalProfile( + profileId: profileId, + displayName: profile.displayName, + fullName: profile.fullName, + shortDescr: profile.shortDescr, + image: profile.image, + contactLink: profile.contactLink, + preferences: profile.preferences, + peerType: profile.peerType, + localAlias: localAlias + ) } public func fromLocalProfile (_ profile: LocalProfile) -> Profile { - Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences) + Profile( + displayName: profile.displayName, + fullName: profile.fullName, + shortDescr: profile.shortDescr, + image: profile.image, + contactLink: profile.contactLink, + preferences: profile.preferences, + peerType: profile.peerType + ) } public struct UserProfileUpdateSummary: Decodable, Hashable { @@ -201,6 +241,7 @@ public enum ChatType: String, Hashable { public protocol NamedChat { var displayName: String { get } var fullName: String { get } + var shortDescr: String? { get } var image: String? { get } var localAlias: String { get } } @@ -220,20 +261,26 @@ public struct FullPreferences: Decodable, Equatable, Hashable { public var fullDelete: SimplePreference public var reactions: SimplePreference public var voice: SimplePreference + public var files: SimplePreference public var calls: SimplePreference + public var commands: [ChatBotCommand] public init( timedMessages: TimedMessagesPreference, fullDelete: SimplePreference, reactions: SimplePreference, voice: SimplePreference, - calls: SimplePreference + files: SimplePreference, + calls: SimplePreference, + commands: [ChatBotCommand] ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = FullPreferences( @@ -241,7 +288,9 @@ public struct FullPreferences: Decodable, Equatable, Hashable { fullDelete: SimplePreference(allow: .no), reactions: SimplePreference(allow: .yes), voice: SimplePreference(allow: .yes), - calls: SimplePreference(allow: .yes) + files: SimplePreference(allow: .always), + calls: SimplePreference(allow: .yes), + commands: [] ) } @@ -250,20 +299,26 @@ public struct Preferences: Codable, Hashable { public var fullDelete: SimplePreference? public var reactions: SimplePreference? public var voice: SimplePreference? + public var files: SimplePreference? public var calls: SimplePreference? + public var commands: [ChatBotCommand]? public init( timedMessages: TimedMessagesPreference?, fullDelete: SimplePreference?, reactions: SimplePreference?, voice: SimplePreference?, - calls: SimplePreference? + files: SimplePreference?, + calls: SimplePreference?, + commands: [ChatBotCommand]? ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } func copy( @@ -271,14 +326,18 @@ public struct Preferences: Codable, Hashable { fullDelete: SimplePreference? = nil, reactions: SimplePreference? = nil, voice: SimplePreference? = nil, - calls: SimplePreference? = nil + files: SimplePreference? = nil, + calls: SimplePreference? = nil, + commands: [ChatBotCommand]? = nil ) -> Preferences { Preferences( timedMessages: timedMessages ?? self.timedMessages, fullDelete: fullDelete ?? self.fullDelete, reactions: reactions ?? self.reactions, voice: voice ?? self.voice, - calls: calls ?? self.calls + files: files ?? self.files, + calls: calls ?? self.calls, + commands: commands ?? self.commands ) } @@ -288,6 +347,7 @@ public struct Preferences: Codable, Hashable { case .fullDelete: return copy(fullDelete: SimplePreference(allow: allowed)) case .reactions: return copy(reactions: SimplePreference(allow: allowed)) case .voice: return copy(voice: SimplePreference(allow: allowed)) + case .files: return copy(voice: SimplePreference(allow: allowed)) case .calls: return copy(calls: SimplePreference(allow: allowed)) } } @@ -297,17 +357,72 @@ public struct Preferences: Codable, Hashable { fullDelete: SimplePreference(allow: .no), reactions: SimplePreference(allow: .yes), voice: SimplePreference(allow: .yes), - calls: SimplePreference(allow: .yes) + files: SimplePreference(allow: .always), + calls: SimplePreference(allow: .yes), + commands: nil ) } +public indirect enum ChatBotCommand: Hashable { + case command(keyword: String, label: String, params: String?) + case menu(label: String, commands: [ChatBotCommand]) + + enum CodingKeys: String, CodingKey { + case type + case keyword + case label + case params + case hidden + case commands + } +} + +extension ChatBotCommand: Decodable { + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + let type = try c.decode(String.self, forKey: CodingKeys.type) + switch type { + case "command": + let keyword = try c.decode(String.self, forKey: CodingKeys.keyword) + let label = try c.decode(String.self, forKey: CodingKeys.label) + let params = c.contains(CodingKeys.params) ? try c.decode((String?).self, forKey: CodingKeys.params) : nil + self = .command(keyword: keyword, label: label, params: params) + case "menu": + let label = try c.decode(String.self, forKey: CodingKeys.label) + let commands = try c.decode(([ChatBotCommand]).self, forKey: CodingKeys.commands) + self = .menu(label: label, commands: commands) + default: + throw DecodingError.dataCorruptedError(forKey: CodingKeys.type, in: c, debugDescription: "Unsupported command type: \(type)") + } + } +} + +extension ChatBotCommand: Encodable { + public func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .command(keyword, label, params): + try c.encode("command", forKey: .type) + try c.encode(keyword, forKey: .keyword) + try c.encode(label, forKey: .label) + if let params { try c.encode(params, forKey: .params) } + case let .menu(label, commands): + try c.encode("menu", forKey: .type) + try c.encode(label, forKey: .label) + try c.encode(commands, forKey: .commands) + } + } +} + public func fullPreferencesToPreferences(_ fullPreferences: FullPreferences) -> Preferences { Preferences( timedMessages: fullPreferences.timedMessages, fullDelete: fullPreferences.fullDelete, reactions: fullPreferences.reactions, voice: fullPreferences.voice, - calls: fullPreferences.calls + files: fullPreferences.files, + calls: fullPreferences.calls, + commands: fullPreferences.commands ) } @@ -317,7 +432,9 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac fullDelete: contactUserPreferences.fullDelete.userPreference.preference, reactions: contactUserPreferences.reactions.userPreference.preference, voice: contactUserPreferences.voice.userPreference.preference, - calls: contactUserPreferences.calls.userPreference.preference + files: contactUserPreferences.files.userPreference.preference, + calls: contactUserPreferences.calls.userPreference.preference, + commands: contactUserPreferences.commands ) } @@ -345,6 +462,10 @@ public struct TimedMessagesPreference: Preference, Hashable { public static var ttlValues: [Int?] { [600, 3600, 86400, 7 * 86400, 30 * 86400, 3 * 30 * 86400, nil] } + + public static var profileLevelTTLValues: [Int?] { + [7 * 86400, 30 * 86400, 3 * 30 * 86400, nil] + } } public enum CustomTimeUnit: Hashable { @@ -451,20 +572,26 @@ public struct ContactUserPreferences: Decodable, Hashable { public var fullDelete: ContactUserPreference public var reactions: ContactUserPreference public var voice: ContactUserPreference + public var files: ContactUserPreference public var calls: ContactUserPreference + public var commands: [ChatBotCommand]? public init( timedMessages: ContactUserPreference, fullDelete: ContactUserPreference, reactions: ContactUserPreference, voice: ContactUserPreference, - calls: ContactUserPreference + files: ContactUserPreference, + calls: ContactUserPreference, + commands: [ChatBotCommand]? ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = ContactUserPreferences( @@ -488,11 +615,17 @@ public struct ContactUserPreferences: Decodable, Hashable { userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), contactPreference: SimplePreference(allow: .yes) ), + files: ContactUserPreference( + enabled: FeatureEnabled(forUser: true, forContact: true), + userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), + contactPreference: SimplePreference(allow: .yes) + ), calls: ContactUserPreference( enabled: FeatureEnabled(forUser: true, forContact: true), userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), contactPreference: SimplePreference(allow: .yes) - ) + ), + commands: nil ) } @@ -565,6 +698,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case fullDelete case reactions case voice + case files case calls public var id: Self { self } @@ -591,6 +725,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return NSLocalizedString("Delete for everyone", comment: "chat feature") case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature") case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") + case .files: return NSLocalizedString("Files and media", comment: "chat feature") case .calls: return NSLocalizedString("Audio/video calls", comment: "chat feature") } } @@ -601,6 +736,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return "trash.slash" case .reactions: return "face.smiling" case .voice: return "mic" + case .files: return "doc" case .calls: return "phone" } } @@ -611,6 +747,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return "trash.slash.fill" case .reactions: return "face.smiling.fill" case .voice: return "mic.fill" + case .files: return "doc.fill" case .calls: return "phone.fill" } } @@ -648,6 +785,12 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .yes: return "Allow voice messages only if your contact allows them." case .no: return "Prohibit sending voice messages." } + case .files: + switch allowed { + case .always: return "Allow your contacts to send files and media." + case .yes: return "Allow files and media only if your contact allows them." + case .no: return "Prohibit sending files and media." + } case .calls: switch allowed { case .always: return "Allow your contacts to call you." @@ -691,6 +834,14 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { : enabled.forContact ? "Only your contact can send voice messages." : "Voice messages are prohibited in this chat." + case .files: + return enabled.forUser && enabled.forContact + ? "Both you and your contact can send files and media." + : enabled.forUser + ? "Only you can send files and media." + : enabled.forContact + ? "Only your contact can send files and media." + : "Files and media are prohibited in this chat." case .calls: return enabled.forUser && enabled.forContact ? "Both you and your contact can make calls." @@ -711,6 +862,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case voice case files case simplexLinks + case reports case history public var id: Self { self } @@ -731,6 +883,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .voice: true case .files: true case .simplexLinks: true + case .reports: false case .history: false } } @@ -744,6 +897,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") case .files: return NSLocalizedString("Files and media", comment: "chat feature") case .simplexLinks: return NSLocalizedString("SimpleX links", comment: "chat feature") + case .reports: return NSLocalizedString("Member reports", comment: "chat feature") case .history: return NSLocalizedString("Visible history", comment: "chat feature") } } @@ -757,6 +911,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .voice: return "mic" case .files: return "doc" case .simplexLinks: return "link.circle" + case .reports: return "flag" case .history: return "clock" } } @@ -770,6 +925,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { 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" } } @@ -819,6 +975,11 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { 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." @@ -862,6 +1023,11 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .on: return "Members can send SimpleX links." case .off: return "SimpleX links are prohibited." } + case .reports: + switch enabled { + case .on: return "Members can report messsages to moderators." + case .off: return "Reporting messages to moderators is prohibited." + } case .history: switch enabled { case .on: return "Up to 100 last messages are sent to new members." @@ -909,7 +1075,9 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { public var fullDelete: ContactFeatureAllowed public var reactions: ContactFeatureAllowed public var voice: ContactFeatureAllowed + public var files: ContactFeatureAllowed public var calls: ContactFeatureAllowed + public var commands: [ChatBotCommand]? public init( timedMessagesAllowed: Bool, @@ -917,14 +1085,18 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { fullDelete: ContactFeatureAllowed, reactions: ContactFeatureAllowed, voice: ContactFeatureAllowed, - calls: ContactFeatureAllowed + files: ContactFeatureAllowed, + calls: ContactFeatureAllowed, + commands: [ChatBotCommand]? ) { self.timedMessagesAllowed = timedMessagesAllowed self.timedMessagesTTL = timedMessagesTTL self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = ContactFeaturesAllowed( @@ -933,7 +1105,9 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { fullDelete: ContactFeatureAllowed.userDefault(.no), reactions: ContactFeatureAllowed.userDefault(.yes), voice: ContactFeatureAllowed.userDefault(.yes), - calls: ContactFeatureAllowed.userDefault(.yes) + files: ContactFeatureAllowed.userDefault(.always), + calls: ContactFeatureAllowed.userDefault(.yes), + commands: nil ) } @@ -946,7 +1120,9 @@ public func contactUserPrefsToFeaturesAllowed(_ contactUserPreferences: ContactU fullDelete: contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete), reactions: contactUserPrefToFeatureAllowed(contactUserPreferences.reactions), voice: contactUserPrefToFeatureAllowed(contactUserPreferences.voice), - calls: contactUserPrefToFeatureAllowed(contactUserPreferences.calls) + files: contactUserPrefToFeatureAllowed(contactUserPreferences.files), + calls: contactUserPrefToFeatureAllowed(contactUserPreferences.calls), + commands: contactUserPreferences.commands ) } @@ -968,7 +1144,9 @@ public func contactFeaturesAllowedToPrefs(_ contactFeaturesAllowed: ContactFeatu fullDelete: contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete), reactions: contactFeatureAllowedToPref(contactFeaturesAllowed.reactions), voice: contactFeatureAllowedToPref(contactFeaturesAllowed.voice), - calls: contactFeatureAllowedToPref(contactFeaturesAllowed.calls) + files: contactFeatureAllowedToPref(contactFeaturesAllowed.files), + calls: contactFeatureAllowedToPref(contactFeaturesAllowed.calls), + commands: contactFeaturesAllowed.commands ) } @@ -1007,7 +1185,9 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { public var voice: RoleGroupPreference public var files: RoleGroupPreference public var simplexLinks: RoleGroupPreference + public var reports: GroupPreference public var history: GroupPreference + public var commands: [ChatBotCommand] public init( timedMessages: TimedMessagesGroupPreference, @@ -1017,7 +1197,9 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { voice: RoleGroupPreference, files: RoleGroupPreference, simplexLinks: RoleGroupPreference, - history: GroupPreference + reports: GroupPreference, + history: GroupPreference, + commands: [ChatBotCommand] ) { self.timedMessages = timedMessages self.directMessages = directMessages @@ -1026,7 +1208,9 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { self.voice = voice self.files = files self.simplexLinks = simplexLinks + self.reports = reports self.history = history + self.commands = commands } public static let sampleData = FullGroupPreferences( @@ -1037,7 +1221,9 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { voice: RoleGroupPreference(enable: .on, role: nil), files: RoleGroupPreference(enable: .on, role: nil), simplexLinks: RoleGroupPreference(enable: .on, role: nil), - history: GroupPreference(enable: .on) + reports: GroupPreference(enable: .on), + history: GroupPreference(enable: .on), + commands: [] ) } @@ -1049,7 +1235,9 @@ public struct GroupPreferences: Codable, Hashable { public var voice: RoleGroupPreference? public var files: RoleGroupPreference? public var simplexLinks: RoleGroupPreference? + public var reports: GroupPreference? public var history: GroupPreference? + public var commands: [ChatBotCommand]? public init( timedMessages: TimedMessagesGroupPreference? = nil, @@ -1059,7 +1247,9 @@ public struct GroupPreferences: Codable, Hashable { voice: RoleGroupPreference? = nil, files: RoleGroupPreference? = nil, simplexLinks: RoleGroupPreference? = nil, - history: GroupPreference? = nil + reports: GroupPreference? = nil, + history: GroupPreference? = nil, + commands: [ChatBotCommand]? = nil ) { self.timedMessages = timedMessages self.directMessages = directMessages @@ -1068,7 +1258,9 @@ public struct GroupPreferences: Codable, Hashable { self.voice = voice self.files = files self.simplexLinks = simplexLinks + self.reports = reports self.history = history + self.commands = commands } public static let sampleData = GroupPreferences( @@ -1079,7 +1271,9 @@ public struct GroupPreferences: Codable, Hashable { voice: RoleGroupPreference(enable: .on, role: nil), files: RoleGroupPreference(enable: .on, role: nil), simplexLinks: RoleGroupPreference(enable: .on, role: nil), - history: GroupPreference(enable: .on) + reports: GroupPreference(enable: .on), + history: GroupPreference(enable: .on), + commands: nil ) } @@ -1092,7 +1286,9 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group voice: fullPreferences.voice, files: fullPreferences.files, simplexLinks: fullPreferences.simplexLinks, - history: fullPreferences.history + reports: fullPreferences.reports, + history: fullPreferences.history, + commands: fullPreferences.commands ) } @@ -1173,11 +1369,11 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable { public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case direct(contact: Contact) - case group(groupInfo: GroupInfo) + case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?) 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") @@ -1187,7 +1383,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.localDisplayName - case let .group(groupInfo): return groupInfo.localDisplayName + case let .group(groupInfo, _): return groupInfo.localDisplayName case .local: return "" case let .contactRequest(contactRequest): return contactRequest.localDisplayName case let .contactConnection(contactConnection): return contactConnection.localDisplayName @@ -1200,7 +1396,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.displayName - case let .group(groupInfo): return groupInfo.displayName + case let .group(groupInfo, _): return groupInfo.displayName case .local: return ChatInfo.privateNotesChatName case let .contactRequest(contactRequest): return contactRequest.displayName case let .contactConnection(contactConnection): return contactConnection.displayName @@ -1213,7 +1409,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.fullName - case let .group(groupInfo): return groupInfo.fullName + case let .group(groupInfo, _): return groupInfo.fullName case .local: return "" case let .contactRequest(contactRequest): return contactRequest.fullName case let .contactConnection(contactConnection): return contactConnection.fullName @@ -1222,11 +1418,22 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var shortDescr: String? { + switch self { + case let .direct(contact): contact.profile.shortDescr + case let .group(groupInfo, _): groupInfo.groupProfile.shortDescr + case .local: nil + case let .contactRequest(contactRequest): contactRequest.profile.shortDescr + case let .contactConnection(contactConnection): nil + case .invalidJSON: nil + } + } + public var image: String? { get { switch self { case let .direct(contact): return contact.image - case let .group(groupInfo): return groupInfo.image + case let .group(groupInfo, _): return groupInfo.image case .local: return nil case let .contactRequest(contactRequest): return contactRequest.image case let .contactConnection(contactConnection): return contactConnection.image @@ -1239,7 +1446,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.localAlias - case let .group(groupInfo): return groupInfo.localAlias + case let .group(groupInfo, _): return groupInfo.localAlias case .local: return "" case let .contactRequest(contactRequest): return contactRequest.localAlias case let .contactConnection(contactConnection): return contactConnection.localAlias @@ -1252,7 +1459,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.id - case let .group(groupInfo): return groupInfo.id + case let .group(groupInfo, _): return groupInfo.id case let .local(noteFolder): return noteFolder.id case let .contactRequest(contactRequest): return contactRequest.id case let .contactConnection(contactConnection): return contactConnection.id @@ -1278,7 +1485,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.apiId - case let .group(groupInfo): return groupInfo.apiId + case let .group(groupInfo, _): return groupInfo.apiId case let .local(noteFolder): return noteFolder.apiId case let .contactRequest(contactRequest): return contactRequest.apiId case let .contactConnection(contactConnection): return contactConnection.apiId @@ -1291,7 +1498,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.ready - case let .group(groupInfo): return groupInfo.ready + case let .group(groupInfo, _): return groupInfo.ready case let .local(noteFolder): return noteFolder.ready case let .contactRequest(contactRequest): return contactRequest.ready case let .contactConnection(contactConnection): return contactConnection.ready @@ -1299,7 +1506,20 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } } - + + public var sndReady: Bool { + switch self { + case let .direct(contact): contact.sndReady + case let .group(groupInfo, groupScope): + groupInfo.membership.memberActive + && (groupScope != nil || (!groupInfo.membership.memberPending && groupInfo.membership.memberRole != .observer)) + case .local: true + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } + } + public var chatDeleted: Bool { get { switch self { @@ -1309,24 +1529,94 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } - public var sendMsgEnabled: Bool { + public var nextConnect: Bool { get { switch self { - case let .direct(contact): return contact.sendMsgEnabled - case let .group(groupInfo): return groupInfo.sendMsgEnabled - case let .local(noteFolder): return noteFolder.sendMsgEnabled - case let .contactRequest(contactRequest): return contactRequest.sendMsgEnabled - case let .contactConnection(contactConnection): return contactConnection.sendMsgEnabled - case .invalidJSON: return false + case let .direct(contact): return contact.sendMsgToConnect + case let .group(groupInfo, _): return groupInfo.nextConnectPrepared + default: return false } } } + public var nextConnectPrepared: Bool { + get { + switch self { + case let .direct(contact): return contact.nextConnectPrepared + case let .group(groupInfo, _): return groupInfo.nextConnectPrepared + default: return false + } + } + } + + public var profileChangeProhibited: Bool { + get { + switch self { + case let .direct(contact): return contact.profileChangeProhibited + case let .group(groupInfo, _): return groupInfo.profileChangeProhibited + default: return false + } + } + } + + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { + get { + switch self { + case let .direct(contact): + if contact.sendMsgToConnect { return nil } + if contact.nextAcceptContactRequest { return ("can't send messages", nil) } + if !contact.active { return ("contact deleted", nil) } + if !contact.sndReady { return (contact.preparedContact?.uiConnLinkType == .con ? "request is sent" : "contact not ready", nil) } + if contact.activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) } + if contact.activeConn?.connDisabled ?? true { return ("contact disabled", nil) } + return nil + case let .group(groupInfo, groupChatScope): + if groupInfo.membership.memberActive { + switch(groupChatScope) { + case .none: + if groupInfo.membership.memberPending { return ("reviewed by admins", "Please contact group admin.") } + if groupInfo.membership.memberRole == .observer { return ("you are observer", "Please contact group admin.") } + return nil + case let .some(.memberSupport(groupMember_: .some(supportMember))): + if supportMember.versionRange.maxVersion < GROUP_KNOCKING_VERSION && !supportMember.memberPending { + return ("member has old version", nil) + } + return nil + case .some(.memberSupport(groupMember_: .none)): + return nil + case .some(.reports): + return ("can't send messages", nil) + } + } else if groupInfo.nextConnectPrepared { + return nil + } else { + switch groupInfo.membership.memberStatus { + case .memRejected: return ("request to join rejected", nil) + case .memGroupDeleted: return ("group is deleted", nil) + case .memRemoved: return ("removed from group", nil) + case .memLeft: return ("you left", nil) + default: return ("can't send messages", nil) + } + } + case .local: + return nil + case .contactRequest: + return ("can't send messages", nil) + case .contactConnection: + return ("can't send messages", nil) + case .invalidJSON: + return ("can't send messages", nil) + } + } + } + + public var sendMsgEnabled: Bool { userCantSendReason == nil } + public var incognito: Bool { get { switch self { case let .direct(contact): return contact.contactConnIncognito - case let .group(groupInfo): return groupInfo.membership.memberIncognito + case let .group(groupInfo, _): return groupInfo.membership.memberIncognito case .local: return false case .contactRequest: return false case let .contactConnection(contactConnection): return contactConnection.incognito @@ -1344,14 +1634,14 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public var contactCard: Bool { switch self { - case let .direct(contact): contact.activeConn == nil && contact.profile.contactLink != nil && contact.active + case let .direct(contact): contact.isContactCard default: false } } - + public var groupInfo: GroupInfo? { switch self { - case let .group(groupInfo): return groupInfo + case let .group(groupInfo, _): return groupInfo default: return nil } } @@ -1366,21 +1656,27 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case .fullDelete: return cups.fullDelete.enabled.forUser case .reactions: return cups.reactions.enabled.forUser case .voice: return cups.voice.enabled.forUser + case .files: return cups.files.enabled.forUser case .calls: return cups.calls.enabled.forUser } - case let .group(groupInfo): + case let .group(groupInfo, _): let prefs = groupInfo.fullGroupPreferences switch feature { case .timedMessages: return prefs.timedMessages.on case .fullDelete: return prefs.fullDelete.on case .reactions: return prefs.reactions.on case .voice: return prefs.voice.on(for: groupInfo.membership) + case .files: return prefs.files.on(for: groupInfo.membership) case .calls: return false } case .local: switch feature { + case .timedMessages: return false + case .fullDelete: return false + case .reactions: return false case .voice: return true - default: return false + case .files: return true + case .calls: return false } default: return false } @@ -1391,7 +1687,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case let .direct(contact): let pref = contact.mergedPreferences.timedMessages return pref.enabled.forUser ? pref.userPreference.preference.ttl : nil - case let .group(groupInfo): + case let .group(groupInfo, _): let pref = groupInfo.fullGroupPreferences.timedMessages return pref.on ? pref.ttl : nil default: @@ -1416,7 +1712,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } else { return .other } - case let .group(groupInfo): + case let .group(groupInfo, _): if !groupInfo.fullGroupPreferences.voice.on(for: groupInfo.membership) { return .groupOwnerCan } else { @@ -1448,59 +1744,76 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } - public var ntfsEnabled: Bool { - self.chatSettings?.enableNtfs == .all + public func groupChatScope() -> GroupChatScope? { + switch self { + case let .group(_, groupChatScope): groupChatScope?.toChatScope() + default: nil + } + } + + 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? { switch self { case let .direct(contact): return contact.chatSettings - case let .group(groupInfo): return groupInfo.chatSettings + case let .group(groupInfo, _): return groupInfo.chatSettings 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 useCommands: Bool { + switch self { + case let .direct(c): c.isBot + case let .group(g, _): (g.groupProfile.groupPreferences?.commands?.count ?? 0) > 0 + default: false + } + } + + public var menuCommands: [ChatBotCommand] { + switch self { + case let .direct(c): c.isBot ? c.profile.preferences?.commands ?? [] : [] + case let .group(g, _): g.groupProfile.groupPreferences?.commands ?? [] + default: [] + } + } + public var chatTags: [Int64]? { switch self { case let .direct(contact): return contact.chatTags - case let .group(groupInfo): return groupInfo.chatTags + case let .group(groupInfo, _): return groupInfo.chatTags default: return nil } } - var createdAt: Date { - switch self { - case let .direct(contact): return contact.createdAt - case let .group(groupInfo): return groupInfo.createdAt - case let .local(noteFolder): return noteFolder.createdAt - case let .contactRequest(contactRequest): return contactRequest.createdAt - case let .contactConnection(contactConnection): return contactConnection.createdAt - case .invalidJSON: return .now - } - } - - public var updatedAt: Date { - switch self { - case let .direct(contact): return contact.updatedAt - case let .group(groupInfo): return groupInfo.updatedAt - case let .local(noteFolder): return noteFolder.updatedAt - case let .contactRequest(contactRequest): return contactRequest.updatedAt - case let .contactConnection(contactConnection): return contactConnection.updatedAt - case .invalidJSON: return .now - } - } - public var chatTs: Date { switch self { case let .direct(contact): return contact.chatTs ?? contact.updatedAt - case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt + case let .group(groupInfo, _): return groupInfo.chatTs ?? groupInfo.updatedAt case let .local(noteFolder): return noteFolder.chatTs case let .contactRequest(contactRequest): return contactRequest.updatedAt case let .contactConnection(contactConnection): return contactConnection.updatedAt case .invalidJSON: return .now } } - + public func ttl(_ globalTTL: ChatItemTTL) -> ChatTTL { switch self { case let .direct(contact): @@ -1509,7 +1822,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } else { ChatTTL.userDefault(globalTTL) } - case let .group(groupInfo): + case let .group(groupInfo, _): return if let ciTTL = groupInfo.chatItemTTL { ChatTTL.chat(ChatItemTTL(ciTTL)) } else { @@ -1529,7 +1842,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public static var sampleData: ChatInfo.SampleData = SampleData( direct: ChatInfo.direct(contact: Contact.sampleData), - group: ChatInfo.group(groupInfo: GroupInfo.sampleData), + group: ChatInfo.group(groupInfo: GroupInfo.sampleData, groupChatScope: nil), local: ChatInfo.local(noteFolder: NoteFolder.sampleData), contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData), contactConnection: ChatInfo.contactConnection(contactConnection: PendingContactConnection.getSampleData()) @@ -1548,8 +1861,8 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike { self.chatItems = chatItems self.chatStats = chatStats } - - public static func invalidJSON(_ json: String) -> ChatData { + + public static func invalidJSON(_ json: Data?) -> ChatData { ChatData( chatInfo: .invalidJSON(json: json), chatItems: [], @@ -1559,20 +1872,64 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike { } public struct ChatStats: Decodable, Hashable { - public init(unreadCount: Int = 0, reportsCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) { + 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 enum GroupChatScope: Decodable { + case memberSupport(groupMemberId_: Int64?) + case reports // surrogate scope used for matching new items to opened Reports "chat scope" in UI, this type is not present in backend +} + +public func sameChatScope(_ scope1: GroupChatScope, _ scope2: GroupChatScope) -> Bool { + return switch (scope1, scope2) { + case let (.memberSupport(groupMemberId1_), .memberSupport(groupMemberId2_)): + groupMemberId1_ == groupMemberId2_ + case (.reports, .reports): + true + case (.reports, .memberSupport): + false + case (.memberSupport(groupMemberId_: let groupMemberId_), .reports): + false + } +} + +public enum GroupChatScopeInfo: Decodable, Hashable { + case memberSupport(groupMember_: GroupMember?) + case reports // surrogate scope used for matching new items to opened Reports "chat scope" in UI, this type is not present in backend + + public func toChatScope() -> GroupChatScope { + return switch self { + case let .memberSupport(groupMember_): + if let groupMember = groupMember_ { + .memberSupport(groupMemberId_: groupMember.groupMemberId) + } else { + .memberSupport(groupMemberId_: nil) + } + case .reports: .reports + } + } +} + public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var contactId: Int64 var localDisplayName: ContactName @@ -1587,30 +1944,33 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { var createdAt: Date var updatedAt: Date var chatTs: Date? - var contactGroupMemberId: Int64? + public var preparedContact: PreparedContact? + public var contactRequestId: Int64? + public var contactGroupMemberId: Int64? var contactGrpInvSent: Bool + public var groupDirectInv: GroupDirectInvitation? 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 { - ( - sndReady - && active - && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false) - && !(activeConn?.connDisabled ?? true) - ) - || nextSendGrpInv - } } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } + public var nextConnectPrepared: Bool { active && preparedContact != nil && (activeConn == nil || activeConn?.connStatus == .prepared) } + public var profileChangeProhibited: Bool { activeConn != nil } + public var nextAcceptContactRequest: Bool { + active && + (contactRequestId != nil || groupDirectInv != nil) && + (activeConn == nil || activeConn?.connStatus == .new || activeConn?.connStatus == .prepared) + } + public var sendMsgToConnect: Bool { nextSendGrpInv || nextConnectPrepared } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } + public var shortDescr: String? { profile.shortDescr } public var image: String? { get { profile.image } } public var contactLink: String? { get { profile.contactLink } } public var localAlias: String { profile.localAlias } @@ -1624,16 +1984,30 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { } } + public var isContactCard: Bool { + (activeConn == nil || activeConn?.connStatus == .prepared) && profile.contactLink != nil && active && preparedContact == nil && contactRequestId == nil + } + + @inline(__always) + public var isBot: Bool { + profile.peerType == .bot + } + public var contactConnIncognito: Bool { activeConn?.customUserProfileId != nil } + public var chatIconName: String { + isBot ? "cube.fill" : "person.crop.circle.fill" + } + public func allowsFeature(_ feature: ChatFeature) -> Bool { switch feature { case .timedMessages: return mergedPreferences.timedMessages.contactPreference.allow != .no case .fullDelete: return mergedPreferences.fullDelete.contactPreference.allow != .no case .reactions: return mergedPreferences.reactions.contactPreference.allow != .no case .voice: return mergedPreferences.voice.contactPreference.allow != .no + case .files: return mergedPreferences.files.contactPreference.allow != .no case .calls: return mergedPreferences.calls.contactPreference.allow != .no } } @@ -1644,6 +2018,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { case .fullDelete: return mergedPreferences.fullDelete.userPreference.preference.allow != .no case .reactions: return mergedPreferences.reactions.userPreference.preference.allow != .no case .voice: return mergedPreferences.voice.userPreference.preference.allow != .no + case .files: return mergedPreferences.files.userPreference.preference.allow != .no case .calls: return mergedPreferences.calls.userPreference.preference.allow != .no } } @@ -1666,6 +2041,36 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { ) } +public struct PreparedContact: Decodable, Hashable { + public var connLinkToConnect: CreatedConnLink + public var uiConnLinkType: ConnectionMode +} + +public struct GroupDirectInvitation: Decodable, Hashable { + public var groupDirectInvLink: String + public var fromGroupId_: Int64? + public var fromGroupMemberId_: Int64? + public var fromGroupMemberConnId_: Int64? + public var groupDirectInvStartedConnection: Bool + + public var memberRemoved: Bool { + fromGroupId_ == nil || fromGroupMemberId_ == nil || fromGroupMemberConnId_ == nil + } + + public static let sampleData = GroupDirectInvitation( + groupDirectInvLink: "simplex_link", + fromGroupId_: 1, + fromGroupMemberId_: 1, + fromGroupMemberConnId_: 1, + groupDirectInvStartedConnection: false + ) +} + +public enum ConnectionMode: String, Decodable, Hashable { + case inv + case con +} + public enum ContactStatus: String, Decodable, Hashable { case active = "active" case deleted = "deleted" @@ -1681,16 +2086,11 @@ public struct ContactRef: Decodable, Equatable, Hashable { public var id: ChatId { get { "@\(contactId)" } } } -public struct ContactSubStatus: Decodable, Hashable { - public var contact: Contact - public var contactError: ChatError? -} - public struct Connection: Decodable, Hashable { public var connId: Int64 public var agentConnId: String public var peerChatVRange: VersionRange - var connStatus: ConnStatus + public var connStatus: ConnStatus public var connLevel: Int public var viaGroupLink: Bool public var customUserProfileId: Int64? @@ -1765,10 +2165,6 @@ public struct UserContact: Decodable, Hashable { self.userContactLinkId = userContactLinkId } - public init(contactRequest: UserContactRequest) { - self.userContactLinkId = contactRequest.userContactLinkId - } - public var id: String { "@>\(userContactLinkId)" } @@ -1776,25 +2172,25 @@ public struct UserContact: Decodable, Hashable { public struct UserContactRequest: Decodable, NamedChat, Hashable { var contactRequestId: Int64 - public var userContactLinkId: Int64 + public var userContactLinkId_: Int64? public var cReqChatVRange: VersionRange var localDisplayName: ContactName var profile: Profile var createdAt: Date public var updatedAt: Date - public var id: ChatId { get { "<@\(contactRequestId)" } } + public var id: ChatId { get { contactRequestChatId(contactRequestId) } } public var apiId: Int64 { get { contactRequestId } } var ready: Bool { get { true } } - public var sendMsgEnabled: Bool { get { false } } public var displayName: String { get { profile.displayName } } + public var shortDescr: String? { profile.shortDescr } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } public var localAlias: String { "" } public static let sampleData = UserContactRequest( contactRequestId: 1, - userContactLinkId: 1, + userContactLinkId_: 1, cReqChatVRange: VersionRange(1, 1), localDisplayName: "alice", profile: Profile.sampleData, @@ -1803,6 +2199,10 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable { ) } +public func contactRequestChatId(_ contactRequestId: Int64) -> ChatId { + return "<@\(contactRequestId)" +} + public struct PendingContactConnection: Decodable, NamedChat, Hashable { public var pccConnId: Int64 var pccAgentConnId: String @@ -1810,7 +2210,7 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable { 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 @@ -1818,7 +2218,6 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable { public var id: ChatId { get { ":\(pccConnId)" } } public var apiId: Int64 { get { pccConnId } } var ready: Bool { get { false } } - public var sendMsgEnabled: Bool { get { false } } var localDisplayName: String { get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) } } @@ -1837,6 +2236,7 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable { } } public var fullName: String { get { "" } } + public var shortDescr: String? { nil } public var image: String? { get { nil } } public var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } } @@ -1938,19 +2338,22 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { 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 preparedGroup: PreparedGroup? public var uiThemes: ThemeModeOverrides? + public var membersRequireAttention: Int 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 nextConnectPrepared: Bool { if let preparedGroup { !preparedGroup.connLinkStartedConnection } else { false } } + public var profileChangeProhibited: Bool { preparedGroup?.connLinkPreparedConnection ?? false } public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } + public var shortDescr: String? { groupProfile.shortDescr } public var image: String? { get { groupProfile.image } } public var chatTags: [Int64] public var chatItemTTL: Int64? @@ -1961,55 +2364,128 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { } public var canDelete: Bool { - return membership.memberRole == .owner || !membership.memberCurrent + return membership.memberRole == .owner || !membership.memberCurrentOrPending } public var canAddMembers: Bool { return membership.memberRole >= .admin && membership.memberActive } + public var canModerate: Bool { + return membership.memberRole >= .moderator && membership.memberActive + } + + public var chatIconName: String { + switch businessChat?.chatType { + case .none: "person.2.circle.fill" + case .business: "briefcase.circle.fill" + case .customer: "person.crop.circle.fill" + } + } + public static let sampleData = GroupInfo( groupId: 1, localDisplayName: "team", groupProfile: GroupProfile.sampleData, fullGroupPreferences: FullGroupPreferences.sampleData, membership: GroupMember.sampleData, - hostConnCustomUserProfileId: nil, chatSettings: ChatSettings.defaults, createdAt: .now, updatedAt: .now, + membersRequireAttention: 0, chatTags: [], localAlias: "" ) } +public struct PreparedGroup: Decodable, Hashable { + public var connLinkToConnect: CreatedConnLink + public var connLinkPreparedConnection: Bool + public var connLinkStartedConnection: Bool +} + public struct GroupRef: Decodable, Hashable { public var groupId: Int64 var localDisplayName: GroupName } public struct GroupProfile: Codable, NamedChat, Hashable { - public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) { + public init( + displayName: String, + fullName: String, + shortDescr: String? = nil, + description: String? = nil, + image: String? = nil, + groupPreferences: GroupPreferences? = nil, + memberAdmission: GroupMemberAdmission? = nil + ) { self.displayName = displayName self.fullName = fullName + self.shortDescr = shortDescr self.description = description self.image = image self.groupPreferences = groupPreferences + self.memberAdmission = memberAdmission } public var displayName: String public var fullName: String + public var shortDescr: String? public var description: String? public var image: String? public var groupPreferences: GroupPreferences? + public var memberAdmission: GroupMemberAdmission? public var localAlias: String { "" } + public var memberAdmission_: GroupMemberAdmission { + get { self.memberAdmission ?? GroupMemberAdmission() } + set { memberAdmission = newValue } + } + public static let sampleData = GroupProfile( displayName: "team", fullName: "My Team" ) } +public struct GroupMemberAdmission: Codable, Hashable { + public var review: MemberCriteria? + + public init( + review: MemberCriteria? = nil + ) { + self.review = review + } + + public static let sampleData = GroupMemberAdmission( + review: .all + ) +} + +public enum MemberCriteria: String, Codable, Identifiable, Hashable { + case all + + public static var values: [MemberCriteria] { [.all] } + + public var id: Self { self } + + public var text: String { + switch self { + case .all: return NSLocalizedString("all", comment: "member criteria value") + } + } +} + +public struct ContactShortLinkData: Codable, Hashable { + public var profile: Profile + public var message: MsgContent? + public var business: Bool +} + +public struct GroupShortLinkData: Codable, Hashable { + public var groupProfile: GroupProfile +} + public struct BusinessChatInfo: Decodable, Hashable { public var chatType: BusinessChatType public var businessId: String @@ -2036,6 +2512,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public var memberContactId: Int64? public var memberContactProfileId: Int64 public var activeConn: Connection? + public var supportChat: GroupSupportChat? public var memberChatVRange: VersionRange public var id: String { "#\(groupId) @\(groupMemberId)" } @@ -2051,7 +2528,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { get { let p = memberProfile let name = p.localAlias == "" ? p.displayName : p.localAlias - return pastMember(name) + return unknownMember(name) } } public var fullName: String { get { memberProfile.fullName } } @@ -2078,23 +2555,42 @@ public struct GroupMember: Identifiable, Decodable, Hashable { ? p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") : p.localAlias ) - return pastMember(name) + return unknownMember(name) } } - private func pastMember(_ name: String) -> String { + private func unknownMember(_ name: String) -> String { memberStatus == .memUnknown - ? String.localizedStringWithFormat(NSLocalizedString("Past member %@", comment: "past/unknown group member"), name) + ? ( + memberId.hasPrefix(name) + // unknown member was created using memberId for name + ? String.localizedStringWithFormat(NSLocalizedString("Member %@", comment: "past/unknown group member"), name) + // unknown member was created with name + : 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 unknownMember(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 .memPendingReview: return true case .memIntroduced: return false case .memIntroInvited: return false case .memAccepted: return false @@ -2107,11 +2603,14 @@ public struct GroupMember: Identifiable, Decodable, Hashable { 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 .memPendingReview: return false case .memIntroduced: return true case .memIntroInvited: return true case .memAccepted: return true @@ -2122,28 +2621,49 @@ public struct GroupMember: Identifiable, Decodable, Hashable { } } + public var memberPending: Bool { + switch memberStatus { + case .memPendingApproval: return true + case .memPendingReview: return true + default: return false + } + } + + public var memberCurrentOrPending: Bool { + memberCurrent || memberPending + } + public func canBeRemoved(groupInfo: GroupInfo) -> Bool { let userRole = groupInfo.membership.memberRole - return memberStatus != .memRemoved && memberStatus != .memLeft - && userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive + return userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive } public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { - if !canBeRemoved(groupInfo: groupInfo) { return nil } + if !canBeRemoved(groupInfo: groupInfo) || memberStatus == .memRemoved || memberStatus == .memLeft || memberPending { return nil } let userRole = groupInfo.membership.memberRole return GroupMemberRole.supportedRoles.filter { $0 <= userRole } } public func canBlockForAll(groupInfo: GroupInfo) -> Bool { let userRole = groupInfo.membership.memberRole - return memberStatus != .memRemoved && memberStatus != .memLeft && memberRole < .admin - && userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive + return memberRole < .moderator + && userRole >= .moderator && userRole >= memberRole && groupInfo.membership.memberActive + && !memberPending } - + public var canReceiveReports: Bool { memberRole >= .moderator && versionRange.maxVersion >= REPORTS_VERSION } + public var supportChatNotRead: Bool { + if let supportChat = supportChat, + supportChat.memberAttention > 0 || supportChat.mentions > 0 || supportChat.unread > 0 { + true + } else { + false + } + } + public var versionRange: VersionRange { if let activeConn { activeConn.peerChatVRange @@ -2151,7 +2671,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { memberChatVRange } } - + public var memberIncognito: Bool { memberProfile.profileId != memberContactProfileId } @@ -2175,6 +2695,13 @@ public struct GroupMember: Identifiable, Decodable, Hashable { ) } +public struct GroupSupportChat: Codable, Hashable { + public var chatTs: Date + public var unread: Int + public var memberAttention: Int + public var mentions: Int +} + public struct GroupMemberSettings: Codable, Hashable { public var showMessages: Bool } @@ -2199,8 +2726,8 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod public var id: Self { self } - public static var supportedRoles: [GroupMemberRole] = [.observer, .member, .admin, .owner] - + public static var supportedRoles: [GroupMemberRole] = [.observer, .member, .moderator, .admin, .owner] + public var text: String { switch self { case .observer: return NSLocalizedString("observer", comment: "member role") @@ -2237,11 +2764,14 @@ public enum GroupMemberCategory: String, Decodable, Hashable { } 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 memPendingReview = "pending_review" case memIntroduced = "introduced" case memIntroInvited = "intro-inv" case memAccepted = "accepted" @@ -2252,11 +2782,14 @@ public enum GroupMemberStatus: String, Decodable, Hashable { 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 .memPendingReview: return "pending review" case .memIntroduced: return "connecting (introduced)" case .memIntroInvited: return "connecting (introduction invitation)" case .memAccepted: return "connecting (accepted)" @@ -2269,11 +2802,14 @@ public enum GroupMemberStatus: String, Decodable, Hashable { 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 .memPendingReview: return "review" case .memIntroduced: return "connecting" case .memIntroInvited: return "connecting" case .memAccepted: return "connecting" @@ -2296,9 +2832,9 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat, Hashable { public var id: ChatId { get { "*\(noteFolderId)" } } public var apiId: Int64 { get { noteFolderId } } public var ready: Bool { get { true } } - public var sendMsgEnabled: Bool { get { true } } public var displayName: String { get { ChatInfo.privateNotesChatName } } public var fullName: String { get { "" } } + public var shortDescr: String? { nil } public var image: String? { get { nil } } public var localAlias: String { get { "" } } @@ -2324,47 +2860,50 @@ public enum InvitedBy: Decodable, Hashable { case unknown } -public struct MemberSubError: Decodable, Hashable { - var member: GroupMemberIds - var memberError: ChatError -} - 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 .rcvDirectMsgConnection(conn, contact): + contact?.id ?? conn.id case let .rcvGroupMsgConnection(_, _, groupMember): - return groupMember.id + groupMember.id case let .userContactConnection(_, userContact): - return userContact.id - default: - return nil + userContact.id } } + // 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 conn: Connection { switch self { 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 NtfConn: Decodable, Hashable { - public var user_: User? - public var connEntity_: ConnectionEntity? + 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 { @@ -2372,6 +2911,44 @@ public struct NtfMsgInfo: Decodable, Hashable { public var msgTs: Date } +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 } + } + + @inline(__always) + public var isError: Bool { + if case .error = self { true } else { false } + } +} + +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? @@ -2385,12 +2962,27 @@ public struct ChatItemDeletion: Decodable, Hashable { public struct AChatItem: Decodable, Hashable { public var chatInfo: ChatInfo public var chatItem: ChatItem +} - public var chatId: String { - if case let .groupRcv(groupMember) = chatItem.chatDir { - return groupMember.id - } - return chatInfo.id +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 + ) } } @@ -2412,11 +3004,12 @@ public struct CIReaction: Decodable, Hashable { } public struct ChatItem: Identifiable, Decodable, Hashable { - public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) { + 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 @@ -2426,6 +3019,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { 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? @@ -2434,7 +3028,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { 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 } @@ -2476,30 +3070,33 @@ public struct ChatItem: Identifiable, Decodable, Hashable { } public var mergeCategory: CIMergeCategory? { - switch content { - case .rcvChatFeature: .chatFeature - case .sndChatFeature: .chatFeature - case .rcvGroupFeature: .chatFeature - case .sndGroupFeature: .chatFeature - case let.rcvGroupEvent(event): - switch event { - case .userRole: nil - case .userDeleted: nil - case .groupDeleted: nil - case .memberCreatedContact: nil - default: .rcvGroupEvent - } - case let .sndGroupEvent(event): - switch event { - case .userRole: nil - case .userLeft: nil - default: .sndGroupEvent - } - default: - if meta.itemDeleted == nil { + if meta.itemDeleted != nil { + chatDir.sent ? .sndItemDeleted : .rcvItemDeleted + } else { + switch content { + case .rcvChatFeature: .chatFeature + case .sndChatFeature: .chatFeature + case .rcvGroupFeature: .chatFeature + case .sndGroupFeature: .chatFeature + case let.rcvGroupEvent(event): + switch event { + case .userRole: nil + case .userDeleted: nil + case .groupDeleted: nil + case .memberCreatedContact: nil + case .newMemberPendingReview: nil + default: .rcvGroupEvent + } + case let .sndGroupEvent(event): + switch event { + case .userRole: nil + case .userLeft: nil + case .memberAccepted: nil + case .userPendingReview: nil + default: .sndGroupEvent + } + default: nil - } else { - chatDir.sent ? .sndItemDeleted : .rcvItemDeleted } } } @@ -2520,11 +3117,14 @@ public struct ChatItem: Identifiable, Decodable, Hashable { switch rcvDirectEvent { case .contactDeleted: return false case .profileUpdated: return false + case .groupInvLinkReceived: return true } case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent): switch rcvGroupEvent { case .groupUpdated: return false case .memberConnected: return false + case .memberAccepted: return false + case .userAccepted: return false case .memberRole: return false case .memberBlocked: return false case .userRole: return true @@ -2536,6 +3136,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { case .invitedViaGroupLink: return false case .memberCreatedContact: return false case .memberProfileUpdated: return false + case .newMemberPendingReview: return true } case .sndGroupEvent: return false case .rcvConnEvent: return false @@ -2555,6 +3156,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { case .rcvDirectE2EEInfo: return false case .sndGroupE2EEInfo: return false case .rcvGroupE2EEInfo: return false + case .chatBanner: return false case .invalidJSON: return false } } @@ -2604,14 +3206,14 @@ public struct ChatItem: Identifiable, Decodable, Hashable { public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember?)? { switch (chatInfo, chatDir) { - case let (.group(groupInfo), .groupRcv(groupMember)): + case let (.group(groupInfo, _), .groupRcv(groupMember)): let m = groupInfo.membership - return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil + return m.memberRole >= .moderator && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil ? (groupInfo, groupMember) : nil - case let (.group(groupInfo), .groupSnd): + case let (.group(groupInfo, _), .groupSnd): let m = groupInfo.membership - return m.memberRole >= .admin ? (groupInfo, nil) : nil + return m.memberRole >= .moderator ? (groupInfo, nil) : nil default: return nil } } @@ -2622,10 +3224,11 @@ public struct ChatItem: Identifiable, Decodable, Hashable { case .rcvDirectE2EEInfo: return false case .sndGroupE2EEInfo: return false case .rcvGroupE2EEInfo: return false + case .chatBanner: return false default: return true } } - + public var isReport: Bool { switch content { case let .sndMsgContent(msgContent), let .rcvMsgContent(msgContent): @@ -2725,14 +3328,14 @@ public struct ChatItem: Identifiable, Decodable, Hashable { 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( @@ -2745,8 +3348,10 @@ public struct ChatItem: Identifiable, Decodable, Hashable { itemDeleted: nil, itemEdited: false, itemLive: false, + userMention: false, deletable: false, - editable: false + editable: false, + showGroupAsSender: false ), content: .sndMsgContent(msgContent: .report(text: text, reason: reason)), quotedItem: CIQuote.getSample(item.id, item.meta.createdAt, item.text, chatDir: item.chatDir), @@ -2767,8 +3372,10 @@ public struct ChatItem: Identifiable, Decodable, Hashable { itemDeleted: nil, itemEdited: false, itemLive: false, + userMention: false, deletable: false, - editable: false + editable: false, + showGroupAsSender: false ), content: .rcvDeleted(deleteMode: .cidmBroadcast), quotedItem: nil, @@ -2789,8 +3396,10 @@ public struct ChatItem: Identifiable, Decodable, Hashable { itemDeleted: nil, itemEdited: false, itemLive: true, + userMention: false, deletable: false, - editable: false + editable: false, + showGroupAsSender: false ), content: .sndMsgContent(msgContent: .text("")), quotedItem: nil, @@ -2800,7 +3409,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { 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, @@ -2840,7 +3449,7 @@ public enum CIDirection: Decodable, Hashable { } } } - + public func sameDirection(_ dir: CIDirection) -> Bool { switch (self, dir) { case let (.groupRcv(m1), .groupRcv(m2)): m1.groupMemberId == m2.groupMemberId @@ -2862,8 +3471,10 @@ public struct CIMeta: Decodable, Hashable { 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 showGroupAsSender: Bool public var timestampText: Text { Text(formatTimestampMeta(itemTs)) } public var recent: Bool { updatedAt + 10 > .now } @@ -2886,8 +3497,10 @@ public struct CIMeta: Decodable, Hashable { itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, + userMention: false, deletable: deletable, - editable: editable + editable: editable, + showGroupAsSender: false ) } @@ -2902,8 +3515,10 @@ public struct CIMeta: Decodable, Hashable { itemDeleted: nil, itemEdited: false, itemLive: false, + userMention: false, deletable: false, - editable: false + editable: false, + showGroupAsSender: false ) } } @@ -2970,7 +3585,7 @@ public enum CIStatus: Decodable, Hashable { case .invalid: return "invalid" } } - + public var sent: Bool { switch self { case .sndNew: true @@ -2985,6 +3600,21 @@ public enum CIStatus: Decodable, Hashable { } } + // as in corresponds to SENT response from agent, opposed to `sent` which means snd status + public var isSent: Bool { + switch self { + case .sndNew: false + case .sndSent: true + case .sndRcvd: false + 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 @@ -3038,6 +3668,17 @@ public enum CIStatus: Decodable, Hashable { } } +public func shouldKeepOldSndCIStatus(oldStatus: CIStatus, newStatus: CIStatus) -> Bool { + switch (oldStatus, newStatus) { + case (.sndRcvd, let new) where !new.isSndRcvd: + return true + case (let old, .sndNew) where old.isSent: + return true + default: + return false + } +} + public enum SndError: Decodable, Hashable { case auth case quota @@ -3182,6 +3823,20 @@ public enum CIForwardedFrom: Decodable, Hashable { } } + 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)") @@ -3230,7 +3885,8 @@ public enum CIContent: Decodable, ItemContent, Hashable { case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo) case sndGroupE2EEInfo(e2eeInfo: E2EEInfo) case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo) - case invalidJSON(json: String) + case chatBanner + case invalidJSON(json: Data?) public var text: String { get { @@ -3265,13 +3921,14 @@ public enum CIContent: Decodable, ItemContent, Hashable { case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo) case .sndGroupE2EEInfo: return e2eeInfoNoPQStr case .rcvGroupE2EEInfo: return e2eeInfoNoPQStr + case .chatBanner: return "" case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item") } } } private func directE2EEInfoStr(_ e2eeInfo: E2EEInfo) -> String { - e2eeInfo.pqEnabled + e2eeInfo.pqEnabled == true ? NSLocalizedString("This chat is protected by quantum resistant end-to-end encryption.", comment: "E2EE info chat item") : e2eeInfoNoPQStr } @@ -3319,6 +3976,14 @@ public enum CIContent: Decodable, ItemContent, Hashable { } } + public var hasMsgContent: Bool { + if let mc = msgContent { + !mc.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } else { + false + } + } + public var showMemberName: Bool { switch self { case .rcvMsgContent: return true @@ -3450,8 +4115,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) } @@ -3725,13 +4394,13 @@ public enum FileError: Decodable, Equatable, Hashable { 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): NSLocalizedString("File is blocked by server operator:\n\(info.reason.text).", 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) @@ -3748,6 +4417,7 @@ public enum MsgContent: Equatable, Hashable { case voice(text: String, duration: Int) case file(String) case report(text: String, reason: ReportReason) + case chat(text: String, chatLink: MsgChatLink) // TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift case unknown(type: String, text: String) @@ -3760,6 +4430,7 @@ public enum MsgContent: Equatable, Hashable { case let .voice(text, _): return text case let .file(text): return text case let .report(text, _): return text + case let .chat(text, _): return text case let .unknown(_, text): return text } } @@ -3809,7 +4480,8 @@ public enum MsgContent: Equatable, Hashable { } } - var cmdString: String { + @inline(__always) + public var cmdString: String { "json \(encodeJSON(self))" } @@ -3820,6 +4492,7 @@ public enum MsgContent: Equatable, Hashable { case image case duration case reason + case chatLink } public static func == (lhs: MsgContent, rhs: MsgContent) -> Bool { @@ -3831,6 +4504,7 @@ public enum MsgContent: Equatable, Hashable { case let (.voice(lt, ld), .voice(rt, rd)): return lt == rt && ld == rd case let (.file(lf), .file(rf)): return lf == rf case let (.report(lt, lr), .report(rt, rr)): return lt == rt && lr == rr + case let (.chat(lt, ll), .chat(rt, rl)): return lt == rt && ll == rl case let (.unknown(lType, lt), .unknown(rType, rt)): return lType == rType && lt == rt default: return false } @@ -3870,6 +4544,10 @@ extension MsgContent: Decodable { 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) + case "chat": + let text = try container.decode(String.self, forKey: CodingKeys.text) + let chatLink = try container.decode(MsgChatLink.self, forKey: CodingKeys.chatLink) + self = .chat(text: text, chatLink: chatLink) default: let text = try? container.decode(String.self, forKey: CodingKeys.text) self = .unknown(type: type, text: text ?? "unknown message format") @@ -3911,6 +4589,10 @@ extension MsgContent: Encodable { try container.encode("report", forKey: .type) try container.encode(text, forKey: .text) try container.encode(reason, forKey: .reason) + case let .chat(text, chatLink): + try container.encode("chat", forKey: .type) + try container.encode(text, forKey: .text) + try container.encode(chatLink, forKey: .chatLink) // TODO use original JSON and type case let .unknown(_, text): try container.encode("text", forKey: .type) @@ -3919,13 +4601,48 @@ extension MsgContent: Encodable { } } +public enum MsgContentTag: String { + case text + case link + case image + case video + case voice + case file + case report +} + +public enum MsgChatLink: Codable, Equatable, Hashable { + case contact(connLink: String, profile: Profile, business: Bool) + case invitation(invLink: String, profile: Profile) + case group(connLink: String, groupProfile: GroupProfile) +} + public struct FormattedText: Decodable, Hashable { public var text: String public var format: Format? + public init(text: String, format: Format? = nil) { + self.text = text + self.format = format + } + + public static func plain(_ text: String) -> [FormattedText] { + text.isEmpty + ? [] + : [FormattedText(text: text, format: nil)] + } + public var isSecret: Bool { if case .secret = format { true } else { false } } + + public var linkUri: String? { + switch format { + case .uri: text + case let .hyperLink(_, linkUri): linkUri + default: nil + } + } } public enum Format: Decodable, Equatable, Hashable { @@ -3936,9 +4653,13 @@ public enum Format: Decodable, Equatable, Hashable { case secret case colored(color: FormatColor) case uri - case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) + case hyperLink(showText: String?, linkUri: String) + case simplexLink(showText: String?, linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) + case command(commandStr: String) + case mention(memberName: String) case email case phone + case unknown public var isSimplexLink: Bool { get { @@ -3954,12 +4675,16 @@ public enum SimplexLinkType: String, Decodable, Hashable { case contact case invitation case group + case channel + case relay 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") + case .relay: return NSLocalizedString("SimpleX relay link", comment: "simplex link type") } } } @@ -3974,18 +4699,16 @@ public enum FormatColor: String, Decodable, Hashable { 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 } } } @@ -3997,7 +4720,7 @@ public enum ReportReason: Hashable { case profile case other case unknown(type: String) - + public static var supportedReasons: [ReportReason] = [.spam, .illegal, .community, .profile, .other] public var text: String { @@ -4010,6 +4733,14 @@ public enum ReportReason: Hashable { 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 { @@ -4043,14 +4774,14 @@ extension ReportReason: Decodable { // Struct to use with simplex API public struct LinkPreview: Codable, Equatable, Hashable { - public init(uri: URL, title: String, description: String = "", image: String) { + public init(uri: String, title: String, description: String = "", image: String) { self.uri = uri self.title = title self.description = description self.image = image } - - public var uri: URL + + public var uri: String public var title: String // TODO remove once optional in haskell public var description: String = "" @@ -4061,9 +4792,61 @@ 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, Hashable { @@ -4154,17 +4937,20 @@ public enum CIGroupInvitationStatus: String, Decodable, Hashable { } public struct E2EEInfo: Decodable, Hashable { - public var pqEnabled: Bool + public var pqEnabled: Bool? } public enum RcvDirectEvent: Decodable, Hashable { case contactDeleted case profileUpdated(fromProfile: Profile, toProfile: Profile) + case groupInvLinkReceived(groupProfile: Profile) var text: String { switch self { case .contactDeleted: return NSLocalizedString("deleted contact", comment: "rcv direct event chat item") case let .profileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile) + case let .groupInvLinkReceived(groupProfile): + return String.localizedStringWithFormat(NSLocalizedString("requested connection from group %@", comment: "rcv direct event chat item"), groupProfile.displayName) } } @@ -4189,6 +4975,8 @@ public enum RcvDirectEvent: Decodable, Hashable { public enum RcvGroupEvent: Decodable, Hashable { case memberAdded(groupMemberId: Int64, profile: Profile) case memberConnected + case memberAccepted(groupMemberId: Int64, profile: Profile) + case userAccepted case memberLeft case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole) case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool) @@ -4200,12 +4988,16 @@ public enum RcvGroupEvent: Decodable, Hashable { case invitedViaGroupLink case memberCreatedContact case memberProfileUpdated(fromProfile: Profile, toProfile: Profile) + case newMemberPendingReview var text: String { switch self { case let .memberAdded(_, profile): return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName) case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item") + case let .memberAccepted(_, profile): + return String.localizedStringWithFormat(NSLocalizedString("accepted %@", comment: "rcv group event chat item"), profile.profileViewName) + case .userAccepted: return NSLocalizedString("accepted you", comment: "rcv group event chat item") case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item") case let .memberRole(_, profile, role): return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text) @@ -4223,8 +5015,9 @@ public enum RcvGroupEvent: Decodable, Hashable { case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item") case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item") case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item") - case .memberCreatedContact: return NSLocalizedString("connected directly", comment: "rcv group event chat item") + case .memberCreatedContact: return NSLocalizedString("requested connection", comment: "rcv group event chat item") case let .memberProfileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile) + case .newMemberPendingReview: return NSLocalizedString("New member wants to join the group.", comment: "rcv group event chat item") } } @@ -4249,6 +5042,8 @@ public enum SndGroupEvent: Decodable, Hashable { case memberDeleted(groupMemberId: Int64, profile: Profile) case userLeft case groupUpdated(groupProfile: GroupProfile) + case memberAccepted(groupMemberId: Int64, profile: Profile) + case userPendingReview var text: String { switch self { @@ -4266,6 +5061,9 @@ public enum SndGroupEvent: Decodable, Hashable { return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.profileViewName) case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item") case .groupUpdated: return NSLocalizedString("group profile updated", comment: "snd group event chat item") + case .memberAccepted: return NSLocalizedString("you accepted this member", comment: "snd group event chat item") + case .userPendingReview: + return NSLocalizedString("Please wait for group moderators to review your request to join the group.", comment: "snd group event chat item") } } } @@ -4412,9 +5210,9 @@ public enum ChatItemTTL: Identifiable, Comparable, Hashable { 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 @@ -4423,21 +5221,21 @@ public enum ChatTTL: Identifiable, Hashable { 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 @@ -4450,9 +5248,9 @@ 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 diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 2bf861f437..451ac8b4ef 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -9,14 +9,12 @@ import Foundation public protocol ChatLike { - var chatInfo: ChatInfo { get} - var chatItems: [ChatItem] { get } - var chatStats: ChatStats { get } + var chatInfo: ChatInfo { get } } extension ChatLike { public func groupFeatureEnabled(_ feature: GroupFeature) -> Bool { - if case let .group(groupInfo) = self.chatInfo { + if case let .group(groupInfo, _) = self.chatInfo { let p = groupInfo.fullGroupPreferences return switch feature { case .timedMessages: p.timedMessages.on @@ -27,6 +25,7 @@ extension ChatLike { case .files: p.files.on(for: groupInfo.membership) case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership) case .history: p.history.on + case .reports: p.reports.on } } else { return true @@ -81,9 +80,9 @@ public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool { 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 let .direct(contact): cInfo.sendMsgEnabled && !contact.sendMsgToConnect + case .group: cInfo.sendMsgEnabled + case .local: cInfo.sendMsgEnabled case .contactRequest: false case .contactConnection: false case .invalidJSON: false @@ -92,13 +91,8 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { 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 let .direct(contact): contact.chatIconName + case let .group(groupInfo, _): groupInfo.chatIconName case .local: "folder.circle.fill" case .contactRequest: "person.crop.circle.fill" default: "circle.fill" diff --git a/apps/ios/SimpleXChat/CryptoFile.swift b/apps/ios/SimpleXChat/CryptoFile.swift index 91f8997ad6..c067cb33bc 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] } } @@ -63,10 +63,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 index 5b9acc4fca..2920c2383c 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -11,7 +11,6 @@ import SwiftUI public struct ErrorAlert: Error { public let title: LocalizedStringKey public let message: LocalizedStringKey? - public let actions: Optional<() -> AnyView> public init( title: LocalizedStringKey, @@ -19,7 +18,6 @@ public struct ErrorAlert: Error { ) { self.title = title self.message = message - self.actions = nil } public init( @@ -29,7 +27,6 @@ public struct ErrorAlert: Error { ) { self.title = title self.message = message - self.actions = { AnyView(actions()) } } public init(_ title: LocalizedStringKey) { @@ -37,22 +34,18 @@ public struct ErrorAlert: Error { } public init(_ error: any Error) { - self = if let chatResponse = error as? ChatResponse { - ErrorAlert(chatResponse) + self = if let e = error as? ChatError { + ErrorAlert(e) } else { - ErrorAlert(LocalizedStringKey(error.localizedDescription)) + ErrorAlert("\(error.localizedDescription)") } } public init(_ chatError: ChatError) { - self = ErrorAlert("\(chatErrorString(chatError))") - } - - public init(_ chatResponse: ChatResponse) { - self = if let networkErrorAlert = getNetworkErrorAlert(chatResponse) { + self = if let networkErrorAlert = getNetworkErrorAlert(chatError) { networkErrorAlert } else { - ErrorAlert("\(responseError(chatResponse))") + ErrorAlert("\(chatErrorString(chatError))") } } } @@ -79,11 +72,7 @@ extension View { set: { if !$0 { errorAlert.wrappedValue = nil } } ), actions: { - if let actions_ = errorAlert.wrappedValue?.actions { - actions_() - } else { - if let alert = errorAlert.wrappedValue { actions(alert) } - } + if let alert = errorAlert.wrappedValue { actions(alert) } }, message: { if let message = errorAlert.wrappedValue?.message { @@ -94,22 +83,23 @@ extension View { } } -public func getNetworkErrorAlert(_ r: ChatResponse) -> ErrorAlert? { - switch r { - 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.") - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .HOST))): - return ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).") - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TRANSPORT(.version)))): - return ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).") - case let .chatCmdError(_, .errorAgent(.SMP(serverAddress, .PROXY(proxyErr)))): - return smpProxyErrorAlert(proxyErr, serverAddress) - case let .chatCmdError(_, .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr))))): - return proxyDestinationErrorAlert(proxyErr, proxyServer, relayServer) - default: - return nil +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(.unknownCAError))): + ErrorAlert(title: "Connection error", message: "Fingerprint in server address does not match certificate: \(serverHostname(addr)).") + 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 } } @@ -117,6 +107,8 @@ private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> Er 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(.unknownCAError)): + return ErrorAlert(title: "Private routing error", message: "Fingerprint in forwarding server address does not match certificate: \(serverHostname(srvAddr)).") 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): @@ -132,6 +124,8 @@ private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: S 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(.unknownCAError)): + return ErrorAlert(title: "Private routing error", message: "Fingerprint in destination server address does not match certificate: \(serverHostname(relayServer)).") 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: diff --git a/apps/ios/SimpleXChat/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift index bc01ee5f6b..9701b01c03 100644 --- a/apps/ios/SimpleXChat/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -454,7 +454,7 @@ public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { let resized = resizeImageToStrSizeSync(image, maxDataSize: 14000), let title = metadata.title, let uri = metadata.originalURL { - linkPreview = LinkPreview(uri: uri, title: title, image: resized) + linkPreview = LinkPreview(uri: uri.absoluteString, title: title, image: resized) } } cb(linkPreview) diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index a922e3a816..31b7ef83ff 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -62,7 +62,7 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, 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 { + if case let .group(groupInfo, _) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: previewMode == .hidden) } else { title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):" @@ -111,10 +111,6 @@ public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntit title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: hideContent) body = NSLocalizedString("message received", comment: "notification") targetContentIdentifier = groupInfo.id - case .sndFileConnection: - title = NSLocalizedString("Sent file event", comment: "notification") - case .rcvFileConnection: - title = NSLocalizedString("Received file event", comment: "notification") case .userContactConnection: title = NSLocalizedString("New contact request", comment: "notification") } @@ -203,6 +199,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/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index af11f21e5b..0363c7957b 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -20,10 +20,11 @@ typedef void* chat_ctrl; extern char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm, int backgroundMode, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctl); extern char *chat_reopen_store(chat_ctrl ctl); -extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); +extern char *chat_parse_uri(char *str, int safe); extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_valid_name(char *name); extern int chat_json_length(char *str); diff --git a/apps/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 4731e7b829..e75173d6cf 100644 --- a/apps/ios/SimpleXChat/hs_init.c +++ b/apps/ios/SimpleXChat/hs_init.c @@ -29,10 +29,10 @@ void haskell_init_nse(void) { char *argv[] = { "simplex", "+RTS", // requires `hs_init_with_rtsopts` - "-A1m", // chunk size for new allocations - "-H1m", // initial heap size + "-A256k", // chunk size for new allocations + "-H512k", // initial heap size "-F0.5", // heap growth triggering GC - "-Fd1", // memory return + "-Fd0.3", // memory return "-c", // compacting garbage collector 0 }; diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index f2059d5627..038546f889 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (може да се копира)"; @@ -22,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)"; @@ -82,6 +61,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е."; +/* 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."; @@ -142,12 +124,21 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ е потвърдено"; +/* No comment provided by engineer. */ +"%@ server" = "%@ сървър"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ сървъри"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ качено"; /* notification title */ "%@ wants to connect!" = "%@ иска да се свърже!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ и %lld членове"; @@ -160,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 мин."; @@ -172,6 +178,9 @@ /* time interval */ "%d sec" = "%d сек."; +/* delete after time */ +"%d seconds(s)" = "%d секунди"; + /* integrity error chat item */ "%d skipped message(s)" = "%d пропуснато(и) съобщение(я)"; @@ -214,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 секунди"; @@ -262,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 ден"; /* time interval */ @@ -271,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 минути"; @@ -310,33 +328,79 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Откажи смяна на адрес?"; +/* No comment provided by engineer. */ +"About operators" = "За операторите"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "За SimpleX Chat"; /* No comment provided by engineer. */ "above, then choose:" = "по-горе, след това избери:"; +/* No comment provided by engineer. */ +"Accent" = "Акцент"; + /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Приеми"; +/* alert action */ +"Accept as member" = "Приеми като член"; + +/* alert action */ +"Accept as observer" = "Приеми като наблюдател"; + +/* No comment provided by engineer. */ +"Accept conditions" = "Приеми условията"; + /* No comment provided by engineer. */ "Accept connection request?" = "Приемане на заявка за връзка?"; +/* alert title */ +"Accept contact request" = "Приеми заявка за контакт"; + /* notification body */ "Accept contact request from %@?" = "Приемане на заявка за контакт от %@?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Приеми инкогнито"; +/* alert title */ +"Accept member" = "Приеми член"; + /* call status */ "accepted call" = "обаждането прието"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Приети условия"; + +/* No comment provided by engineer. */ +"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 friends" = "Добави приятели"; + +/* No comment provided by engineer. */ +"Add list" = "Добави списък"; + +/* placeholder for sending contact request */ +"Add message" = "Добави съобщение"; + /* No comment provided by engineer. */ "Add profile" = "Добави профил"; @@ -346,18 +410,48 @@ /* 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" = "Допълнителен вторичен"; + /* 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" = "админ"; @@ -373,27 +467,42 @@ /* 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!" = "Всички съобщения ще бъдат изтрити - това не може да бъде отменено!"; @@ -403,6 +512,15 @@ /* 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." = "Всички ваши контакти ще останат свързани."; @@ -418,9 +536,18 @@ /* 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 files and media only if your contact allows them." = "Разреши файлове и медия само ако вашият контакт ги разрешава."; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа)"; @@ -436,9 +563,15 @@ /* 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." = "Позволи изпращане на файлове и медия."; @@ -466,21 +599,27 @@ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Позволи на вашите контакти да изпращат изчезващи съобщения."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Позволи на вашите контактите да изпращат файлове и медия."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Позволи на вашите контакти да изпращат гласови съобщения."; /* No comment provided by engineer. */ "Already connected?" = "Вече сте свързани?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "В процес на свързване!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Вече се присъединихте към групата!"; /* pref value */ "always" = "винаги"; +/* No comment provided by engineer. */ +"Always use private routing." = "Винаги използвай поверително рутиране."; + /* No comment provided by engineer. */ "Always use relay" = "Винаги използвай реле"; @@ -490,6 +629,9 @@ /* No comment provided by engineer. */ "and %lld other events" = "и %lld други събития"; +/* report reason */ +"Another reason" = "Друга причина"; + /* No comment provided by engineer. */ "Answer call" = "Отговор на повикване"; @@ -505,6 +647,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" = "Икона на приложението"; @@ -514,6 +659,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" = "Версия на приложението"; @@ -526,9 +674,36 @@ /* 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" = "Архивиране на база данни"; @@ -577,6 +752,9 @@ /* No comment provided by engineer. */ "Back" = "Назад"; +/* No comment provided by engineer. */ +"Background" = "Фон"; + /* No comment provided by engineer. */ "Bad desktop address" = "Грешен адрес на настолното устройство"; @@ -592,12 +770,45 @@ /* 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. */ +"Bio" = "Био"; + +/* alert title */ +"Bio too large" = "Биографията е твърде дълга"; + +/* No comment provided by engineer. */ +"Black" = "Черна"; + /* No comment provided by engineer. */ "Block" = "Блокирай"; @@ -622,15 +833,25 @@ /* 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" = "удебелен"; +/* No comment provided by engineer. */ +"Bot" = "Бот"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "И вие, и вашият контакт можете да добавяте реакции към съобщението."; @@ -643,15 +864,33 @@ /* 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 files and media." = "И вие, и вашият контакт можете да изпращате файлове и медия."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "И вие, и вашият контакт можете да изпращате гласови съобщения."; /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Български, финландски, тайландски и украински - благодарение на потребителите и [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Бизнес адрес"; + +/* No comment provided by engineer. */ +"Business chats" = "Бизнес чатове"; + +/* No comment provided by engineer. */ +"Business connection" = "Бизнес връзка"; + +/* 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 already ended!" = "Разговорът вече приключи!"; @@ -667,17 +906,33 @@ /* 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" = "Обаждането на члена не е позволено"; + +/* alert title */ +"Can't change profile" = "Промяната на профила е невъзможна"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Не може да покани контакта!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Не може да поканят контактите!"; +/* No comment provided by engineer. */ +"Can't message member" = "Изпращането на съобщения на груповия член не е налично"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Отказ"; /* No comment provided by engineer. */ @@ -689,15 +944,27 @@ /* No comment provided by engineer. */ "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?" = "Промяна на паролата на базата данни?"; @@ -723,7 +990,7 @@ "Change self-destruct mode" = "Промени режима на самоунищожение"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Промени кода за достъп за самоунищожение"; /* chat item text */ @@ -741,17 +1008,32 @@ /* chat item text */ "changing address…" = "промяна на адреса…"; +/* No comment provided by engineer. */ +"Chat" = "Чат"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Чатът вече съществува"; + +/* new chat sheet title */ +"Chat already exists!" = "Чатът вече съществува!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Цветове на чата"; + /* 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 exported" = "Базата данни е експортирана"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Базата данни на е импортирана"; /* No comment provided by engineer. */ "Chat is running" = "Чатът работи"; @@ -762,18 +1044,51 @@ /* 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!" = "Чатът ще бъде изтрит за вас - това не може да бъде отменено!"; + +/* chat toolbar */ +"Chat with admins" = "Чат с администраторите"; + +/* No comment provided by engineer. */ +"Chat with member" = "Чат с член"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Разговаряйте с членовете, преди да се присъединят."; + /* No comment provided by engineer. */ "Chats" = "Чатове"; +/* No comment provided by engineer. */ +"Chats with members" = "Чатове с членовете"; + +/* 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." = "Проверете адреса на сървъра и опитайте отново."; @@ -789,6 +1104,15 @@ /* No comment provided by engineer. */ "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" = "Изчисти"; @@ -798,15 +1122,30 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Изчисти разговора?"; +/* No comment provided by engineer. */ +"Clear group?" = "Изчисти група?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Изчисти или изтрий група?"; + /* No comment provided by engineer. */ "Clear private notes?" = "Изчистване на лични бележки?"; /* No comment provided by engineer. */ "Clear verification" = "Изчисти проверката"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Цветни чатове с нови теми."; + +/* No comment provided by engineer. */ +"Color mode" = "Цветен режим"; + /* No comment provided by engineer. */ "colored" = "цветен"; +/* report reason */ +"Community guidelines violation" = "Нарушение на правилата на общността"; + /* server test step */ "Compare file" = "Сравни файл"; @@ -816,15 +1155,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): **%@**." = "Условията вече са приети за тези оператори: **%@**."; + +/* alert button */ +"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" = "Потвърди мрежовите настройки"; @@ -843,6 +1215,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Потвърди качването"; +/* token status text */ +"Confirmed" = "Потвърдено"; + /* server test step */ "Connect" = "Свързване"; @@ -850,7 +1225,7 @@ "Connect automatically" = "Автоматично свъзрване"; /* No comment provided by engineer. */ -"Connect incognito" = "Свързване инкогнито"; +"Connect faster! 🚀" = "Свържете се по-бързо! 🚀"; /* No comment provided by engineer. */ "Connect to desktop" = "Свързване с настолно устройство"; @@ -859,34 +1234,37 @@ "connect to SimpleX Chat developers." = "свържете се с разработчиците на SimpleX Chat."; /* No comment provided by engineer. */ -"Connect to yourself?" = "Свърване със себе си?"; +"Connect to your friends faster." = "Свържете се с приятелите си по-бързо."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Свърване със себе си?\nТова е вашят еднократен линк за връзка!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Свърване със себе си?\nТова е вашият личен SimpleX адрес!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Свързване чрез адрес за контакт"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Свърване чрез линк"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Свързване чрез еднократен линк за връзка"; -/* No comment provided by engineer. */ +/* new chat action */ "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" = "Свързан с настолно устройство"; @@ -894,6 +1272,9 @@ /* No comment provided by engineer. */ "connecting" = "свързване"; +/* No comment provided by engineer. */ +"Connecting" = "Свързване"; + /* No comment provided by engineer. */ "connecting (accepted)" = "свързване (прието)"; @@ -915,6 +1296,9 @@ /* 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" = "Свързване с настолно устройство"; @@ -925,6 +1309,12 @@ "Connection" = "Връзка"; /* No comment provided by engineer. */ +"Connection and servers status." = "Състояние на връзката и сървърите."; + +/* No comment provided by engineer. */ +"Connection blocked" = "Връзката е блокирана"; + +/* alert title */ "Connection error" = "Грешка при свързване"; /* No comment provided by engineer. */ @@ -933,13 +1323,25 @@ /* 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 terminated" = "Връзката е прекратена"; -/* No comment provided by engineer. */ +/* alert title */ "Connection timeout" = "Времето на изчакване за установяване на връзката изтече"; /* connection information */ @@ -991,16 +1393,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 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" = "Създай групов линк"; @@ -1018,10 +1423,7 @@ "Create queue" = "Създай опашка"; /* No comment provided by engineer. */ -"Create secret group" = "Създай тайна група"; - -/* No comment provided by engineer. */ -"Create SimpleX address" = "Създай SimpleX адрес"; +"Create SimpleX address" = "Създаване на адрес в SimpleX"; /* No comment provided by engineer. */ "Create your profile" = "Създай своя профил"; @@ -1125,7 +1527,8 @@ /* message decrypt error item */ "Decryption error" = "Грешка при декриптиране"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "по подразбиране (%@)"; /* No comment provided by engineer. */ @@ -1135,8 +1538,7 @@ "default (yes)" = "по подразбиране (да)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Изтрий"; /* No comment provided by engineer. */ @@ -1211,7 +1613,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Изтрий съобщението?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Изтрий съобщенията"; /* No comment provided by engineer. */ @@ -1248,7 +1650,7 @@ "deleted contact" = "изтрит контакт"; /* rcv group event chat item */ -"deleted group" = "групата изтрита"; +"deleted group" = "групата е изтрита"; /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1358,14 +1760,14 @@ /* No comment provided by engineer. */ "Don't enable" = "Не активирай"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Не показвай отново"; /* No comment provided by engineer. */ "Downgrade and open chat" = "Понижи версията и отвори чата"; /* alert button - chat item action */ +chat item action */ "Download" = "Изтегли"; /* No comment provided by engineer. */ @@ -1404,7 +1806,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. */ @@ -1578,7 +1980,7 @@ /* No comment provided by engineer. */ "Error changing role" = "Грешка при промяна на ролята"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Грешка при промяна на настройката"; /* No comment provided by engineer. */ @@ -1602,19 +2004,19 @@ /* No comment provided by engineer. */ "Error decrypting file" = "Грешка при декриптирането на файла"; -/* No comment provided by engineer. */ -"Error deleting chat database" = "Грешка при изтриване на чат базата данни"; +/* alert title */ +"Error deleting chat database" = "Грешка при изтриване на базата данни"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Грешка при изтриването на чата!"; /* No comment provided by engineer. */ "Error deleting connection" = "Грешка при изтриване на връзката"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Грешка при изтриване на базата данни"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Грешка при изтриване на старата база данни"; /* No comment provided by engineer. */ @@ -1635,11 +2037,11 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Грешка при криптиране на базата данни"; -/* No comment provided by engineer. */ -"Error exporting chat database" = "Грешка при експортиране на чат базата данни"; +/* alert title */ +"Error exporting chat database" = "Грешка при експортиране на базата данни"; -/* No comment provided by engineer. */ -"Error importing chat database" = "Грешка при импортиране на чат базата данни"; +/* alert title */ +"Error importing chat database" = "Грешка при импортиране на базата данни"; /* No comment provided by engineer. */ "Error joining group" = "Грешка при присъединяване към група"; @@ -1650,7 +2052,7 @@ /* alert title */ "Error receiving file" = "Грешка при получаване на файл"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Грешка при отстраняване на член"; /* No comment provided by engineer. */ @@ -1719,7 +2121,9 @@ /* No comment provided by engineer. */ "Error: " = "Грешка: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Грешка: %@"; /* No comment provided by engineer. */ @@ -1731,9 +2135,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" = "Изход без запазване"; @@ -1809,6 +2210,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "Намирайте чатове по-бързо"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; + /* No comment provided by engineer. */ "Fix" = "Поправи"; @@ -1875,7 +2279,7 @@ /* No comment provided by engineer. */ "Group already exists" = "Групата вече съществува"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Групата вече съществува!"; /* No comment provided by engineer. */ @@ -2014,7 +2418,7 @@ "Import" = "Импортиране"; /* No comment provided by engineer. */ -"Import chat database?" = "Импортиране на чат база данни?"; +"Import chat database?" = "Импортиране на база данни?"; /* No comment provided by engineer. */ "Import database" = "Импортиране на база данни"; @@ -2121,7 +2525,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Невалидно име!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Невалиден линк"; /* No comment provided by engineer. */ @@ -2209,24 +2613,18 @@ "Join" = "Присъединяване"; /* No comment provided by engineer. */ -"join as %@" = "присъединяване като %@"; +"Join as %@" = "присъединяване като %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Влез в групата"; /* No comment provided by engineer. */ "Join group conversations" = "Присъединяване към групи"; -/* No comment provided by engineer. */ -"Join group?" = "Влез в групата?"; - /* No comment provided by engineer. */ "Join incognito" = "Влез инкогнито"; -/* No comment provided by engineer. */ -"Join with current profile" = "Присъединяване с текущия профил"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Влез в твоята група?\nТова е вашят линк за група %@!"; /* No comment provided by engineer. */ @@ -2493,7 +2891,7 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Множество профили за чат"; -/* swipe action */ +/* notification label action */ "Mute" = "Без звук"; /* No comment provided by engineer. */ @@ -2514,10 +2912,10 @@ /* No comment provided by engineer. */ "Network settings" = "Мрежови настройки"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "Състояние на мрежата"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "никога"; /* No comment provided by engineer. */ @@ -2620,8 +3018,9 @@ "observer" = "наблюдател"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "изключено"; /* blur media */ @@ -2633,7 +3032,9 @@ /* feature offered item */ "offered %@: %@" = "предлага %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ок"; /* No comment provided by engineer. */ @@ -2699,16 +3100,16 @@ /* 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. */ +/* new chat action */ "Open chat" = "Отвори чат"; /* authentication reason */ "Open chat console" = "Отвори конзолата"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Отвори група"; /* authentication reason */ @@ -2759,9 +3160,6 @@ /* No comment provided by engineer. */ "Password to show" = "Парола за показване"; -/* past/unknown group member */ -"Past member %@" = "Бивш член %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Постави адрес на настолно устройство"; @@ -2798,7 +3196,7 @@ /* 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. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Моля, проверете мрежовата си връзка с %@ и опитайте отново."; /* No comment provided by engineer. */ @@ -2837,9 +3235,6 @@ /* No comment provided by engineer. */ "Polish interface" = "Полски интерфейс"; -/* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Запазете последната чернова на съобщението с прикачени файлове."; @@ -2966,9 +3361,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "получено потвърждение…"; -/* notification */ -"Received file event" = "Събитие за получен файл"; - /* message info title */ "Received message" = "Получено съобщение"; @@ -3005,14 +3397,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Намалена консумация на батерията"; -/* reject incoming call via notification - swipe action */ +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Отхвърляне"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Отхвърляне (подателят НЕ бива уведомен)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Отхвърли заявката за контакт"; /* call status */ @@ -3060,18 +3453,12 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Предоговори криптирането?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Изпрати отново заявката за свързване?"; - /* No comment provided by engineer. */ "Repeat download" = "Повтори изтеглянето"; /* No comment provided by engineer. */ "Repeat import" = "Повтори импортирането"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Изпрати отново заявката за присъединяване?"; - /* No comment provided by engineer. */ "Repeat upload" = "Повтори качването"; @@ -3108,7 +3495,7 @@ /* No comment provided by engineer. */ "Restore database error" = "Грешка при възстановяване на базата данни"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Опитай отново"; /* chat item action */ @@ -3133,7 +3520,7 @@ "Safer groups" = "По-безопасни групи"; /* alert button - chat item action */ +chat item action */ "Save" = "Запази"; /* alert button */ @@ -3259,9 +3646,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "Изпращайте потвърждениe за доставка на"; -/* No comment provided by engineer. */ -"send direct message" = "изпрати лично съобщение"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Изпрати лично съобщение за свързване"; @@ -3325,9 +3709,6 @@ /* copied message info */ "Sent at: %@" = "Изпратено на: %@"; -/* notification */ -"Sent file event" = "Събитие за изпратен файл"; - /* message info title */ "Sent message" = "Изпратено съобщение"; @@ -3335,10 +3716,10 @@ "Sent messages will be deleted after set time." = "Изпратените съобщения ще бъдат изтрити след зададеното време."; /* server test error */ -"Server requires authorization to create queues, check password" = "Сървърът изисква оторизация за създаване на опашки, проверете паролата"; +"Server requires authorization to create queues, check password." = "Сървърът изисква оторизация за създаване на опашки, проверете паролата"; /* server test error */ -"Server requires authorization to upload, check password" = "Сървърът изисква оторизация за качване, проверете паролата"; +"Server requires authorization to upload, check password." = "Сървърът изисква оторизация за качване, проверете паролата"; /* No comment provided by engineer. */ "Server test failed!" = "Тестът на сървъра е неуспешен!"; @@ -3389,7 +3770,7 @@ "Shape profile images" = "Променете формата на профилните изображения"; /* alert action - chat item action */ +chat item action */ "Share" = "Сподели"; /* No comment provided by engineer. */ @@ -3434,6 +3815,9 @@ /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX Адрес"; +/* alert title */ +"SimpleX address settings" = "Автоматично приемане на настройки"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Сигурността на SimpleX Chat беше одитирана от Trail of Bits."; @@ -3650,13 +4034,10 @@ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита."; -/* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Профилът се споделя само с вашите контакти."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Втората отметка, която пропуснахме! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Подателят НЯМА да бъде уведомен"; /* No comment provided by engineer. */ @@ -3701,12 +4082,6 @@ /* No comment provided by engineer. */ "This group no longer exists." = "Тази група вече не съществува."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Това е вашят еднократен линк за връзка!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Това е вашият личен SimpleX адрес!"; - /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Тази настройка се прилага за съобщения в текущия ви профил **%@**."; @@ -3749,12 +4124,6 @@ /* 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. */ "Turkish interface" = "Турски интерфейс"; @@ -3836,7 +4205,7 @@ /* authentication reason */ "Unlock app" = "Отключи приложението"; -/* swipe action */ +/* notification label action */ "Unmute" = "Уведомявай"; /* swipe action */ @@ -3881,7 +4250,7 @@ /* No comment provided by engineer. */ "Use chat" = "Използвай чата"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Използвай текущия профил"; /* No comment provided by engineer. */ @@ -3893,7 +4262,7 @@ /* No comment provided by engineer. */ "Use iOS call interface" = "Използвай интерфейса за повикване на iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Използвай нов инкогнито профил"; /* No comment provided by engineer. */ @@ -4094,33 +4463,24 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Вече сте вече свързани с %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Вече се свързвате с %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Вече се свързвате чрез този еднократен линк за връзка!"; /* No comment provided by engineer. */ "You are already in group %@." = "Вече сте в група %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "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. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Вие вече се присъединявате към групата чрез този линк."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "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." = "Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт."; - -/* No comment provided by engineer. */ -"you are invited to group" = "вие сте поканени в групата"; - /* No comment provided by engineer. */ "You are invited to group" = "Поканени сте в групата"; @@ -4175,7 +4535,7 @@ /* alert message */ "You can view invitation link again in connection details." = "Можете да видите отново линкът за покана в подробностите за връзката."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Не може да изпращате съобщения!"; /* chat item text */ @@ -4196,10 +4556,7 @@ /* 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. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Вече сте направили заявката за връзка!\nИзпрати отново заявката за свързване?"; /* No comment provided by engineer. */ @@ -4256,9 +4613,6 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Ще се свържете с всички членове на групата."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Все още ще получавате обаждания и известия от заглушени профили, когато са активни."; @@ -4281,10 +4635,10 @@ "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" = "Вашите чат профили"; @@ -4299,7 +4653,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" = "Вашият текущ профил"; @@ -4320,10 +4674,10 @@ "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 random profile" = "Вашият автоматично генериран профил"; @@ -4335,8 +4689,5 @@ "Your settings" = "Вашите настройки"; /* No comment provided by engineer. */ -"Your SimpleX address" = "Вашият SimpleX адрес"; - -/* No comment provided by engineer. */ -"Your SMP servers" = "Вашите SMP сървъри"; +"Your SimpleX address" = "Вашият адрес в SimpleX"; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 837e76ebbf..dd486001c7 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (lze kopírovat)"; @@ -16,26 +10,20 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- více stabilní doručování zpráv.\n- o trochu lepší skupiny.\n- a více!"; +/* No comment provided by engineer. */ +"- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- volitelně informuje smazané kontakty.\n- profilová jména s mezerami.\n- a více!"; + /* 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. */ -"." = "."; +"(new)" = "(nový)"; /* 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)"; @@ -46,6 +34,12 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Hvězda na GitHubu](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"**Create 1-time link**: to create and share a new invitation link." = "**Vytvořit jednorázový odkaz**: pro vytvoření a sdílení nové pozvánky."; + +/* No comment provided by engineer. */ +"**Create group**: to create a new group." = "**Vytvořit skupinu**: pro vytvoření nové skupiny."; + /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e šifrovaný** audio hovor"; @@ -64,9 +58,15 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**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." = "**Skenovat / Vložit odkaz**: pro připojení pomocí odkazu který jste obdrželi."; + /* 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."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Varování**: archiv bude odstraněn."; + /* No comment provided by engineer. */ "*bold*" = "\\*tučně*"; @@ -109,6 +109,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ připojen"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ staženo"; + /* notification title */ "%@ is connected!" = "%@ je připojen!"; @@ -118,9 +121,24 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ je ověřený"; +/* No comment provided by engineer. */ +"%@ server" = "%@ server"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ servery"; + +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ nahrán"; + /* notification title */ "%@ wants to connect!" = "%@ se chce připojit!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@, %@ a %lld členů"; + /* No comment provided by engineer. */ "%@, %@ and %lld other members connected" = "%@, %@ a %lld ostatní členové připojeni"; @@ -130,9 +148,24 @@ /* 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"; +/* alert title */ +"%d messages not forwarded" = "%d zprávy nebyly přeposlány"; + /* time interval */ "%d min" = "%d minuty"; @@ -142,6 +175,9 @@ /* time interval */ "%d sec" = "%d sek"; +/* delete after time */ +"%d seconds(s)" = "%d sekund"; + /* integrity error chat item */ "%d skipped message(s)" = "%d přeskočené zprávy"; @@ -163,15 +199,24 @@ /* 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 messages moderated by %@" = "%lld zprávy moderované %@"; + /* 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"; @@ -211,10 +256,14 @@ /* No comment provided by engineer. */ "~strike~" = "\\~stávka~"; +/* time to disappear */ +"0 sec" = "0 sek"; + /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 den"; /* time interval */ @@ -223,12 +272,20 @@ /* 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"; +/* delete after time */ +"1 year" = "1 rok"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Jednorázový odkaz lze použít *pouze s jedním kontaktem* - sdílejte osobně nebo prostřednictvím libovolného komunikátoru."; + /* No comment provided by engineer. */ "5 minutes" = "5 minut"; @@ -262,6 +319,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Přerušit změnu adresy?"; +/* No comment provided by engineer. */ +"About operators" = "O operátorech"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "O SimpleX chat"; @@ -269,26 +329,54 @@ "above, then choose:" = "výše, pak vyberte:"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Přijmout"; +/* alert action */ +"Accept as member" = "Přijmout za člena"; + +/* alert action */ +"Accept as observer" = "Přijmout jako pozorovatele"; + +/* No comment provided by engineer. */ +"Accept conditions" = "Přijmout podmínky"; + /* No comment provided by engineer. */ "Accept connection request?" = "Přijmout kontakt?"; +/* alert title */ +"Accept contact request" = "Přijmout žádost o kontakt"; + /* notification body */ "Accept contact request from %@?" = "Přijmout žádost o kontakt od %@?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Přijmout inkognito"; +/* alert title */ +"Accept member" = "Přijmout člena"; + /* call status */ "accepted call" = "přijatý hovor"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Přijaté podmínky"; + +/* token status text */ +"Active" = "Aktivní"; + +/* No comment provided by engineer. */ +"Active connections" = "Aktivní spojení"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům."; +/* No comment provided by engineer. */ +"Add friends" = "Přidat přátele"; + /* No comment provided by engineer. */ "Add profile" = "Přidat profil"; @@ -298,48 +386,96 @@ /* 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 team members" = "Přidat členy týmu"; + /* No comment provided by engineer. */ "Add to another device" = "Přidat do jiného zařízení"; +/* No comment provided by engineer. */ +"Add to list" = "Přidat do seznamu"; + /* 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." = "Přidat členy týmu do konverzace."; + +/* No comment provided by engineer. */ +"Added message servers" = "Přidané servery zpráv"; + +/* No comment provided by engineer. */ +"Additional accent" = "Další zbarvení"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Další zbarvení 2"; + /* No comment provided by engineer. */ "Address" = "Adresa"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "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?" = "Adresa nebo jednorázový odkaz?"; + +/* No comment provided by engineer. */ +"Address settings" = "Nastavení adresy"; + /* member role */ "admin" = "správce"; +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Správci mohou blokovat člena pro všechny."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Správci mohou vytvářet odkazy pro připojení ke skupinám."; /* No comment provided by engineer. */ "Advanced network settings" = "Pokročilá nastavení sítě"; +/* No comment provided by engineer. */ +"Advanced settings" = "Pokročilá nastavení"; + /* chat item text */ "agreeing encryption for %@…" = "povoluji šifrování pro %@…"; /* chat item text */ "agreeing encryption…" = "povoluji šifrování…"; +/* No comment provided by engineer. */ +"All" = "Vše"; + /* No comment provided by engineer. */ "All app data is deleted." = "Všechna data aplikace jsou smazána."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Všechny chaty a zprávy budou smazány – tuto akci nelze vrátit zpět!"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Všechny chaty budou odstraněny ze seznamu %@ a seznam bude odstraněn."; + /* No comment provided by engineer. */ "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." = "Všechna data jsou uchována ve vašem zařízení."; + /* 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 will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Všechny zprávy budou smazány – tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás."; +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "Všechny nové zprávy od %@ budou skryté!"; + +/* profile dropdown */ +"All profiles" = "Všechny profily"; + +/* No comment provided by engineer. */ +"All servers" = "Všechny servery"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Všechny vaše kontakty zůstanou připojeny."; @@ -352,6 +488,9 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Povolte hovory, pouze pokud je váš kontakt povolí."; +/* No comment provided by engineer. */ +"Allow calls?" = "Povolit volání?"; + /* 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í."; @@ -370,12 +509,21 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Povolit odesílání mizících zpráv."; +/* No comment provided by engineer. */ +"Allow sharing" = "Povolit sdílení"; + /* 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." = "Povolit nahlášení zpráv moderátorům."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Povolit odesílání souborů a médii."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Povolit odesílání SimpleX odkazů."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Povolit odesílání hlasových zpráv."; @@ -397,21 +545,36 @@ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Umožněte svým kontaktům odesílat mizící zprávy."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Povolit vašim kontaktům odesílání souborů a médii."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Povolte svým kontaktům odesílání hlasových zpráv."; /* No comment provided by engineer. */ "Already connected?" = "Již připojeno?"; +/* new chat sheet title */ +"Already connecting!" = "Již připojováno!"; + +/* new chat sheet title */ +"Already joining the group!" = "Již se ke skupině připojujete!"; + /* pref value */ "always" = "vždy"; +/* No comment provided by engineer. */ +"Always use private routing." = "Vždy používat soukromé směrování."; + /* No comment provided by engineer. */ "Always use relay" = "Spojení přes relé"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle."; +/* report reason */ +"Another reason" = "Jiný důvod"; + /* No comment provided by engineer. */ "Answer call" = "Přijmout hovor"; @@ -421,6 +584,9 @@ /* No comment provided by engineer. */ "App build: %@" = "Sestavení aplikace: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Přenos dat aplikace"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Aplikace šifruje nové místní soubory (s výjimkou videí)."; @@ -442,6 +608,21 @@ /* No comment provided by engineer. */ "Appearance" = "Vzhled"; +/* No comment provided by engineer. */ +"Apply" = "Použít"; + +/* No comment provided by engineer. */ +"Archive" = "Archiv"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archivovat všechny hlášení?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archivovat a nahrát"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Archivované kontakty"; + /* No comment provided by engineer. */ "Attach" = "Připojit"; @@ -484,6 +665,9 @@ /* No comment provided by engineer. */ "Back" = "Zpět"; +/* No comment provided by engineer. */ +"Background" = "Pozadí"; + /* integrity error chat item */ "bad message hash" = "špatný hash zprávy"; @@ -496,9 +680,45 @@ /* No comment provided by engineer. */ "Bad message ID" = "Špatné ID zprávy"; +/* No comment provided by engineer. */ +"Better calls" = "Lepší volání"; + +/* No comment provided by engineer. */ +"Better groups" = "Lepší skupiny"; + +/* No comment provided by engineer. */ +"Better groups performance" = "Lepší výkon skupin"; + +/* No comment provided by engineer. */ +"Better message dates." = "Lepší datumy zpráv."; + /* No comment provided by engineer. */ "Better messages" = "Lepší zprávy"; +/* No comment provided by engineer. */ +"Better networking" = "Lepší sítě"; + +/* No comment provided by engineer. */ +"Better notifications" = "Lepší upozornění"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Lepší soukromí a zabezpečení"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Lepší zabezpečení ✅"; + +/* No comment provided by engineer. */ +"Block member" = "Blokovat člena"; + +/* No comment provided by engineer. */ +"Block member?" = "Blokovat člena?"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Blokován správcem"; + +/* No comment provided by engineer. */ +"Blur media" = "Rozmazat média"; + /* No comment provided by engineer. */ "bold" = "tučně"; @@ -514,12 +734,18 @@ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Vy i váš kontakt můžete posílat mizící zprávy."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Vy i vaše kontakty můžete posílat soubory a média."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Hlasové zprávy můžete posílat vy i váš kontakt."; /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulharský, finský, thajský a ukrajinský - díky uživatelům a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Obchodní adresa"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -538,28 +764,65 @@ /* No comment provided by engineer. */ "Calls" = "Hovory"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Volání zakázáno!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Kamera není k dispozici"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Kontaktu nelze volat"; + +/* No comment provided by engineer. */ +"Can't call member" = "Členu nelze volat"; + +/* alert title */ +"Can't change profile" = "Nelze změnit profil"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Nelze pozvat kontakt!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Nelze pozvat kontakty!"; +/* No comment provided by engineer. */ +"Can't message member" = "Členu nelze poslat zprávu"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Zrušit"; +/* No comment provided by engineer. */ +"Cancel migration" = "Zrušit přesun"; + /* feature offered item */ "cancelled %@" = "zrušeno %@"; /* 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. */ +"Cannot forward message" = "Zprávu nelze přeposlat"; + /* alert title */ "Cannot receive file" = "Nelze přijmout soubor"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Kapacita překročena - příjemce neobdrží dříve poslané zprávy."; + +/* No comment provided by engineer. */ +"Cellular" = "Mobilní"; + /* No comment provided by engineer. */ "Change" = "Změnit"; +/* alert title */ +"Change automatic message deletion?" = "Změnit automatické mazání zpráv?"; + +/* authentication reason */ +"Change chat profiles" = "Změnit chat profily"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Změnit přístupovou frázi databáze?"; @@ -585,7 +848,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 */ @@ -603,6 +866,9 @@ /* chat item text */ "changing address…" = "změna adresy…"; +/* No comment provided by engineer. */ +"Chat colors" = "Barvy chatu"; + /* No comment provided by engineer. */ "Chat console" = "Konzola pro chat"; @@ -612,6 +878,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Databáze chatu odstraněna"; +/* No comment provided by engineer. */ +"Chat database exported" = "Chat databáze exportována"; + /* No comment provided by engineer. */ "Chat database imported" = "Importovaná databáze chatu"; @@ -621,6 +890,9 @@ /* No comment provided by engineer. */ "Chat is stopped" = "Chat je zastaven"; +/* No comment provided by engineer. */ +"Chat migrated!" = "Chat přesunut!"; + /* No comment provided by engineer. */ "Chat preferences" = "Předvolby chatu"; @@ -684,27 +956,30 @@ /* No comment provided by engineer. */ "Confirm password" = "Potvrdit heslo"; +/* No comment provided by engineer. */ +"Confirm upload" = "Potvrdit nahrání"; + /* server test step */ "Connect" = "Připojit"; /* No comment provided by engineer. */ -"Connect incognito" = "Spojit se inkognito"; +"Connect automatically" = "Připojit automaticky"; /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "připojit se k vývojářům SimpleX Chat."; -/* No comment provided by engineer. */ +/* new chat sheet title */ +"Connect to yourself?\nThis is your own one-time link!" = "Připojit se k sobě?\nToto je váš vlastní jednorázový odkaz!"; + +/* new chat sheet title */ "Connect via link" = "Připojte se prostřednictvím odkazu"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Připojit se jednorázovým odkazem"; /* No comment provided by engineer. */ "connected" = "připojeno"; -/* rcv group event chat item */ -"connected directly" = "připojeno přímo"; - /* No comment provided by engineer. */ "connecting" = "připojování"; @@ -735,7 +1010,7 @@ /* No comment provided by engineer. */ "Connection" = "Připojení"; -/* No comment provided by engineer. */ +/* alert title */ "Connection error" = "Chyba připojení"; /* No comment provided by engineer. */ @@ -747,7 +1022,7 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Požadavek na připojení byl odeslán!"; -/* No comment provided by engineer. */ +/* alert title */ "Connection timeout" = "Časový limit připojení"; /* connection information */ @@ -807,12 +1082,12 @@ /* 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"; -/* No comment provided by engineer. */ -"Create secret group" = "Vytvořit tajnou skupinu"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Vytvořit SimpleX adresu"; @@ -906,7 +1181,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. */ @@ -916,8 +1192,7 @@ "default (yes)" = "výchozí (ano)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Smazat"; /* No comment provided by engineer. */ @@ -983,7 +1258,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. */ @@ -1109,7 +1384,7 @@ /* No comment provided by engineer. */ "Don't enable" = "Nepovolovat"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Znovu neukazuj"; /* No comment provided by engineer. */ @@ -1142,7 +1417,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. */ @@ -1286,7 +1561,7 @@ /* No comment provided by engineer. */ "Error changing role" = "Chyba při změně role"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Chyba změny nastavení"; /* No comment provided by engineer. */ @@ -1307,19 +1582,19 @@ /* No comment provided by engineer. */ "Error decrypting file" = "Chyba dešifrování souboru"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Chyba při mazání databáze chatu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Chyba při mazání chatu!"; /* No comment provided by engineer. */ "Error deleting connection" = "Chyba při mazání připojení"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Chyba při mazání databáze"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Chyba při mazání staré databáze"; /* No comment provided by engineer. */ @@ -1337,10 +1612,10 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Chyba šifrování databáze"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Chyba při exportu databáze chatu"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Chyba při importu databáze chatu"; /* No comment provided by engineer. */ @@ -1349,7 +1624,7 @@ /* alert title */ "Error receiving file" = "Chyba při příjmu souboru"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Chyba při odebrání člena"; /* No comment provided by engineer. */ @@ -1406,7 +1681,9 @@ /* No comment provided by engineer. */ "Error: " = "Chyba: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Chyba: %@"; /* No comment provided by engineer. */ @@ -1475,6 +1752,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "Najděte chaty rychleji"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Je možné, že otisk certifikátu v adrese serveru je nesprávný"; + /* No comment provided by engineer. */ "Fix" = "Opravit"; @@ -1803,9 +2083,9 @@ "Join" = "Připojte se na"; /* No comment provided by engineer. */ -"join as %@" = "připojit se jako %@"; +"Join as %@" = "připojit se jako %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Připojit ke skupině"; /* No comment provided by engineer. */ @@ -2012,7 +2292,7 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Více chatovacích profilů"; -/* swipe action */ +/* notification label action */ "Mute" = "Ztlumit"; /* No comment provided by engineer. */ @@ -2027,10 +2307,10 @@ /* No comment provided by engineer. */ "Network settings" = "Nastavení sítě"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "Stav sítě"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "nikdy"; /* notification */ @@ -2124,8 +2404,9 @@ "observer" = "pozorovatel"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "vypnuto"; /* blur media */ @@ -2137,7 +2418,9 @@ /* feature offered item */ "offered %@: %@" = "nabídl %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2200,10 +2483,10 @@ /* 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. */ +/* new chat action */ "Open chat" = "Otevřete chat"; /* authentication reason */ @@ -2257,7 +2540,7 @@ /* 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ý."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Zkontrolujte síťové připojení pomocí %@ a zkuste to znovu."; /* No comment provided by engineer. */ @@ -2290,9 +2573,6 @@ /* No comment provided by engineer. */ "Polish interface" = "Polské rozhraní"; -/* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Je možné, že otisk certifikátu v adrese serveru je nesprávný"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachování posledního návrhu zprávy s přílohami."; @@ -2398,9 +2678,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "obdržel potvrzení…"; -/* notification */ -"Received file event" = "Událost přijatého souboru"; - /* message info title */ "Received message" = "Přijatá zpráva"; @@ -2431,14 +2708,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Snížení spotřeby baterie"; -/* reject incoming call via notification - swipe action */ +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Odmítnout"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Odmítnout kontakt (odesílatel NEBUDE upozorněn)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Odmítnout žádost o kontakt"; /* call status */ @@ -2532,7 +2810,7 @@ "Run chat" = "Spustit chat"; /* alert button - chat item action */ +chat item action */ "Save" = "Uložit"; /* alert button */ @@ -2634,9 +2912,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "Potvrzení o doručení zasílat na"; -/* No comment provided by engineer. */ -"send direct message" = "odeslat přímou zprávu"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Odeslat přímou zprávu pro připojení"; @@ -2697,9 +2972,6 @@ /* copied message info */ "Sent at: %@" = "Posláno v: % @"; -/* notification */ -"Sent file event" = "Odeslaná událost souboru"; - /* message info title */ "Sent message" = "Poslaná zpráva"; @@ -2707,10 +2979,10 @@ "Sent messages will be deleted after set time." = "Odeslané zprávy se po uplynutí nastavené doby odstraní."; /* server test error */ -"Server requires authorization to create queues, check password" = "Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo"; +"Server requires authorization to create queues, check password." = "Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo"; /* server test error */ -"Server requires authorization to upload, check password" = "Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo"; +"Server requires authorization to upload, check password." = "Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo"; /* No comment provided by engineer. */ "Server test failed!" = "Test serveru se nezdařil!"; @@ -2746,7 +3018,7 @@ "Settings" = "Nastavení"; /* alert action - chat item action */ +chat item action */ "Share" = "Sdílet"; /* No comment provided by engineer. */ @@ -2968,13 +3240,10 @@ /* 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."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Druhé zaškrtnutí jsme přehlédli! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Odesílatel NEBUDE informován"; /* No comment provided by engineer. */ @@ -3043,12 +3312,6 @@ /* No comment provided by engineer. */ "Transport isolation" = "Izolace transportu"; -/* 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: %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu."; - /* No comment provided by engineer. */ "Turn off" = "Vypnout"; @@ -3100,7 +3363,7 @@ /* authentication reason */ "Unlock app" = "Odemknout aplikaci"; -/* swipe action */ +/* notification label action */ "Unmute" = "Zrušit ztlumení"; /* swipe action */ @@ -3133,7 +3396,7 @@ /* No comment provided by engineer. */ "Use chat" = "Použijte chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Použít aktuální profil"; /* No comment provided by engineer. */ @@ -3142,7 +3405,7 @@ /* No comment provided by engineer. */ "Use iOS call interface" = "Použít rozhraní volání iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Použít nový inkognito profil"; /* No comment provided by engineer. */ @@ -3277,12 +3540,6 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Již jste připojeni k %@."; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu."; - -/* No comment provided by engineer. */ -"you are invited to group" = "jste pozváni do skupiny"; - /* No comment provided by engineer. */ "You are invited to group" = "Jste pozváni do skupiny"; @@ -3325,7 +3582,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "K formátování zpráv můžete použít markdown:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Nemůžete posílat zprávy!"; /* chat item text */ @@ -3452,10 +3709,10 @@ "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. 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 random profile" = "Váš náhodný profil"; @@ -3469,6 +3726,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"; - diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index cad89ed29a..caf58399de 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (kann kopiert werden)"; @@ -22,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)"; @@ -199,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)"; @@ -241,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"; @@ -289,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"; @@ -298,11 +278,16 @@ /* 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"; @@ -344,7 +329,7 @@ "Abort changing address?" = "Wechsel der Empfängeradresse beenden?"; /* No comment provided by engineer. */ -"About operators" = "Über Betreiber"; +"About operators" = "Über die Betreiber"; /* No comment provided by engineer. */ "About SimpleX Chat" = "Über SimpleX Chat"; @@ -356,23 +341,39 @@ "Accent" = "Akzent"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Annehmen"; +/* alert action */ +"Accept as member" = "Als Mitglied übernehmen"; + +/* alert action */ +"Accept as observer" = "Als Beobachter übernehmen"; + /* No comment provided by engineer. */ "Accept conditions" = "Nutzungsbedingungen akzeptieren"; /* No comment provided by engineer. */ "Accept connection request?" = "Kontaktanfrage annehmen?"; +/* alert title */ +"Accept contact request" = "Kontaktanfrage annehmen"; + /* notification body */ "Accept contact request from %@?" = "Die Kontaktanfrage von %@ annehmen?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Inkognito akzeptieren"; +/* alert title */ +"Accept member" = "Mitglied annehmen"; + +/* rcv group event chat item */ +"accepted %@" = "%@ angenommen"; + /* call status */ "accepted call" = "Anruf angenommen"; @@ -380,7 +381,10 @@ "Accepted conditions" = "Akzeptierte Nutzungsbedingungen"; /* chat list item title */ -"accepted invitation" = "Einladung akzeptiert"; +"accepted invitation" = "Einladung angenommen"; + +/* rcv group event chat item */ +"accepted you" = "hat Sie angenommen"; /* No comment provided by engineer. */ "Acknowledged" = "Bestätigt"; @@ -388,6 +392,9 @@ /* 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"; @@ -397,14 +404,20 @@ /* No comment provided by engineer. */ "Add friends" = "Freunde aufnehmen"; +/* No comment provided by engineer. */ +"Add list" = "Liste hinzufügen"; + +/* placeholder for sending contact request */ +"Add message" = "Nachricht 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"; @@ -412,6 +425,9 @@ /* 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"; @@ -469,12 +485,21 @@ /* chat item text */ "agreeing encryption…" = "Verschlüsselung zustimmen…"; +/* member criteria value */ +"all" = "alle"; + +/* 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!" = "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."; @@ -502,6 +527,12 @@ /* 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."; @@ -526,6 +557,9 @@ /* No comment provided by engineer. */ "Allow downgrade" = "Herabstufung erlauben"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Erlauben Sie Dateien und Medien nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; + /* 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)"; @@ -547,6 +581,9 @@ /* 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."; @@ -560,7 +597,7 @@ "Allow voice messages only if your contact allows them." = "Erlauben Sie Sprachnachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; /* No comment provided by engineer. */ -"Allow voice messages?" = "Sprachnachricht erlauben?"; +"Allow voice messages?" = "Sprachnachrichten erlauben?"; /* No comment provided by engineer. */ "Allow your contacts adding message reactions." = "Erlauben Sie Ihren Kontakten Reaktionen auf Nachrichten zu geben."; @@ -574,16 +611,19 @@ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Erlauben Sie Ihren Kontakten Dateien und Medien zu senden."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Erlauben Sie Ihren Kontakten Sprachnachrichten zu senden."; /* No comment provided by engineer. */ "Already connected?" = "Sind Sie bereits verbunden?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Bereits verbunden!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Sie sind bereits Mitglied der Gruppe!"; /* pref value */ @@ -601,6 +641,9 @@ /* 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"; @@ -616,6 +659,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"; @@ -643,15 +689,36 @@ /* 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"; @@ -700,9 +767,6 @@ /* 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"; @@ -730,6 +794,9 @@ /* 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"; @@ -742,12 +809,21 @@ /* 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. */ +"Bio" = "Biografie"; + +/* alert title */ +"Bio too large" = "Biografie zu lang"; + /* No comment provided by engineer. */ "Black" = "Schwarz"; @@ -773,9 +849,10 @@ "blocked" = "Blockiert"; /* rcv group event chat item */ -"blocked %@" = "%@ wurde blockiert"; +"blocked %@" = "hat %@ 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. */ @@ -785,11 +862,14 @@ "Blur for better privacy." = "Für bessere Privatsphäre verpixeln."; /* No comment provided by engineer. */ -"Blur media" = "Medium unscharf machen"; +"Blur media" = "Medien verpixeln"; /* No comment provided by engineer. */ "bold" = "fett"; +/* No comment provided by engineer. */ +"Bot" = "Bot"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben."; @@ -802,6 +882,9 @@ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Ihr Kontakt und Sie können beide verschwindende Nachrichten senden."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Sowohl Sie, als auch Ihr Kontakt können Dateien und Medien senden."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden."; @@ -814,9 +897,18 @@ /* No comment provided by engineer. */ "Business chats" = "Geschäftliche Chats"; +/* No comment provided by engineer. */ +"Business connection" = "Geschäftliche Verbindung"; + +/* 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"; @@ -847,6 +939,9 @@ /* No comment provided by engineer. */ "Can't call member" = "Mitglied kann nicht angerufen werden"; +/* alert title */ +"Can't change profile" = "Änderung des Profils nicht möglich"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Kontakt kann nicht eingeladen werden!"; @@ -856,8 +951,12 @@ /* No comment provided by engineer. */ "Can't message member" = "Mitglied kann nicht benachrichtigt werden"; +/* No comment provided by engineer. */ +"can't send messages" = "Es können keine Nachrichten gesendet werden"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Abbrechen"; /* No comment provided by engineer. */ @@ -884,6 +983,9 @@ /* 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"; @@ -912,7 +1014,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 */ @@ -936,7 +1038,7 @@ /* No comment provided by engineer. */ "Chat already exists" = "Chat besteht bereits"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Chat besteht bereits!"; /* No comment provided by engineer. */ @@ -990,9 +1092,21 @@ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; +/* chat toolbar */ +"Chat with admins" = "Chat mit Administratoren"; + +/* No comment provided by engineer. */ +"Chat with member" = "Chat mit einem Mitglied"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Mit Mitgliedern chatten bevor sie beitreten."; + /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Chats with members" = "Chats mit Mitgliedern"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Alle 20min Nachrichten überprüfen."; @@ -1032,6 +1146,12 @@ /* 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?"; @@ -1047,6 +1167,9 @@ /* No comment provided by engineer. */ "colored" = "farbig"; +/* report reason */ +"Community guidelines violation" = "Verstoß gegen die Gemeinschaftsrichtlinien"; + /* server test step */ "Compare file" = "Datei vergleichen"; @@ -1068,15 +1191,9 @@ /* 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. */ +/* alert button */ "Conditions of use" = "Nutzungsbedingungen"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for operator(s): **%@**." = "Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**."; - /* No comment provided by engineer. */ "Conditions will be accepted for the operator(s): **%@**." = "Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**."; @@ -1089,6 +1206,9 @@ /* 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"; @@ -1119,6 +1239,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Hochladen bestätigen"; +/* token status text */ +"Confirmed" = "Bestätigt"; + /* server test step */ "Connect" = "Verbinden"; @@ -1126,7 +1249,7 @@ "Connect automatically" = "Automatisch verbinden"; /* No comment provided by engineer. */ -"Connect incognito" = "Inkognito verbinden"; +"Connect faster! 🚀" = "Schneller miteinander verbinden! 🚀"; /* No comment provided by engineer. */ "Connect to desktop" = "Mit dem Desktop verbinden"; @@ -1137,25 +1260,22 @@ /* 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?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Mit Ihnen selbst verbinden?\nDas ist Ihr eigener Einmal-Link!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Sich mit Ihnen selbst verbinden?\nDas ist Ihre eigene SimpleX-Adresse!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Über die Kontakt-Adresse verbinden"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Über einen Link verbinden"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Über einen Einmal-Link verbinden"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Mit %@ verbinden"; /* No comment provided by engineer. */ @@ -1167,9 +1287,6 @@ /* 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"; @@ -1219,6 +1336,9 @@ "Connection and servers status." = "Verbindungs- und Server-Status."; /* No comment provided by engineer. */ +"Connection blocked" = "Verbindung blockiert"; + +/* alert title */ "Connection error" = "Verbindungsfehler"; /* No comment provided by engineer. */ @@ -1227,19 +1347,28 @@ /* 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 Serverbetreiber 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. */ +/* alert title */ "Connection timeout" = "Verbindungszeitüberschreitung"; /* No comment provided by engineer. */ @@ -1260,9 +1389,15 @@ /* 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 deleted!" = "Kontakt gelöscht!"; +/* No comment provided by engineer. */ +"contact disabled" = "Kontakt deaktiviert"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "Kontakt nutzt E2E-Verschlüsselung"; @@ -1281,9 +1416,18 @@ /* No comment provided by engineer. */ "Contact name" = "Kontaktname"; +/* No comment provided by engineer. */ +"contact not ready" = "Kontakt nicht bereit"; + /* No comment provided by engineer. */ "Contact preferences" = "Kontakt-Präferenzen"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "KONTAKTANFRAGEN VON GRUPPEN"; + +/* No comment provided by engineer. */ +"contact should accept…" = "Kontakt sollte annehmen…"; + /* 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!"; @@ -1293,6 +1437,9 @@ /* 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"; @@ -1335,6 +1482,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/). 💻"; @@ -1345,10 +1495,10 @@ "Create queue" = "Erzeuge Warteschlange"; /* No comment provided by engineer. */ -"Create secret group" = "Geheime Gruppe erstellen"; +"Create SimpleX address" = "SimpleX-Adresse erstellen"; /* No comment provided by engineer. */ -"Create SimpleX address" = "SimpleX-Adresse erstellen"; +"Create your address" = "Ihre Adresse erstellen"; /* No comment provided by engineer. */ "Create your profile" = "Erstellen Sie Ihr Profil"; @@ -1381,7 +1531,7 @@ "Current passphrase…" = "Aktuelles Passwort…"; /* No comment provided by engineer. */ -"Current profile" = "Aktueller Profil"; +"Current profile" = "Aktuelles Profil"; /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Die derzeit maximal unterstützte Dateigröße beträgt %@."; @@ -1476,8 +1626,9 @@ /* No comment provided by engineer. */ "decryption errors" = "Entschlüsselungs-Fehler"; -/* pref value */ -"default (%@)" = "Voreinstellung (%@)"; +/* delete after time +pref value */ +"default (%@)" = "Default (%@)"; /* No comment provided by engineer. */ "default (no)" = "Voreinstellung (Nein)"; @@ -1486,8 +1637,7 @@ "default (yes)" = "Voreinstellung (Ja)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Löschen"; /* No comment provided by engineer. */ @@ -1514,12 +1664,18 @@ /* No comment provided by engineer. */ "Delete chat" = "Chat löschen"; +/* No comment provided by engineer. */ +"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"; /* No comment provided by engineer. */ "Delete chat profile?" = "Chat-Profil löschen?"; +/* alert title */ +"Delete chat with member?" = "Chat mit dem Mitglied löschen?"; + /* No comment provided by engineer. */ "Delete chat?" = "Chat löschen?"; @@ -1568,17 +1724,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"; @@ -1598,6 +1757,9 @@ /* 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."; @@ -1640,9 +1802,15 @@ /* No comment provided by engineer. */ "Delivery receipts!" = "Empfangsbestätigungen!"; +/* No comment provided by engineer. */ +"Deprecated options" = "Veraltete Optionen"; + /* No comment provided by engineer. */ "Description" = "Beschreibung"; +/* alert title */ +"Description too large" = "Beschreibung zu lang"; + /* No comment provided by engineer. */ "Desktop address" = "Desktop-Adresse"; @@ -1706,6 +1874,12 @@ /* 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"; @@ -1766,6 +1940,9 @@ /* 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"; @@ -1773,13 +1950,19 @@ "Don't enable" = "Nicht aktivieren"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Verpassen Sie keine wichtigen Nachrichten."; + +/* alert action */ "Don't show again" = "Nicht nochmals anzeigen"; +/* No comment provided by engineer. */ +"Done" = "Fertig"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Datenbank herabstufen und den Chat öffnen"; /* alert button - chat item action */ +chat item action */ "Download" = "Herunterladen"; /* No comment provided by engineer. */ @@ -1830,20 +2013,26 @@ /* No comment provided by engineer. */ "Edit group profile" = "Gruppenprofil bearbeiten"; +/* No comment provided by engineer. */ +"Empty message!" = "Leere Nachricht!"; + /* No comment provided by engineer. */ "Enable" = "Aktivieren"; /* 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" = "Flux aktivieren"; +"Enable disappearing messages by default." = "Verschwindende Nachrichten sind per Voreinstellung aktiviert."; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Für einen besseren Metadatenschutz Flux in den Netzwerk- und Servereinstellungen aktivieren."; /* No comment provided by engineer. */ "Enable for all" = "Für Alle aktivieren"; @@ -1956,6 +2145,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"; @@ -2010,30 +2202,45 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Fehler beim Annehmen der Kontaktanfrage"; +/* alert title */ +"Error accepting member" = "Fehler beim Annehmen des Mitglieds"; + /* 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 adding short link" = "Fehler beim Hinzufügen eines verkürzten Links"; + /* No comment provided by engineer. */ "Error changing address" = "Fehler beim Wechseln der Empfängeradresse"; +/* alert title */ +"Error changing chat profile" = "Fehler beim Wechseln des Chat-Profils"; + /* 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. */ +/* alert title */ "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"; + +/* alert message */ "Error connecting to forwarding server %@. Please try later." = "Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut."; +/* subscription status explanation */ +"Error connecting to the server used to receive messages from this connection: %@" = "Fehler beim Herstellen der Verbindung zum Server, der für den Empfang von Nachrichten dieser Verbindung genutzt wird: %@"; + /* No comment provided by engineer. */ "Error creating address" = "Fehler beim Erstellen der Adresse"; @@ -2043,6 +2250,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"; @@ -2052,22 +2262,28 @@ /* 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"; -/* No comment provided by engineer. */ +/* alert title */ +"Error deleting chat" = "Fehler beim Löschen des Chats mit dem Mitglied"; + +/* alert title */ "Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Fehler beim Löschen des Chats!"; /* No comment provided by engineer. */ "Error deleting connection" = "Fehler beim Löschen der Verbindung"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Fehler beim Löschen der Datenbank"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Fehler beim Löschen der alten Datenbank"; /* No comment provided by engineer. */ @@ -2088,13 +2304,13 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Fehler beim Verschlüsseln der Datenbank"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Fehler beim Importieren der Chat-Datenbank"; /* No comment provided by engineer. */ @@ -2107,10 +2323,13 @@ "Error migrating settings" = "Fehler beim Migrieren der Einstellungen"; /* No comment provided by engineer. */ -"Error opening chat" = "Fehler beim Öffnen des Chats"; +"Error opening chat" = "Fehler beim Öffnen des Chat"; + +/* No comment provided by engineer. */ +"Error opening group" = "Fehler beim Vorbereiten der Gruppe"; /* alert title */ -"Error receiving file" = "Fehler beim Empfangen der Datei"; +"Error receiving file" = "Fehler beim Herunterladen der Datei"; /* No comment provided by engineer. */ "Error reconnecting server" = "Fehler beim Wiederherstellen der Verbindung zum Server"; @@ -2118,12 +2337,24 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Fehler beim Wiederherstellen der Verbindungen zu den Servern"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "Fehler beim Registrieren für Benachrichtigungen"; + +/* alert title */ +"Error rejecting contact request" = "Fehler bei der Ablehnung der Kontaktanfrage"; + +/* alert title */ "Error removing member" = "Fehler beim Entfernen des Mitglieds"; +/* alert title */ +"Error reordering lists" = "Fehler beim Umsortieren der Listen"; + /* No comment provided by engineer. */ "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"; @@ -2157,6 +2388,9 @@ /* No comment provided by engineer. */ "Error sending message" = "Fehler beim Senden der Nachricht"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Fehler bei der Einstellung des automatischen Akzeptierens"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Fehler beim Setzen von Empfangsbestätigungen!"; @@ -2166,7 +2400,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Fehler beim Beenden des Chats"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Fehler beim Wechseln des Profils"; /* alertTitle */ @@ -2175,6 +2409,9 @@ /* 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"; @@ -2199,9 +2436,14 @@ /* No comment provided by engineer. */ "Error: " = "Fehler: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Fehler: %@"; +/* server test error */ +"Error: %@." = "Fehler: %@."; + /* No comment provided by engineer. */ "Error: no database file" = "Fehler: Keine Datenbankdatei"; @@ -2217,9 +2459,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Auch wenn sie im Chat deaktiviert sind."; -/* No comment provided by engineer. */ -"event happened" = "event happened"; - /* No comment provided by engineer. */ "Exit without saving" = "Beenden ohne Speichern"; @@ -2229,6 +2468,9 @@ /* No comment provided by engineer. */ "expired" = "Abgelaufen"; +/* token status text */ +"Expired" = "Abgelaufen"; + /* No comment provided by engineer. */ "Export database" = "Datenbank exportieren"; @@ -2253,18 +2495,30 @@ /* 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%@." = "Die Datei wurde vom Serverbetreiber 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."; @@ -2281,10 +2535,10 @@ "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: %@"; @@ -2298,6 +2552,9 @@ /* chat feature */ "Files and media" = "Dateien und Medien"; +/* No comment provided by engineer. */ +"Files and media are prohibited in this chat." = "In diesem Chat sind Dateien und Medien nicht erlaubt."; + /* No comment provided by engineer. */ "Files and media are prohibited." = "In dieser Gruppe sind Dateien und Medien nicht erlaubt."; @@ -2322,6 +2579,18 @@ /* No comment provided by engineer. */ "Find chats faster" = "Chats schneller finden"; +/* No comment provided by engineer. */ +"Fingerprint in destination server address does not match certificate: %@." = "Fingerabdruck in der Zielserveradresse stimmt nicht mit dem Zertifikat überein: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in forwarding server address does not match certificate: %@." = "Fingerabdruck in der Weiterleitungsserveradresse stimmt nicht mit dem Zertifikat überein: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in server address does not match certificate: %@." = "Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein: %@."; + +/* server test error */ +"Fingerprint in server address does not match certificate." = "Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein."; + /* No comment provided by engineer. */ "Fix" = "Reparieren"; @@ -2341,7 +2610,7 @@ "Fix not supported by group member" = "Reparatur wird vom Gruppenmitglied nicht unterstützt"; /* No comment provided by engineer. */ -"for better metadata privacy." = "für einen besseren Metadatenschutz."; +"For all moderators" = "Für alle Moderatoren"; /* servers error */ "For chat profile %@:" = "Für das Chat-Profil %@:"; @@ -2352,6 +2621,9 @@ /* 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"; @@ -2388,8 +2660,8 @@ /* 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."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Weiterleitungsserver %1$@ konnte sich nicht mit dem Zielserver %2$@ 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: %@."; @@ -2424,6 +2696,9 @@ /* 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"; @@ -2433,13 +2708,16 @@ /* message preview */ "Good morning!" = "Guten Morgen!"; +/* shown on group welcome message */ +"group" = "Gruppe"; + /* No comment provided by engineer. */ "Group" = "Gruppe"; /* No comment provided by engineer. */ "Group already exists" = "Die Gruppe besteht bereits"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Die Gruppe besteht bereits!"; /* No comment provided by engineer. */ @@ -2463,6 +2741,9 @@ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde."; +/* No comment provided by engineer. */ +"group is deleted" = "Gruppe wurde gelöscht"; + /* No comment provided by engineer. */ "Group link" = "Gruppen-Link"; @@ -2487,6 +2768,9 @@ /* snd group event chat item */ "group profile updated" = "Gruppenprofil aktualisiert"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "Das Gruppenprofil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an die Gruppenmitglieder gesendet."; + /* No comment provided by engineer. */ "Group welcome message" = "Gruppen-Begrüßungsmeldung"; @@ -2496,9 +2780,15 @@ /* No comment provided by engineer. */ "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"; @@ -2535,6 +2825,9 @@ /* 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. */ "How SimpleX works" = "Wie SimpleX funktioniert"; @@ -2569,10 +2862,10 @@ "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"; @@ -2622,6 +2915,12 @@ /* 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"; @@ -2688,6 +2987,21 @@ /* No comment provided by engineer. */ "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"; @@ -2703,7 +3017,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Ungültiger Anzeigename!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Ungültiger Link"; /* No comment provided by engineer. */ @@ -2803,24 +3117,18 @@ "Join" = "Beitreten"; /* No comment provided by engineer. */ -"join as %@" = "beitreten als %@"; +"Join as %@" = "Als %@ beitreten"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Treten Sie der Gruppe bei"; /* No comment provided by engineer. */ "Join group conversations" = "Gruppenunterhaltungen beitreten"; -/* No comment provided by engineer. */ -"Join group?" = "Der Gruppe beitreten?"; - /* No comment provided by engineer. */ "Join incognito" = "Inkognito beitreten"; -/* No comment provided by engineer. */ -"Join with current profile" = "Mit dem aktuellen Profil beitreten"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Ihrer Gruppe beitreten?\nDas ist Ihr Link für die Gruppe %@!"; /* No comment provided by engineer. */ @@ -2838,6 +3146,9 @@ /* alert title */ "Keep unused invitation?" = "Nicht genutzte Einladung behalten?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "Ihre Chats übersichtlich halten"; + /* No comment provided by engineer. */ "Keep your connections" = "Ihre Verbindungen beibehalten"; @@ -2871,6 +3182,9 @@ /* rcv group event chat item */ "left" = "hat die Gruppe verlassen"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Weniger Datenverkehr in mobilen Netzen."; + /* email subject */ "Let's talk in SimpleX Chat" = "Lassen Sie uns in SimpleX Chat kommunizieren"; @@ -2889,6 +3203,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"; @@ -2898,6 +3221,9 @@ /* No comment provided by engineer. */ "Live messages" = "Live Nachrichten"; +/* in progress text */ +"Loading profile…" = "Profil wird geladen…"; + /* No comment provided by engineer. */ "Local name" = "Lokaler Name"; @@ -2949,15 +3275,30 @@ /* No comment provided by engineer. */ "Member" = "Mitglied"; +/* past/unknown group member */ +"Member %@" = "Mitglied %@"; + /* profile update event chat item */ "member %@ changed to %@" = "Der Mitgliedsname von %1$@ wurde auf %2$@ geändert"; +/* No comment provided by engineer. */ +"Member admission" = "Aufnahme von Mitgliedern"; + /* rcv group event chat item */ "member connected" = "ist der Gruppe beigetreten"; +/* No comment provided by engineer. */ +"member has old version" = "Das Mitglied hat eine alte App-Version"; + /* item status text */ "Member inactive" = "Mitglied inaktiv"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Mitglied ist gelöscht - Anfrage kann nicht angenommen werden"; + +/* chat feature */ +"Member reports" = "Mitglieder-Meldungen"; + /* 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."; @@ -2973,27 +3314,36 @@ /* 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!"; +/* alert message */ +"Member will join the group, accept member?" = "Mitglied wird der Gruppe beitreten. Annehmen?"; + /* 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 senden."; +"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 senden."; +"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 senden."; +"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"; @@ -3015,11 +3365,14 @@ /* item status text */ "Message forwarded" = "Nachricht weitergeleitet"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Sobald Sie auf Verbinden tippen, erhalten Sie sofort eine Nachricht."; + /* 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"; +"Message queue info" = "Information Nachrichtenwarteschlange"; /* chat feature */ "Message reactions" = "Reaktionen auf Nachrichten"; @@ -3063,9 +3416,15 @@ /* No comment provided by engineer. */ "Messages & files" = "Nachrichten"; +/* No comment provided by engineer. */ +"Messages are protected by **end-to-end encryption**." = "Nachrichten sind durch **Ende-zu-Ende-Verschlüsselung** geschützt."; + /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Die Nachrichten von %@ werden angezeigt!"; +/* alert message */ +"Messages in this chat will never be deleted." = "Nachrichten in diesem Chat werden nie gelöscht."; + /* No comment provided by engineer. */ "Messages received" = "Empfangene Nachrichten"; @@ -3076,10 +3435,10 @@ "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, Ablehnung und Einbruchs-Wiederherstellung geschützt."; +"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, Ablehnung und Einbruchs-Wiederherstellung geschützt."; +"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"; @@ -3138,9 +3497,15 @@ /* 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!"; @@ -3156,12 +3521,12 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Mehrere Chat-Profile"; -/* No comment provided by engineer. */ -"mute" = "Stummschalten"; - -/* swipe action */ +/* notification label action */ "Mute" = "Stummschalten"; +/* notification label action */ +"Mute all" = "Alle stummschalten"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Bei Inaktivität stummgeschaltet!"; @@ -3189,12 +3554,15 @@ /* No comment provided by engineer. */ "Network settings" = "Netzwerkeinstellungen"; -/* No comment provided by engineer. */ +/* alert title */ "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"; @@ -3216,6 +3584,9 @@ /* notification */ "New events" = "Neue Ereignisse"; +/* No comment provided by engineer. */ +"New group role: Moderator" = "Neue Gruppen-Rolle: Moderator"; + /* No comment provided by engineer. */ "New in %@" = "Neu in %@"; @@ -3225,6 +3596,9 @@ /* No comment provided by engineer. */ "New member role" = "Neue Mitgliedsrolle"; +/* rcv group event chat item */ +"New member wants to join the group." = "Ein neues Mitglied will der Gruppe beitreten."; + /* notification */ "new message" = "Neue Nachricht"; @@ -3255,6 +3629,18 @@ /* 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 chats with members" = "Keine Chats mit Mitgliedern"; + /* No comment provided by engineer. */ "No contacts selected" = "Keine Kontakte ausgewählt"; @@ -3288,6 +3674,9 @@ /* 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."; @@ -3303,17 +3692,20 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Keine Berechtigung für das Aufnehmen von Sprachnachrichten"; +/* alert title */ +"No private routing session" = "Keine private Routing-Sitzung"; + /* No comment provided by engineer. */ "No push server" = "Lokal"; /* No comment provided by engineer. */ -"No received or sent files" = "Keine empfangenen oder gesendeten Dateien"; +"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 den Empfang von Dateien."; +"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."; @@ -3321,15 +3713,30 @@ /* servers error */ "No servers to send files." = "Keine Server für das Versenden von Dateien."; +/* No comment provided by engineer. */ +"no subscription" = "Kein Abonnement"; + /* 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. */ +"not synchronized" = "Nicht synchronisiert"; + +/* No comment provided by engineer. */ +"Notes" = "Anmerkungen"; + /* No comment provided by engineer. */ "Nothing selected" = "Nichts ausgewählt"; @@ -3342,9 +3749,15 @@ /* 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)"; @@ -3352,8 +3765,9 @@ "observer" = "Beobachter"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "Aus"; /* blur media */ @@ -3365,7 +3779,9 @@ /* feature offered item */ "offered %@: %@" = "angeboten %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3407,6 +3823,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."; @@ -3419,6 +3841,9 @@ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "Nur Sie können verschwindende Nachrichten senden."; +/* No comment provided by engineer. */ +"Only you can send files and media." = "Nur Sie können Dateien und Medien senden."; + /* No comment provided by engineer. */ "Only you can send voice messages." = "Nur Sie können Sprachnachrichten versenden."; @@ -3435,32 +3860,62 @@ "Only your contact can send disappearing messages." = "Nur Ihr Kontakt kann verschwindende Nachrichten senden."; /* No comment provided by engineer. */ -"Only your contact can send voice messages." = "Nur Ihr Kontakt kann Sprachnachrichten versenden."; +"Only your contact can send files and media." = "Nur Ihr Kontakt kann Dateien und Medien senden."; /* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Nur Ihr Kontakt kann Sprachnachrichten versenden."; + +/* alert action */ "Open" = "Öffnen"; /* No comment provided by engineer. */ "Open changes" = "Änderungen öffnen"; -/* No comment provided by engineer. */ +/* new chat action */ "Open chat" = "Chat öffnen"; /* authentication reason */ "Open chat console" = "Chat-Konsole öffnen"; +/* alert action */ +"Open clean link" = "Trackingfreien Link öffnen"; + /* No comment provided by engineer. */ "Open conditions" = "Nutzungsbedingungen öffnen"; -/* No comment provided by engineer. */ +/* alert action */ +"Open full link" = "Vollständigen Link öffnen"; + +/* new chat action */ "Open group" = "Gruppe öffnen"; +/* alert title */ +"Open link?" = "Link öffnen?"; + /* authentication reason */ "Open migration to another device" = "Migration auf ein anderes Gerät öffnen"; +/* new chat action */ +"Open new chat" = "Neuen Chat öffnen"; + +/* new chat action */ +"Open new group" = "Neue Gruppe öffnen"; + /* No comment provided by engineer. */ "Open Settings" = "Geräte-Einstellungen öffnen"; +/* No comment provided by engineer. */ +"Open to accept" = "Zum Akzeptieren öffnen"; + +/* No comment provided by engineer. */ +"Open to connect" = "Zum Verbinden öffnen"; + +/* No comment provided by engineer. */ +"Open to join" = "Zum Beitreten öffnen"; + +/* No comment provided by engineer. */ +"Open to use bot" = "Bot für die Nutzung erlaubt"; + /* No comment provided by engineer. */ "Opening app…" = "App wird geöffnet…"; @@ -3488,6 +3943,9 @@ /* 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"; @@ -3527,9 +3985,6 @@ /* No comment provided by engineer. */ "Password to show" = "Passwort anzeigen"; -/* past/unknown group member */ -"Past member %@" = "Ehemaliges Mitglied %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Desktop-Adresse einfügen"; @@ -3545,9 +4000,18 @@ /* No comment provided by engineer. */ "peer-to-peer" = "Peer-to-Peer"; +/* No comment provided by engineer. */ +"pending" = "ausstehend"; + /* No comment provided by engineer. */ "Pending" = "Ausstehend"; +/* No comment provided by engineer. */ +"pending approval" = "ausstehende Genehmigung"; + +/* No comment provided by engineer. */ +"pending review" = "Ausstehende Überprüfung"; + /* No comment provided by engineer. */ "Periodic" = "Periodisch"; @@ -3578,7 +4042,7 @@ /* 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."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Bitte überprüfen Sie Ihre Netzwerkverbindung mit %@ und versuchen Sie es erneut."; /* No comment provided by engineer. */ @@ -3614,15 +4078,24 @@ /* 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."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können."; + +/* 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"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren."; @@ -3644,12 +4117,21 @@ /* 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"; @@ -3662,9 +4144,12 @@ /* No comment provided by engineer. */ "Private routing" = "Privates Routing"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Fehler beim privaten Routing"; +/* alert title */ +"Private routing timeout" = "Zeitüberschreitung der privaten Routing-Sitzung"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil und Serververbindungen"; @@ -3695,6 +4180,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."; @@ -3722,6 +4210,9 @@ /* 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 background timeout" = "Protokoll Hintergrund-Zeitüberschreitung"; + /* No comment provided by engineer. */ "Protocol timeout" = "Protokollzeitüberschreitung"; @@ -3794,9 +4285,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "Bestätigung erhalten…"; -/* notification */ -"Received file event" = "Datei-Ereignis empfangen"; - /* message info title */ "Received message" = "Empfangene Nachricht"; @@ -3813,7 +4301,7 @@ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist."; /* No comment provided by engineer. */ -"Receiving file will be stopped." = "Der Empfang der Datei wird beendet."; +"Receiving file will be stopped." = "Das Herunterladen der Datei wird beendet."; /* No comment provided by engineer. */ "Receiving via" = "Empfangen über"; @@ -3857,16 +4345,32 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Reduzierter Batterieverbrauch"; -/* reject incoming call via notification - swipe action */ +/* No comment provided by engineer. */ +"Register" = "Registrieren"; + +/* token info */ +"Register notification token?" = "Benachrichtigungs-Token registrieren?"; + +/* token status text */ +"Registered" = "Registriert"; + +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Ablehnen"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Kontakt ablehnen (der Absender wird NICHT benachrichtigt)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Kontaktanfrage ablehnen"; +/* alert title */ +"Reject member?" = "Mitglied ablehnen?"; + +/* No comment provided by engineer. */ +"rejected" = "abgelehnt"; + /* call status */ "rejected call" = "Abgelehnter Anruf"; @@ -3885,6 +4389,9 @@ /* No comment provided by engineer. */ "Remove image" = "Bild entfernen"; +/* No comment provided by engineer. */ +"Remove link tracking" = "Link-Tracking entfernen"; + /* No comment provided by engineer. */ "Remove member" = "Mitglied entfernen"; @@ -3903,12 +4410,18 @@ /* profile update event chat item */ "removed contact address" = "Die Kontaktadresse wurde entfernt"; +/* No comment provided by engineer. */ +"removed from group" = "Aus der Gruppe entfernt"; + /* profile update event chat item */ "removed profile picture" = "Das Profil-Bild wurde entfernt"; /* rcv group event chat item */ "removed you" = "hat Sie aus der Gruppe entfernt"; +/* No comment provided by engineer. */ +"Removes messages and blocks members." = "Entfernt Nachrichten und blockiert Mitglieder."; + /* No comment provided by engineer. */ "Renegotiate" = "Neu aushandeln"; @@ -3918,24 +4431,63 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Verschlüsselung neu aushandeln?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Verbindungsanfrage wiederholen?"; - /* No comment provided by engineer. */ "Repeat download" = "Herunterladen wiederholen"; /* No comment provided by engineer. */ "Repeat import" = "Import wiederholen"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Verbindungsanfrage wiederholen?"; - /* No comment provided by engineer. */ "Repeat upload" = "Hochladen wiederholen"; /* chat item action */ "Reply" = "Antwort"; +/* chat item action */ +"Report" = "Melden"; + +/* report reason */ +"Report content: only group moderators will see it." = "Inhalt melden: Nur Gruppenmoderatoren werden es sehen."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Mitgliederprofil melden: Nur Gruppenmoderatoren werden es sehen."; + +/* report reason */ +"Report other: only group moderators will see it." = "Anderes melden: Nur Gruppenmoderatoren werden es sehen."; + +/* No comment provided by engineer. */ +"Report reason?" = "Grund der Meldung?"; + +/* alert title */ +"Report sent to moderators" = "Meldung wurde an die Moderatoren gesendet"; + +/* 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"; + +/* No comment provided by engineer. */ +"request is sent" = "Anfrage wurde gesendet"; + +/* No comment provided by engineer. */ +"request to join rejected" = "Beitrittsanfrage abgelehnt"; + +/* rcv group event chat item */ +"requested connection" = "Angefragte Verbindung"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "Angefragte Verbindung von Gruppe %@"; + /* chat list item title */ "requested to connect" = "Zur Verbindung aufgefordert"; @@ -3984,17 +4536,29 @@ /* No comment provided by engineer. */ "Restore database error" = "Fehler bei der Wiederherstellung der Datenbank"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Wiederholen"; /* chat item action */ "Reveal" = "Aufdecken"; +/* No comment provided by engineer. */ +"review" = "Überprüfung"; + /* No comment provided by engineer. */ "Review conditions" = "Nutzungsbedingungen einsehen"; /* No comment provided by engineer. */ -"Review later" = "Später einsehen"; +"Review group members" = "Gruppenmitglieder überprüfen"; + +/* admission stage */ +"Review members" = "Überprüfung der Mitglieder"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Überprüfung der Mitglieder vor der Aufnahme (\"Anklopfen\")."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "Von Administratoren überprüft"; /* No comment provided by engineer. */ "Revoke" = "Widerrufen"; @@ -4012,18 +4576,24 @@ "Run chat" = "Chat starten"; /* No comment provided by engineer. */ -"Safely receive files" = "Dateien sicher empfangen"; +"Safely receive files" = "Dateien sicher herunterladen"; /* No comment provided by engineer. */ "Safer groups" = "Sicherere Gruppen"; /* alert button - chat item action */ +chat item action */ "Save" = "Speichern"; /* alert button */ "Save (and notify contacts)" = "Speichern (und Kontakte benachrichtigen)"; +/* alert button */ +"Save (and notify members)" = "Speichern (und Mitglieder benachrichtigen)"; + +/* alert title */ +"Save admission settings?" = "Speichern der Aufnahme-Einstellungen?"; + /* alert button */ "Save and notify contact" = "Speichern und Kontakt benachrichtigen"; @@ -4039,6 +4609,12 @@ /* No comment provided by engineer. */ "Save group profile" = "Gruppenprofil speichern"; +/* alert title */ +"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"; @@ -4112,7 +4688,7 @@ "Search" = "Suche"; /* No comment provided by engineer. */ -"Search bar accepts invitation links." = "In der Suchleiste werden nun auch Einladungslinks akzeptiert."; +"Search bar accepts invitation links." = "In der Suchleiste werden nun auch Einladungslinks angenommen."; /* No comment provided by engineer. */ "Search or paste SimpleX link" = "Suchen oder SimpleX-Link einfügen"; @@ -4136,7 +4712,7 @@ "Secured" = "Abgesichert"; /* No comment provided by engineer. */ -"Security assessment" = "Sicherheits-Gutachten"; +"Security assessment" = "Security-Gutachten"; /* No comment provided by engineer. */ "Security code" = "Sicherheitscode"; @@ -4175,10 +4751,10 @@ "Send a live message - it will update for the recipient(s) as you type it" = "Eine Live Nachricht senden - der/die Empfänger sieht/sehen Nachrichtenaktualisierungen, während Sie sie eingeben"; /* No comment provided by engineer. */ -"Send delivery receipts to" = "Empfangsbestätigungen senden an"; +"Send contact request?" = "Kontakt-Anfrage senden?"; /* No comment provided by engineer. */ -"send direct message" = "Direktnachricht senden"; +"Send delivery receipts to" = "Empfangsbestätigungen senden an"; /* No comment provided by engineer. */ "Send direct message to connect" = "Eine Direktnachricht zum Verbinden senden"; @@ -4207,18 +4783,30 @@ /* No comment provided by engineer. */ "Send notifications" = "Benachrichtigungen senden"; +/* No comment provided by engineer. */ +"Send private reports" = "Private Meldungen senden"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Senden Sie Fragen und Ideen"; /* No comment provided by engineer. */ "Send receipts" = "Bestätigungen senden"; +/* No comment provided by engineer. */ +"Send request" = "Anfrage senden"; + +/* No comment provided by engineer. */ +"Send request without message" = "Anfrage ohne Nachricht senden"; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Senden Sie diese aus dem Fotoalbum oder von individuellen Tastaturen."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Bis zu 100 der letzten Nachrichten an neue Gruppenmitglieder senden."; +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Senden Sie Ihr privates Feedback an Gruppen."; + /* alert message */ "Sender cancelled file transfer." = "Der Absender hat die Dateiübertragung abgebrochen."; @@ -4258,9 +4846,6 @@ /* No comment provided by engineer. */ "Sent directly" = "Direkt gesendet"; -/* notification */ -"Sent file event" = "Datei-Ereignis wurde gesendet"; - /* message info title */ "Sent message" = "Gesendete Nachricht"; @@ -4307,10 +4892,10 @@ "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"; +"Server requires authorization to create queues, check password." = "Der Server erfordert zum Erstellen von Warteschlangen eine Autorisierung. Bitte überprüfen Sie das Passwort."; /* server test error */ -"Server requires authorization to upload, check password" = "Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung"; +"Server requires authorization to upload, check password." = "Der Server erfordert zum Hochladen eine Autorisierung. Bitte überprüfen Sie das Passwort."; /* No comment provided by engineer. */ "Server test failed!" = "Server Test ist fehlgeschlagen!"; @@ -4339,6 +4924,9 @@ /* 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…"; @@ -4351,6 +4939,12 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen."; +/* No comment provided by engineer. */ +"Set member admission" = "Aufnahme von Mitgliedern 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"; @@ -4366,6 +4960,9 @@ /* No comment provided by engineer. */ "Set passphrase to export" = "Passwort für den Export festlegen"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "Definieren Sie eine Begrüßungsmeldung, die neuen Mitgliedern angezeigt wird!"; @@ -4382,7 +4979,7 @@ "Shape profile images" = "Form der Profil-Bilder"; /* alert action - chat item action */ +chat item action */ "Share" = "Teilen"; /* No comment provided by engineer. */ @@ -4406,6 +5003,12 @@ /* No comment provided by engineer. */ "Share link" = "Link teilen"; +/* alert button */ +"Share old address" = "Alte Adresse teilen"; + +/* alert button */ +"Share old link" = "Vollständigen Link teilen"; + /* No comment provided by engineer. */ "Share profile" = "Profil teilen"; @@ -4421,6 +5024,18 @@ /* No comment provided by engineer. */ "Share with contacts" = "Mit Kontakten teilen"; +/* No comment provided by engineer. */ +"Share your address" = "Ihre Adresse teilen"; + +/* No comment provided by engineer. */ +"Short description" = "Kurze Beschreibung"; + +/* No comment provided by engineer. */ +"Short link" = "Verkürzter Link"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Verkürzte SimpleX-Adresse"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Bei Nachrichten, die über privates Routing versendet wurden, → anzeigen."; @@ -4463,6 +5078,12 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX-Adresse oder Einmal-Link?"; +/* alert title */ +"SimpleX address settings" = "Einstellungen automatisch akzeptieren"; + +/* 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."; @@ -4505,6 +5126,9 @@ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Die SimpleX-Protokolle wurden von Trail of Bits überprüft."; +/* simplex link type */ +"SimpleX relay link" = "SimpleX Relais-Link"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereinfachter Inkognito-Modus"; @@ -4547,6 +5171,10 @@ /* 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."; @@ -4584,13 +5212,13 @@ "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?"; /* alert action */ "Stop sharing" = "Teilen beenden"; @@ -4604,6 +5232,9 @@ /* No comment provided by engineer. */ "Stopping chat" = "Chat wird beendet"; +/* No comment provided by engineer. */ +"Storage" = "Ablage"; + /* No comment provided by engineer. */ "strike" = "durchstreichen"; @@ -4646,9 +5277,21 @@ /* No comment provided by engineer. */ "Tap button " = "Schaltfläche antippen "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "Verbinden tippen, um zu chatten"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "Verbinden tippen, um die Anfrage zu senden"; + +/* No comment provided by engineer. */ +"Tap Connect to use bot" = "Verbinden tippen, um den Bot zu nutzen."; + /* No comment provided by engineer. */ "Tap Create SimpleX address in the menu to create it later." = "Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen."; +/* No comment provided by engineer. */ +"Tap Join group" = "Tippen, um der Gruppe beizutreten"; + /* No comment provided by engineer. */ "Tap to activate profile." = "Zum Aktivieren des Profils tippen."; @@ -4670,9 +5313,15 @@ /* No comment provided by engineer. */ "TCP connection" = "TCP-Verbindung"; +/* No comment provided by engineer. */ +"TCP connection bg timeout" = "TCP-Verbindung Hintergrund-Zeitüberschreitung"; + /* 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"; @@ -4682,12 +5331,15 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* 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"; @@ -4706,6 +5358,9 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Dank der Nutzer - Tragen Sie per Weblate bei!"; +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "Die Adresse wird gekürzt sein, und Ihr Profil wird über die Adresse geteilt."; + /* 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."; @@ -4745,6 +5400,9 @@ /* 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." = "Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der Vorherigen).\nDies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompromittiert wurde."; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "Der Link wird gekürzt sein, und das Gruppen-Profil wird über den Link geteilt."; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "Diese Nachricht wird für alle Gruppenmitglieder gelöscht."; @@ -4760,22 +5418,16 @@ /* 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."; - /* 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 same conditions will apply to operator(s): **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den/die 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! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Der Absender wird NICHT benachrichtigt"; /* No comment provided by engineer. */ @@ -4803,13 +5455,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! Es werden alle empfangenen und gesendeten Dateien und Medien 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." = "Dieser Vorgang kann nicht rückgängig gemacht werden - alle empfangenen und gesendeten Dateien sowie Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten."; /* 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! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, 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." = "Dieser Vorgang kann nicht rückgängig gemacht werden - die früher als ausgewählt gesendeten und empfangenen Nachrichten werden gelöscht. Dies kann ein paar Minuten dauern."; + +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Dieser Vorgang kann nicht rückgängig gemacht werden - die in diesem Chat früher als ausgewählt gesendeten und empfangenen Nachrichten werden gelöscht."; /* 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, 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."; @@ -4833,17 +5488,23 @@ "This group no longer exists." = "Diese Gruppe existiert nicht mehr."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Das ist Ihr eigener Einmal-Link!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Das ist Ihre eigene SimpleX-Adresse!"; +"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. */ +"This setting is for your current profile **%@**." = "Diese Einstellung gilt für Ihr aktuelles Profil **%@**."; + +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Die Zeit bis zum Verschwinden wird nur für neue Kontakte eingestellt."; + /* No comment provided by engineer. */ "Title" = "Bezeichnung"; @@ -4892,9 +5553,15 @@ /* No comment provided by engineer. */ "To send" = "Für das Senden"; +/* alert message */ +"To send commands you must be connected." = "Um Befehle senden zu können, müssen Sie verbunden sein."; + /* 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."; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "Wenn Sie nach dem Verbindungsversuch ein anderes Profil verwenden möchten, löschen Sie den Chat und verwenden Sie den Link erneut."; + /* 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."; @@ -4907,6 +5574,9 @@ /* 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"; @@ -4919,11 +5589,8 @@ /* 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: %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird."; +/* subscription status explanation */ +"Trying to connect to the server used to receive messages from this connection." = "Versuche eine Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten dieser Verbindung genutzt wird."; /* No comment provided by engineer. */ "Turkish interface" = "Türkische Bedienoberfläche"; @@ -4953,7 +5620,7 @@ "Unblock member?" = "Mitglied freigeben?"; /* rcv group event chat item */ -"unblocked %@" = "%@ wurde freigegeben"; +"unblocked %@" = "hat %@ freigegeben"; /* No comment provided by engineer. */ "Undelivered messages" = "Nicht ausgelieferte Nachrichten"; @@ -5015,10 +5682,7 @@ /* authentication reason */ "Unlock app" = "App entsperren"; -/* No comment provided by engineer. */ -"unmute" = "Stummschaltung aufheben"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Stummschaltung aufheben"; /* No comment provided by engineer. */ @@ -5027,6 +5691,9 @@ /* 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."; @@ -5042,6 +5709,9 @@ /* No comment provided by engineer. */ "Update settings?" = "Einstellungen aktualisieren?"; +/* No comment provided by engineer. */ +"Updated conditions" = "Aktualisierte Nutzungsbedingungen"; + /* rcv group event chat item */ "updated group profile" = "Aktualisiertes Gruppenprofil"; @@ -5051,9 +5721,27 @@ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden."; +/* alert button */ +"Upgrade" = "Aktualisieren"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Adresse aktualisieren"; + +/* alert message */ +"Upgrade address?" = "Adresse aktualisieren?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "Aktualisieren und den Chat öffnen"; +/* alert message */ +"Upgrade group link?" = "Gruppen-Link aktualisieren?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Link hinzufügen"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Ihre Adresse aktualisieren"; + /* No comment provided by engineer. */ "Upload errors" = "Fehler beim Hochladen"; @@ -5081,7 +5769,7 @@ /* No comment provided by engineer. */ "Use chat" = "Verwenden Sie Chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Aktuelles Profil nutzen"; /* No comment provided by engineer. */ @@ -5097,9 +5785,12 @@ "Use from desktop" = "Vom Desktop aus nutzen"; /* No comment provided by engineer. */ -"Use iOS call interface" = "iOS Anrufschnittstelle nutzen"; +"Use incognito profile" = "Inkognito-Profil nutzen"; /* No comment provided by engineer. */ +"Use iOS call interface" = "iOS Anrufschnittstelle nutzen"; + +/* new chat action */ "Use new incognito profile" = "Neues Inkognito-Profil nutzen"; /* No comment provided by engineer. */ @@ -5123,12 +5814,21 @@ /* 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. */ "Use the app with one hand." = "Die App mit einer Hand bedienen."; +/* No comment provided by engineer. */ +"Use web port" = "Web-Port nutzen"; + /* No comment provided by engineer. */ "User selection" = "Benutzer-Auswahl"; @@ -5193,10 +5893,10 @@ "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"; @@ -5279,6 +5979,9 @@ /* No comment provided by engineer. */ "Welcome message is too long" = "Die Begrüßungsmeldung ist zu lang"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Begrüßen Sie Ihre Kontakte 👋"; + /* No comment provided by engineer. */ "What's new" = "Was ist neu"; @@ -5346,7 +6049,10 @@ "You **must not** use the same database on two devices." = "Sie dürfen die selbe Datenbank **nicht** auf zwei Geräten nutzen."; /* No comment provided by engineer. */ -"You accepted connection" = "Sie haben die Verbindung akzeptiert"; +"You accepted connection" = "Sie haben die Verbindung angenommen"; + +/* snd group event chat item */ +"you accepted this member" = "Sie haben dieses Mitglied angenommen"; /* No comment provided by engineer. */ "You allow" = "Sie erlauben"; @@ -5360,36 +6066,33 @@ /* No comment provided by engineer. */ "You are already connected with %@." = "Sie sind bereits mit %@ verbunden."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Sie sind bereits mit %@ verbunden."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Sie sind bereits über diesen Einmal-Link verbunden!"; /* No comment provided by engineer. */ "You are already in group %@." = "Sie sind bereits Mitglied der Gruppe %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Sie sind bereits Mitglied der Gruppe %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Sie sind über diesen Link bereits Mitglied der Gruppe!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Sie sind über diesen Link bereits Mitglied der Gruppe."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Sie sind bereits Mitglied dieser Gruppe!\nVerbindungsanfrage wiederholen?"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird."; - -/* No comment provided by engineer. */ -"you are invited to group" = "Sie sind zu der Gruppe eingeladen"; +/* subscription status explanation */ +"You are connected to the server used to receive messages from this connection." = "Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten dieser Verbindung genutzt wird."; /* No comment provided by engineer. */ "You are invited to group" = "Sie sind zu der Gruppe eingeladen"; +/* subscription status explanation */ +"You are not connected to the server used to receive messages from this connection (no subscription)." = "Sie sind nicht mit dem Server verbunden, der für den Empfang von Nachrichten dieser Verbindung genutzt wird (kein Abonnement)."; + /* 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."; @@ -5405,9 +6108,6 @@ /* 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 operators in Network & servers settings." = "Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren."; - /* No comment provided by engineer. */ "You can configure servers via settings." = "Sie können die Server über die Einstellungen konfigurieren."; @@ -5462,7 +6162,10 @@ /* 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. */ +/* alert message */ +"You can view your reports in Chat with admins." = "Sie können Ihre Meldungen im Chat mit den Administratoren sehen."; + +/* alert title */ "You can't send messages!" = "Sie können keine Nachrichten versenden!"; /* chat item text */ @@ -5483,10 +6186,7 @@ /* 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!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Sie haben bereits ein Verbindungsanfrage beantragt!\nVerbindungsanfrage wiederholen?"; /* No comment provided by engineer. */ @@ -5534,9 +6234,15 @@ /* chat list item description */ "you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt"; +/* token info */ +"You should receive notifications." = "Sie sollten Benachrichtigungen erhalten."; + /* snd group event chat item */ "you unblocked %@" = "Sie haben %@ freigegeben"; +/* No comment provided by engineer. */ +"You will be able to send messages **only after your request is accepted**." = "Sie können erst dann Nachrichten versenden, **sobald Ihre Anfrage angenommen wurde**."; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!"; @@ -5544,7 +6250,7 @@ "You will be connected when group link host's device is online, please wait or check later!" = "Sie werden verbunden, sobald das Endgerät des Gruppenlink-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!"; /* No comment provided by engineer. */ -"You will be connected when your connection request is accepted, please wait or check later!" = "Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach!"; +"You will be connected when your connection request is accepted, please wait or check later!" = "Sie werden verbunden, sobald Ihre Verbindungsanfrage angenommen wird. Bitte warten oder schauen Sie später nochmal nach!"; /* No comment provided by engineer. */ "You will be connected when your contact's device is online, please wait or check later!" = "Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!"; @@ -5552,14 +6258,11 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Sie werden mit allen Gruppenmitgliedern verbunden."; - /* 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 wird beibehalten."; +"You will stop receiving messages from this chat. Chat history will be preserved." = "Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf bleibt erhalten."; /* No comment provided by engineer. */ "You 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."; @@ -5576,6 +6279,9 @@ /* 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 business contact" = "Ihr geschäftlicher Kontakt"; + /* No comment provided by engineer. */ "Your calls" = "Anrufe"; @@ -5591,8 +6297,14 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Ihre Chat-Profile"; +/* alert message */ +"Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Ihr Chat wurde nach %@ verschoben, aber es ist ein unerwarteter Fehler aufgetreten, als Sie zum Profil weitergeleitet wurden."; + /* No comment provided by engineer. */ -"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."; +"Your connection was moved to %@ but an error happened when switching 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" = "Ihr Kontakt"; /* 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 (%@)."; @@ -5612,6 +6324,9 @@ /* No comment provided by engineer. */ "Your current profile" = "Mein aktuelles Chat-Profil"; +/* No comment provided by engineer. */ +"Your group" = "Ihre Gruppe"; + /* No comment provided by engineer. */ "Your ICE servers" = "Ihre ICE-Server"; @@ -5619,7 +6334,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"; @@ -5627,15 +6342,15 @@ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Ihr Profil **%@** wird geteilt."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten 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."; /* 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."; - /* No comment provided by engineer. */ "Your random profile" = "Ihr Zufallsprofil"; @@ -5651,6 +6366,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Ihre SimpleX-Adresse"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Ihre SMP-Server"; - diff --git a/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings index 0dee85ad95..e0554c9fb6 100644 --- a/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings @@ -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/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index e7570f177e..5ce7ab6843 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (puede copiarse)"; @@ -22,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)"; @@ -77,7 +56,7 @@ "**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 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."; @@ -199,6 +178,9 @@ /* time interval */ "%d sec" = "%d segundo(s)"; +/* delete after time */ +"%d seconds(s)" = "%d segundos"; + /* integrity error chat item */ "%d skipped message(s)" = "%d mensaje(s) omitido(s)"; @@ -241,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"; @@ -289,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "un dia"; /* time interval */ @@ -298,12 +278,17 @@ /* 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"; @@ -332,7 +317,7 @@ "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"; @@ -356,23 +341,39 @@ "Accent" = "Color"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Aceptar"; +/* alert action */ +"Accept as member" = "Aceptar como miembro"; + +/* alert action */ +"Accept as observer" = "Aceptar como observador"; + /* No comment provided by engineer. */ "Accept conditions" = "Aceptar condiciones"; /* No comment provided by engineer. */ "Accept connection request?" = "¿Aceptar solicitud de conexión?"; +/* alert title */ +"Accept contact request" = "Aceptar petición de contacto"; + /* notification body */ "Accept contact request from %@?" = "¿Aceptar solicitud de contacto de %@?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Aceptar incógnito"; +/* alert title */ +"Accept member" = "Aceptar miembro"; + +/* rcv group event chat item */ +"accepted %@" = "%@ aceptado"; + /* call status */ "accepted call" = "llamada aceptada"; @@ -382,12 +383,18 @@ /* chat list item title */ "accepted invitation" = "invitación aceptada"; +/* rcv group event chat item */ +"accepted you" = "te ha aceptado"; + /* 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"; @@ -397,6 +404,12 @@ /* No comment provided by engineer. */ "Add friends" = "Añadir amigos"; +/* No comment provided by engineer. */ +"Add list" = "Añadir lista"; + +/* placeholder for sending contact request */ +"Add message" = "Añadir mensaje"; + /* No comment provided by engineer. */ "Add profile" = "Añadir perfil"; @@ -412,11 +425,14 @@ /* 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 los miembros de tu equipo a las conversaciones."; +"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"; @@ -443,7 +459,7 @@ "Address or 1-time link?" = "¿Dirección o enlace de un uso?"; /* No comment provided by engineer. */ -"Address settings" = "Configuración de dirección"; +"Address settings" = "Configurar dirección"; /* member role */ "admin" = "administrador"; @@ -469,11 +485,20 @@ /* chat item text */ "agreeing encryption…" = "acordando cifrado…"; +/* member criteria value */ +"all" = "todos"; + +/* 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."; @@ -491,10 +516,10 @@ "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!"; +"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 podrá deshacerse!"; +"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!"; @@ -502,6 +527,12 @@ /* 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."; @@ -521,13 +552,16 @@ "Allow calls?" = "¿Permitir llamadas?"; /* No comment provided by engineer. */ -"Allow disappearing messages only if your contact allows it to you." = "Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí."; +"Allow disappearing messages only if your contact allows it to you." = "Se permiten los mensajes temporales pero sólo si tu contacto también los permite."; /* No comment provided by engineer. */ "Allow downgrade" = "Permitir versión anterior"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas)"; +"Allow files and media only if your contact allows them." = "Se permiten archivos y multimedia pero sólo si tu contacto también los permite."; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas)"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Se permiten las reacciones a los mensajes pero sólo si tu contacto también las permite."; @@ -547,6 +581,9 @@ /* 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."; @@ -563,27 +600,30 @@ "Allow voice messages?" = "¿Permites los mensajes de voz?"; /* No comment provided by engineer. */ -"Allow your contacts adding message reactions." = "Permitir que tus contactos añadan reacciones a los mensajes."; +"Allow your contacts adding message reactions." = "Permites que tus contactos añadan reacciones a los mensajes."; /* No comment provided by engineer. */ -"Allow your contacts to call you." = "Permites que tus contactos puedan llamarte."; +"Allow your contacts to call you." = "Permites que tus contactos te llamen."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Permites a tus contactos eliminar irreversiblemente los mensajes enviados. (24 horas)"; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Permites que tus contactos eliminan irreversiblemente los mensajes enviados. (24 horas)"; /* No comment provided by engineer. */ -"Allow your contacts to send disappearing messages." = "Permites a tus contactos enviar mensajes temporales."; +"Allow your contacts to send disappearing messages." = "Permites que tus contactos envien mensajes temporales."; /* No comment provided by engineer. */ -"Allow your contacts to send voice messages." = "Permites a tus contactos enviar mensajes de voz."; +"Allow your contacts to send files and media." = "Permes que tus contactos envíen archivos y multimedia."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "Permites que tus contactos envien mensajes de voz."; /* No comment provided by engineer. */ "Already connected?" = "¿Ya está conectado?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "¡Ya en proceso de conexión!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "¡Ya en proceso de unirte al grupo!"; /* pref value */ @@ -601,6 +641,9 @@ /* 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"; @@ -617,7 +660,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"; @@ -626,7 +672,7 @@ "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" = "Sesión de aplicación"; +"App session" = "por sesión"; /* No comment provided by engineer. */ "App version" = "Versión de la aplicación"; @@ -643,15 +689,36 @@ /* 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"; @@ -662,7 +729,7 @@ "attempts" = "intentos"; /* No comment provided by engineer. */ -"Audio & video calls" = "Llamadas y videollamadas"; +"Audio & video calls" = "Llamadas y Videollamadas"; /* No comment provided by engineer. */ "Audio and video calls" = "Llamadas y videollamadas"; @@ -700,9 +767,6 @@ /* 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"; @@ -730,6 +794,9 @@ /* 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."; @@ -742,12 +809,21 @@ /* 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. */ +"Bio" = "Biografía"; + +/* alert title */ +"Bio too large" = "Biografía demasiado larga"; + /* No comment provided by engineer. */ "Black" = "Negro"; @@ -775,7 +851,8 @@ /* 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. */ @@ -790,6 +867,9 @@ /* No comment provided by engineer. */ "bold" = "negrita"; +/* No comment provided by engineer. */ +"Bot" = "Bot"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "Tanto tú como tu contacto podéis añadir reacciones a los mensajes."; @@ -802,6 +882,9 @@ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Tanto tú como tu contacto podéis enviar mensajes temporales."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Tanto tú como tu contacto podéis enviar archivos y multimedia."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Tanto tú como tu contacto podéis enviar mensajes de voz."; @@ -814,9 +897,18 @@ /* No comment provided by engineer. */ "Business chats" = "Chats empresariales"; +/* No comment provided by engineer. */ +"Business connection" = "Conexión empresarial"; + +/* 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"; @@ -847,6 +939,9 @@ /* No comment provided by engineer. */ "Can't call member" = "No se puede llamar al miembro"; +/* alert title */ +"Can't change profile" = "No se puede cambiar el perfil"; + /* No comment provided by engineer. */ "Can't invite contact!" = "¡No se puede invitar el contacto!"; @@ -856,8 +951,12 @@ /* No comment provided by engineer. */ "Can't message member" = "No se pueden enviar mensajes al miembro"; +/* No comment provided by engineer. */ +"can't send messages" = "no se pueden enviar mensajes"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Cancelar"; /* No comment provided by engineer. */ @@ -884,6 +983,9 @@ /* 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"; @@ -912,7 +1014,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 */ @@ -936,7 +1038,7 @@ /* No comment provided by engineer. */ "Chat already exists" = "El chat ya existe"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "¡El chat ya existe!"; /* No comment provided by engineer. */ @@ -985,14 +1087,26 @@ "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 podrá deshacerse!"; +"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 podrá deshacerse!"; +"Chat will be deleted for you - this cannot be undone!" = "El chat será eliminado para tí. ¡No puede deshacerse!"; + +/* chat toolbar */ +"Chat with admins" = "Chatea con administradores"; + +/* No comment provided by engineer. */ +"Chat with member" = "Chat con miembro"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Chatea con el miembro antes de unirse."; /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Chats with members" = "Chat con miembros"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Comprobar mensajes cada 20 min."; @@ -1009,7 +1123,7 @@ "Choose _Migrate from another device_ on the new device and scan QR code." = "En el nuevo dispositivo selecciona _Migrar desde otro dispositivo_ y escanéa el código QR."; /* No comment provided by engineer. */ -"Choose file" = "Elije archivo"; +"Choose file" = "Elegir archivo"; /* No comment provided by engineer. */ "Choose from library" = "Elige de la biblioteca"; @@ -1033,7 +1147,13 @@ "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"; @@ -1047,6 +1167,9 @@ /* 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"; @@ -1068,15 +1191,9 @@ /* 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. */ +/* alert button */ "Conditions of use" = "Condiciones de uso"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Las condiciones de los operadores habilitados serán aceptadas después de 30 días."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for operator(s): **%@**." = "Las condiciones serán aceptadas para el/los operador(es): **%@**."; - /* No comment provided by engineer. */ "Conditions will be accepted for the operator(s): **%@**." = "Las condiciones serán aceptadas para el/los operador(es): **%@**."; @@ -1089,6 +1206,9 @@ /* 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"; @@ -1119,6 +1239,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Confirmar subida"; +/* token status text */ +"Confirmed" = "Confirmado"; + /* server test step */ "Connect" = "Conectar"; @@ -1126,7 +1249,7 @@ "Connect automatically" = "Conectar automáticamente"; /* No comment provided by engineer. */ -"Connect incognito" = "Conectar incognito"; +"Connect faster! 🚀" = "¡Conéctate más rápido! 🚀"; /* No comment provided by engineer. */ "Connect to desktop" = "Conectar con ordenador"; @@ -1135,27 +1258,24 @@ "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."; +"Connect to your friends faster." = "Conéctate más rápido con tus amigos."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "¿Conectarte a tí mismo?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "¿Conectarte a tí mismo?\n¡Este es tu propio enlace de un solo uso!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "¿Conectarte a tí mismo?\n¡Esta es tu propia dirección SimpleX!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Conectar mediante dirección de contacto"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Conectar mediante enlace"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Conectar mediante enlace de un sólo uso"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Conectar con %@"; /* No comment provided by engineer. */ @@ -1167,9 +1287,6 @@ /* 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"; @@ -1177,7 +1294,7 @@ "Connected to desktop" = "Conectado con ordenador"; /* No comment provided by engineer. */ -"connecting" = "conectando"; +"connecting" = "conectando..."; /* No comment provided by engineer. */ "Connecting" = "Conectando"; @@ -1219,6 +1336,9 @@ "Connection and servers status." = "Estado de tu conexión y servidores."; /* No comment provided by engineer. */ +"Connection blocked" = "Conexión bloqueada"; + +/* alert title */ "Connection error" = "Error conexión"; /* No comment provided by engineer. */ @@ -1227,19 +1347,28 @@ /* 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. */ +/* alert title */ "Connection timeout" = "Tiempo de conexión agotado"; /* No comment provided by engineer. */ @@ -1260,9 +1389,15 @@ /* 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 deleted!" = "¡Contacto eliminado!"; +/* No comment provided by engineer. */ +"contact disabled" = "contacto desactivado"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "el contacto dispone de cifrado de extremo a extremo"; @@ -1281,11 +1416,20 @@ /* No comment provided by engineer. */ "Contact name" = "Contacto"; +/* No comment provided by engineer. */ +"contact not ready" = "en espera de ser aceptado"; + /* 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 podrá deshacerse!"; +"Contact requests from groups" = "Solicitudes de contacto en grupo"; + +/* No comment provided by engineer. */ +"contact should accept…" = "el contacto debe aceptarte…"; + +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "El contacto será eliminado. ¡No puede deshacerse!"; /* No comment provided by engineer. */ "Contacts" = "Contactos"; @@ -1293,6 +1437,9 @@ /* 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"; @@ -1318,7 +1465,7 @@ "Create" = "Crear"; /* No comment provided by engineer. */ -"Create 1-time link" = "Crear enlace de un uso"; +"Create 1-time link" = "Crear enlace de un solo uso"; /* No comment provided by engineer. */ "Create a group using a random profile." = "Crear grupo usando perfil aleatorio."; @@ -1335,6 +1482,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/). 💻"; @@ -1345,10 +1495,10 @@ "Create queue" = "Crear cola"; /* No comment provided by engineer. */ -"Create secret group" = "Crea grupo secreto"; +"Create SimpleX address" = "Crear dirección SimpleX"; /* No comment provided by engineer. */ -"Create SimpleX address" = "Crear dirección SimpleX"; +"Create your address" = "Crea tu dirección"; /* No comment provided by engineer. */ "Create your profile" = "Crea tu perfil"; @@ -1476,7 +1626,8 @@ /* No comment provided by engineer. */ "decryption errors" = "errores de descifrado"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "predeterminado (%@)"; /* No comment provided by engineer. */ @@ -1486,8 +1637,7 @@ "default (yes)" = "predeterminado (sí)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Eliminar"; /* No comment provided by engineer. */ @@ -1514,12 +1664,18 @@ /* No comment provided by engineer. */ "Delete chat" = "Eliminar chat"; +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Elimina los mensajes del dispositivo."; + /* No comment provided by engineer. */ "Delete chat profile" = "Eliminar perfil"; /* No comment provided by engineer. */ "Delete chat profile?" = "¿Eliminar perfil?"; +/* alert title */ +"Delete chat with member?" = "¿Eliminar chat con miembro?"; + /* No comment provided by engineer. */ "Delete chat?" = "¿Eliminar chat?"; @@ -1568,14 +1724,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"; @@ -1587,7 +1746,7 @@ "Delete old database?" = "¿Eliminar base de datos antigua?"; /* No comment provided by engineer. */ -"Delete or moderate up to 200 messages." = "Borra o modera hasta 200 mensajes a la vez."; +"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?"; @@ -1598,6 +1757,9 @@ /* 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."; @@ -1623,7 +1785,7 @@ "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"; @@ -1640,9 +1802,15 @@ /* No comment provided by engineer. */ "Delivery receipts!" = "¡Confirmación de entrega!"; +/* No comment provided by engineer. */ +"Deprecated options" = "Opciones obsoletas"; + /* No comment provided by engineer. */ "Description" = "Descripción"; +/* alert title */ +"Description too large" = "Descripción demasiado larga"; + /* No comment provided by engineer. */ "Desktop address" = "Dirección ordenador"; @@ -1706,6 +1874,12 @@ /* 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"; @@ -1758,7 +1932,7 @@ "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."; +"Do not use credentials with proxy." = "No se usan credenciales con proxy."; /* No comment provided by engineer. */ "Do NOT use private routing." = "NO usar enrutamiento privado."; @@ -1766,6 +1940,9 @@ /* 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"; @@ -1773,13 +1950,19 @@ "Don't enable" = "No activar"; /* No comment provided by engineer. */ +"Don't miss important messages." = "No pierdas los mensajes importantes."; + +/* alert action */ "Don't show again" = "No volver a mostrar"; +/* No comment provided by engineer. */ +"Done" = "Hecho"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Degradar y abrir Chat"; /* alert button - chat item action */ +chat item action */ "Download" = "Descargar"; /* No comment provided by engineer. */ @@ -1830,20 +2013,26 @@ /* No comment provided by engineer. */ "Edit group profile" = "Editar perfil de grupo"; +/* No comment provided by engineer. */ +"Empty message!" = "¡Mensaje vacío!"; + /* No comment provided by engineer. */ "Enable" = "Activar"; /* 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" = "Habilita Flux"; +"Enable disappearing messages by default." = "Activa por defecto los mensajes temporales."; + +/* 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"; @@ -1852,7 +2041,7 @@ "Enable in direct chats (BETA)!" = "¡Activar en chats directos (BETA)!"; /* No comment provided by engineer. */ -"Enable instant notifications?" = "¿Activar notificación instantánea?"; +"Enable instant notifications?" = "¿Activar notificaciones instantáneas?"; /* No comment provided by engineer. */ "Enable lock" = "Activar bloqueo"; @@ -1897,7 +2086,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"; @@ -1956,6 +2145,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"; @@ -1981,16 +2173,16 @@ "Enter password above to show!" = "¡Introduce la contraseña arriba para mostrar!"; /* No comment provided by engineer. */ -"Enter server manually" = "Introduce el servidor manualmente"; +"Enter server manually" = "Añadir manualmente"; /* No comment provided by engineer. */ "Enter this device name…" = "Nombre de este dispositivo…"; /* placeholder */ -"Enter welcome message…" = "Introduce mensaje de bienvenida…"; +"Enter welcome message…" = "Deja un mensaje de bienvenida…"; /* placeholder */ -"Enter welcome message… (optional)" = "Introduce mensaje de bienvenida… (opcional)"; +"Enter welcome message… (optional)" = "Deja un mensaje de bienvenida… (opcional)"; /* No comment provided by engineer. */ "Enter your name…" = "Introduce tu nombre…"; @@ -2010,30 +2202,45 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Error al aceptar solicitud del contacto"; +/* alert title */ +"Error accepting member" = "Error al aceptar el miembro"; + /* 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 adding short link" = "Error al añadir enlace corto"; + /* No comment provided by engineer. */ "Error changing address" = "Error al cambiar servidor"; +/* alert title */ +"Error changing chat profile" = "Error al cambiar perfil de chat"; + /* 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. */ +/* alert title */ "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"; + +/* alert message */ "Error connecting to forwarding server %@. Please try later." = "Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde."; +/* subscription status explanation */ +"Error connecting to the server used to receive messages from this connection: %@" = "Error al conectar con el servidor usado para recibir mensajes de esta conexión: %@"; + /* No comment provided by engineer. */ "Error creating address" = "Error al crear dirección"; @@ -2043,6 +2250,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"; @@ -2052,22 +2262,28 @@ /* 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"; -/* No comment provided by engineer. */ +/* alert title */ +"Error deleting chat" = "Error al eliminar el chat"; + +/* alert title */ "Error deleting chat database" = "Error al eliminar base de datos"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "¡Error al eliminar chat!"; /* No comment provided by engineer. */ "Error deleting connection" = "Error al eliminar conexión"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Error al eliminar base de datos"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Error al eliminar base de datos antigua"; /* No comment provided by engineer. */ @@ -2088,13 +2304,13 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Error al cifrar base de datos"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Error al importar base de datos"; /* No comment provided by engineer. */ @@ -2109,6 +2325,9 @@ /* No comment provided by engineer. */ "Error opening chat" = "Error al abrir chat"; +/* No comment provided by engineer. */ +"Error opening group" = "Error al abrir el grupo"; + /* alert title */ "Error receiving file" = "Error al recibir archivo"; @@ -2118,12 +2337,24 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Error al reconectar con los servidores"; -/* No comment provided by engineer. */ -"Error removing member" = "Error al eliminar miembro"; +/* alert title */ +"Error registering for notifications" = "Error al registrarse para notificaciones"; + +/* alert title */ +"Error rejecting contact request" = "Error al rechazar la solicitud del contacto"; + +/* alert title */ +"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"; @@ -2157,6 +2388,9 @@ /* No comment provided by engineer. */ "Error sending message" = "Error al enviar mensaje"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Error al configurar auto aceptar"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "¡Error al configurar confirmaciones de entrega!"; @@ -2166,7 +2400,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Error al parar SimpleX"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Error al cambiar perfil"; /* alertTitle */ @@ -2175,6 +2409,9 @@ /* 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"; @@ -2199,9 +2436,14 @@ /* No comment provided by engineer. */ "Error: " = "Error: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Error: %@"; +/* server test error */ +"Error: %@." = "Error: %@."; + /* No comment provided by engineer. */ "Error: no database file" = "Error: sin archivo de base de datos"; @@ -2217,9 +2459,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Incluso si está desactivado para la conversación."; -/* No comment provided by engineer. */ -"event happened" = "evento ocurrido"; - /* No comment provided by engineer. */ "Exit without saving" = "Salir sin guardar"; @@ -2229,6 +2468,9 @@ /* No comment provided by engineer. */ "expired" = "expirados"; +/* token status text */ +"Expired" = "Expirado"; + /* No comment provided by engineer. */ "Export database" = "Exportar base de datos"; @@ -2253,20 +2495,32 @@ /* 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 not found - most likely file was deleted or cancelled." = "Archivo no encontrado, probablemente haya sido borrado o cancelado."; +"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: %@"; @@ -2298,6 +2552,9 @@ /* chat feature */ "Files and media" = "Archivos y multimedia"; +/* No comment provided by engineer. */ +"Files and media are prohibited in this chat." = "Los archivos y multimedia no están permitidos en este chat."; + /* No comment provided by engineer. */ "Files and media are prohibited." = "Los archivos y multimedia no están permitidos en este grupo."; @@ -2322,6 +2579,18 @@ /* No comment provided by engineer. */ "Find chats faster" = "Encuentra chats mas rápido"; +/* No comment provided by engineer. */ +"Fingerprint in destination server address does not match certificate: %@." = "La huella en la dirección del servidor de destino no coincide con el certificado: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in forwarding server address does not match certificate: %@." = "La huella en la dirección del servidor de reenvío no coincide con el certificado: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in server address does not match certificate: %@." = "La huella en la dirección del servidor no coincide con el certificado: %@."; + +/* server test error */ +"Fingerprint in server address does not match certificate." = "La huella en la dirección del servidor no coincide con el certificado."; + /* No comment provided by engineer. */ "Fix" = "Reparar"; @@ -2341,7 +2610,7 @@ "Fix not supported by group member" = "Corrección no compatible con miembro del grupo"; /* No comment provided by engineer. */ -"for better metadata privacy." = "para mejorar la privacidad de los metadatos."; +"For all moderators" = "Para todos los moderadores"; /* servers error */ "For chat profile %@:" = "Para el perfil de chat %@:"; @@ -2353,7 +2622,10 @@ "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 private routing" = "Para el enrutamiento privado"; +"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"; @@ -2388,8 +2660,8 @@ /* 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."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "El servidor de reenvío %1$@ no ha podido conectarse al servidor de destino %2$@. 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: %@."; @@ -2424,6 +2696,9 @@ /* 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"; @@ -2433,13 +2708,16 @@ /* message preview */ "Good morning!" = "¡Buenos días!"; +/* shown on group welcome message */ +"group" = "grupo"; + /* No comment provided by engineer. */ "Group" = "Grupo"; /* No comment provided by engineer. */ "Group already exists" = "El grupo ya existe"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "¡El grupo ya existe!"; /* No comment provided by engineer. */ @@ -2463,6 +2741,9 @@ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "La invitación al grupo ya no es válida, ha sido eliminada por el remitente."; +/* No comment provided by engineer. */ +"group is deleted" = "el grupo ha sido eliminado"; + /* No comment provided by engineer. */ "Group link" = "Enlace de grupo"; @@ -2487,18 +2768,27 @@ /* snd group event chat item */ "group profile updated" = "perfil de grupo actualizado"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "El perfil del grupo ha cambiado. Si lo guardas, el perfil actualizado se enviará a los miembros del grupo."; + /* No comment provided by engineer. */ "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"; @@ -2518,7 +2808,7 @@ "Hide profile" = "Ocultar perfil"; /* No comment provided by engineer. */ -"Hide:" = "Ocultar:"; +"Hide:" = "Oculta:"; /* No comment provided by engineer. */ "History" = "Historial"; @@ -2535,6 +2825,9 @@ /* 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. */ "How SimpleX works" = "Cómo funciona SimpleX"; @@ -2622,6 +2915,12 @@ /* 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"; @@ -2688,6 +2987,21 @@ /* No comment provided by engineer. */ "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"; @@ -2703,7 +3017,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "¡Nombre mostrado no válido!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Enlace no válido"; /* No comment provided by engineer. */ @@ -2800,27 +3114,21 @@ "Japanese interface" = "Interfáz en japonés"; /* swipe action */ -"Join" = "Unirte"; +"Join" = "Unirme"; /* No comment provided by engineer. */ -"join as %@" = "unirte como %@"; +"Join as %@" = "Unirme como %@"; -/* No comment provided by engineer. */ -"Join group" = "Unirte al grupo"; +/* new chat sheet title */ +"Join group" = "Unirme al grupo"; /* No comment provided by engineer. */ "Join group conversations" = "Unirse a la conversación del grupo"; /* No comment provided by engineer. */ -"Join group?" = "¿Unirte al grupo?"; +"Join incognito" = "Unirme en modo incógnito"; -/* No comment provided by engineer. */ -"Join incognito" = "Unirte en modo incógnito"; - -/* No comment provided by engineer. */ -"Join with current profile" = "Unirte con el perfil actual"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "¿Unirse a tu grupo?\n¡Este es tu enlace para el grupo %@!"; /* No comment provided by engineer. */ @@ -2836,7 +3144,10 @@ "Keep the app open to use it from desktop" = "Mantén la aplicación abierta para usarla desde el ordenador"; /* alert title */ -"Keep unused invitation?" = "¿Guardar invitación no usada?"; +"Keep unused invitation?" = "¿Guardar enlace no usado?"; + +/* No comment provided by engineer. */ +"Keep your chats clean" = "Mantén los chats limpios"; /* No comment provided by engineer. */ "Keep your connections" = "Conserva tus conexiones"; @@ -2871,6 +3182,9 @@ /* rcv group event chat item */ "left" = "ha salido"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Menos tráfico en redes móviles."; + /* email subject */ "Let's talk in SimpleX Chat" = "Hablemos en SimpleX Chat"; @@ -2889,6 +3203,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"; @@ -2898,6 +3221,9 @@ /* No comment provided by engineer. */ "Live messages" = "Mensajes en vivo"; +/* in progress text */ +"Loading profile…" = "Cargando perfil. . ."; + /* No comment provided by engineer. */ "Local name" = "Nombre local"; @@ -2949,17 +3275,32 @@ /* No comment provided by engineer. */ "Member" = "Miembro"; +/* past/unknown group member */ +"Member %@" = "Miembro %@"; + /* profile update event chat item */ "member %@ changed to %@" = "el miembro %1$@ ha cambiado a %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Admisión de miembros"; + /* rcv group event chat item */ "member connected" = "conectado"; +/* No comment provided by engineer. */ +"member has old version" = "el miembro usa una versión antigua"; + /* item status text */ "Member inactive" = "Miembro inactivo"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\" y todos serán notificados."; +"Member is deleted - can't accept request" = "Miembro eliminado, no puede aceptar solicitudes"; + +/* chat feature */ +"Member reports" = "Informes de miembros"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\". Se notificará en el chat."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará al grupo."; @@ -2968,10 +3309,13 @@ "Member role will be changed to \"%@\". The member will receive a new invitation." = "El rol del miembro cambiará a \"%@\" y recibirá una invitación nueva."; /* No comment provided by engineer. */ -"Member will be removed from chat - this cannot be undone!" = "El miembro será eliminado del chat. ¡No 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 podrá deshacerse!"; +"Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No puede deshacerse!"; + +/* alert message */ +"Member will join the group, accept member?" = "El miembro se unirá al grupo, ¿aceptas al miembro?"; /* No comment provided by engineer. */ "Members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; @@ -2979,6 +3323,9 @@ /* 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."; @@ -2994,6 +3341,9 @@ /* 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"; @@ -3015,6 +3365,9 @@ /* item status text */ "Message forwarded" = "Mensaje reenviado"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Tras pulsar Contactar, mensajea ya."; + /* 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."; @@ -3064,7 +3417,13 @@ "Messages & files" = "Mensajes"; /* No comment provided by engineer. */ -"Messages from %@ will be shown!" = "¡Los mensajes de %@ serán mostrados!"; +"Messages are protected by **end-to-end encryption**." = "Los mensajes están protegidos mediante **cifrado de extremo a extremo**."; + +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "¡Los mensajes nuevos 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"; @@ -3073,7 +3432,7 @@ "Messages sent" = "Mensajes enviados"; /* alert message */ -"Messages were deleted after you selected them." = "Los mensajes han sido borrados después de seleccionarlos."; +"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."; @@ -3138,9 +3497,15 @@ /* 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!"; @@ -3156,12 +3521,12 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Múltiples perfiles"; -/* No comment provided by engineer. */ -"mute" = "silenciar"; - -/* swipe action */ +/* notification label action */ "Mute" = "Silenciar"; +/* notification label action */ +"Mute all" = "Silenciar todo"; + /* No comment provided by engineer. */ "Muted when inactive!" = "¡Silenciado cuando está inactivo!"; @@ -3189,12 +3554,15 @@ /* No comment provided by engineer. */ "Network settings" = "Configuración de red"; -/* No comment provided by engineer. */ +/* alert title */ "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"; @@ -3216,6 +3584,9 @@ /* notification */ "New events" = "Eventos nuevos"; +/* No comment provided by engineer. */ +"New group role: Moderator" = "Nuevo rol de grupo: Moderador"; + /* No comment provided by engineer. */ "New in %@" = "Nuevo en %@"; @@ -3225,6 +3596,9 @@ /* No comment provided by engineer. */ "New member role" = "Nuevo rol de miembro"; +/* rcv group event chat item */ +"New member wants to join the group." = "Un miembro nuevo desea unirse al grupo."; + /* notification */ "new message" = "mensaje nuevo"; @@ -3244,7 +3618,7 @@ "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 por cada servidor."; +"New SOCKS credentials will be used for each server." = "Se usarán credenciales SOCKS nuevas para cada servidor."; /* pref value */ "no" = "no"; @@ -3255,6 +3629,18 @@ /* 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 chats with members" = "Sin chats"; + /* No comment provided by engineer. */ "No contacts selected" = "Ningún contacto seleccionado"; @@ -3286,10 +3672,13 @@ "No info, try to reload" = "No hay información, intenta recargar"; /* servers error */ -"No media & file servers." = "Ningún servidor de archivos y multimedia."; +"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." = "Ningún servidor de mensajes."; +"No message servers." = "Sin servidores para mensajes."; /* No comment provided by engineer. */ "No network connection" = "Sin conexión de red"; @@ -3303,33 +3692,51 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Sin permiso para grabar mensajes de voz"; +/* alert title */ +"No private routing session" = "Ninguna sesión con enrutamiento privado"; + /* No comment provided by engineer. */ -"No push server" = "Ningún servidor push"; +"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." = "Ningún servidor para enrutamiento privado."; +"No servers for private message routing." = "Sin servidores para enrutamiento privado."; /* servers error */ -"No servers to receive files." = "Ningún servidor para recibir archivos."; +"No servers to receive files." = "Sin servidores para recibir archivos."; /* servers error */ -"No servers to receive messages." = "Ningún servidor para recibir mensajes."; +"No servers to receive messages." = "Sin servidores para recibir mensajes."; /* servers error */ -"No servers to send files." = "Ningún servidor para enviar archivos."; +"No servers to send files." = "Sin servidores para enviar archivos."; + +/* No comment provided by engineer. */ +"no subscription" = "sin suscripciones"; /* 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. */ +"not synchronized" = "no sincronizado"; + +/* No comment provided by engineer. */ +"Notes" = "Notas"; + /* No comment provided by engineer. */ "Nothing selected" = "Nada seleccionado"; @@ -3342,9 +3749,15 @@ /* 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\")"; @@ -3352,8 +3765,9 @@ "observer" = "observador"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "desactivado"; /* blur media */ @@ -3365,7 +3779,9 @@ /* feature offered item */ "offered %@: %@" = "ofrecido %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3396,7 +3812,7 @@ "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" = "Sólo borrar la conversación"; +"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."; @@ -3407,6 +3823,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."; @@ -3419,6 +3841,9 @@ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "Sólo tú puedes enviar mensajes temporales."; +/* No comment provided by engineer. */ +"Only you can send files and media." = "Sólo tú puedes enviar archivos y multimedia."; + /* No comment provided by engineer. */ "Only you can send voice messages." = "Sólo tú puedes enviar mensajes de voz."; @@ -3435,32 +3860,62 @@ "Only your contact can send disappearing messages." = "Sólo tu contacto puede enviar mensajes temporales."; /* No comment provided by engineer. */ -"Only your contact can send voice messages." = "Sólo tu contacto puede enviar mensajes de voz."; +"Only your contact can send files and media." = "Sólo tu contacto puede enviar archivos y multimedia."; /* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Sólo tu contacto puede enviar mensajes de voz."; + +/* alert action */ "Open" = "Abrir"; /* No comment provided by engineer. */ "Open changes" = "Abrir cambios"; -/* No comment provided by engineer. */ +/* new chat action */ "Open chat" = "Abrir chat"; /* authentication reason */ "Open chat console" = "Abrir consola de Chat"; +/* alert action */ +"Open clean link" = "Abrir enlace limpio"; + /* No comment provided by engineer. */ "Open conditions" = "Abrir condiciones"; -/* No comment provided by engineer. */ +/* alert action */ +"Open full link" = "Abrir enlace completo"; + +/* new chat action */ "Open group" = "Grupo abierto"; +/* alert title */ +"Open link?" = "¿Abrir enlace?"; + /* authentication reason */ "Open migration to another device" = "Abrir menú migración a otro dispositivo"; +/* new chat action */ +"Open new chat" = "Abrir chat nuevo"; + +/* new chat action */ +"Open new group" = "Abrir grupo nuevo"; + /* No comment provided by engineer. */ "Open Settings" = "Abrir Configuración"; +/* No comment provided by engineer. */ +"Open to accept" = "Abrir para aceptar"; + +/* No comment provided by engineer. */ +"Open to connect" = "Abrir para conectar"; + +/* No comment provided by engineer. */ +"Open to join" = "Abre para unirte"; + +/* No comment provided by engineer. */ +"Open to use bot" = "Abre para usar el bot"; + /* No comment provided by engineer. */ "Opening app…" = "Iniciando aplicación…"; @@ -3483,11 +3938,14 @@ "Or securely share this file link" = "O comparte de forma segura este enlace al archivo"; /* No comment provided by engineer. */ -"Or show this code" = "O muestra este código QR"; +"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"; @@ -3527,9 +3985,6 @@ /* No comment provided by engineer. */ "Password to show" = "Contraseña para hacerlo visible"; -/* past/unknown group member */ -"Past member %@" = "Miembro pasado %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Pegar dirección de ordenador"; @@ -3545,9 +4000,18 @@ /* No comment provided by engineer. */ "peer-to-peer" = "p2p"; +/* No comment provided by engineer. */ +"pending" = "pendiente"; + /* No comment provided by engineer. */ "Pending" = "Pendientes"; +/* No comment provided by engineer. */ +"pending approval" = "pendiente de aprobación"; + +/* No comment provided by engineer. */ +"pending review" = "pendiente de revisión"; + /* No comment provided by engineer. */ "Periodic" = "Periódicamente"; @@ -3578,7 +4042,7 @@ /* 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."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Comprueba tu conexión de red con %@ e inténtalo de nuevo."; /* No comment provided by engineer. */ @@ -3600,7 +4064,7 @@ "Please enter the previous password after restoring database backup. This action can not be undone." = "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."; /* No comment provided by engineer. */ -"Please remember or store it securely - there is no way to recover a lost passcode!" = "Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay forma de recuperar un código perdido!"; +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay manera de recuperar un código perdido!"; /* No comment provided by engineer. */ "Please report it to the developers." = "Por favor, informa a los desarrolladores."; @@ -3609,10 +4073,22 @@ "Please restart the app and migrate the database to enable push notifications." = "Reinicia la aplicación y migra la base de datos para activar las notificaciones automáticas."; /* No comment provided by engineer. */ -"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Guarda la contraseña de forma segura, NO podrás acceder al chat si la pierdes."; +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Guarda la contraseña de forma segura, NO podrás acceder al chat si se pierde."; /* 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."; +"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 se pierde."; + +/* token info */ +"Please try to disable and re-enable notfications." = "Por favor, intenta desactivar y reactivar las notificaciones."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo."; + +/* 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"; @@ -3620,9 +4096,6 @@ /* No comment provided by engineer. */ "Port" = "Puerto"; -/* server test error */ -"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."; @@ -3644,12 +4117,21 @@ /* 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"; @@ -3662,9 +4144,12 @@ /* No comment provided by engineer. */ "Private routing" = "Enrutamiento privado"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Error de enrutamiento privado"; +/* alert title */ +"Private routing timeout" = "Timeout enrutamiento privado"; + /* No comment provided by engineer. */ "Profile and server connections" = "Eliminar perfil y conexiones"; @@ -3695,6 +4180,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."; @@ -3711,7 +4199,7 @@ "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"; @@ -3722,6 +4210,9 @@ /* No comment provided by engineer. */ "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 background timeout" = "Timeout protocolo en segundo plano"; + /* No comment provided by engineer. */ "Protocol timeout" = "Timeout protocolo"; @@ -3753,7 +4244,7 @@ "Rate the app" = "Valora la aplicación"; /* No comment provided by engineer. */ -"Reachable chat toolbar" = "Barra de herramientas accesible"; +"Reachable chat toolbar" = "Barra de menú accesible"; /* chat item menu */ "React…" = "Reacciona…"; @@ -3794,9 +4285,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "confirmación recibida…"; -/* notification */ -"Received file event" = "Evento de archivo recibido"; - /* message info title */ "Received message" = "Mensaje entrante"; @@ -3840,7 +4328,7 @@ "Reconnect all servers?" = "¿Reconectar todos los servidores?"; /* No comment provided by engineer. */ -"Reconnect server to force message delivery. It uses additional traffic." = "Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional."; +"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?"; @@ -3857,16 +4345,32 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Reducción del uso de batería"; -/* reject incoming call via notification - swipe action */ +/* No comment provided by engineer. */ +"Register" = "Registrar"; + +/* token info */ +"Register notification token?" = "¿Registrar el token de notificaciones?"; + +/* token status text */ +"Registered" = "Registrado"; + +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Rechazar"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Rechazar contacto (NO se notifica al remitente)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Rechazar solicitud de contacto"; +/* alert title */ +"Reject member?" = "¿Rechazar al miembro?"; + +/* No comment provided by engineer. */ +"rejected" = "rechazado"; + /* call status */ "rejected call" = "llamada rechazada"; @@ -3885,6 +4389,9 @@ /* No comment provided by engineer. */ "Remove image" = "Eliminar imagen"; +/* No comment provided by engineer. */ +"Remove link tracking" = "Limpiar enlaces de seguimiento"; + /* No comment provided by engineer. */ "Remove member" = "Expulsar miembro"; @@ -3903,12 +4410,18 @@ /* profile update event chat item */ "removed contact address" = "dirección de contacto eliminada"; +/* No comment provided by engineer. */ +"removed from group" = "expulsado del grupo"; + /* profile update event chat item */ "removed profile picture" = "ha eliminado la imagen del perfil"; /* rcv group event chat item */ "removed you" = "te ha expulsado"; +/* No comment provided by engineer. */ +"Removes messages and blocks members." = "Elimina mensajes y bloquea miembros."; + /* No comment provided by engineer. */ "Renegotiate" = "Renegociar"; @@ -3918,24 +4431,63 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "¿Renegociar cifrado?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "¿Repetir solicitud de conexión?"; - /* No comment provided by engineer. */ "Repeat download" = "Repetir descarga"; /* No comment provided by engineer. */ "Repeat import" = "Repetir importación"; -/* No comment provided by engineer. */ -"Repeat join request?" = "¿Repetir solicitud de admisión?"; - /* No comment provided by engineer. */ "Repeat upload" = "Repetir subida"; /* chat item action */ "Reply" = "Responder"; +/* chat item action */ +"Report" = "Informe"; + +/* report reason */ +"Report content: only group moderators will see it." = "Informar de contenido: sólo los moderadores del grupo lo verán."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Informar del perfil de un miembro: sólo los moderadores del grupo lo verán."; + +/* report reason */ +"Report other: only group moderators will see it." = "Informar de otros: sólo los moderadores del grupo lo verán."; + +/* No comment provided by engineer. */ +"Report reason?" = "¿Motivo del informe?"; + +/* alert title */ +"Report sent to moderators" = "Informe enviado a los moderadores"; + +/* 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"; + +/* No comment provided by engineer. */ +"request is sent" = "petición enviada"; + +/* No comment provided by engineer. */ +"request to join rejected" = "petición para unirse rechazada"; + +/* rcv group event chat item */ +"requested connection" = "conexión solicitada"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "conexión solicitada desde el grupo %@"; + /* chat list item title */ "requested to connect" = "solicitado para conectar"; @@ -3984,17 +4536,29 @@ /* No comment provided by engineer. */ "Restore database error" = "Error al restaurar base de datos"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Reintentar"; /* chat item action */ "Reveal" = "Revelar"; +/* No comment provided by engineer. */ +"review" = "por revisar"; + /* No comment provided by engineer. */ "Review conditions" = "Revisar condiciones"; /* No comment provided by engineer. */ -"Review later" = "Revisar más tarde"; +"Review group members" = "Revisa los miembros del grupo"; + +/* admission stage */ +"Review members" = "Revisar miembros"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Revisa a los miembros antes de admitirles en el grupo."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "en revisión por los administradores"; /* No comment provided by engineer. */ "Revoke" = "Revocar"; @@ -4018,12 +4582,18 @@ "Safer groups" = "Grupos más seguros"; /* alert button - chat item action */ +chat item action */ "Save" = "Guardar"; /* alert button */ "Save (and notify contacts)" = "Guardar (y notificar contactos)"; +/* alert button */ +"Save (and notify members)" = "Guardar (y notificar miembros)"; + +/* alert title */ +"Save admission settings?" = "¿Guardar configuración?"; + /* alert button */ "Save and notify contact" = "Guardar y notificar contacto"; @@ -4039,6 +4609,12 @@ /* No comment provided by engineer. */ "Save group profile" = "Guardar perfil de grupo"; +/* alert title */ +"Save group profile?" = "¿Guardar perfil del 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"; @@ -4103,7 +4679,7 @@ "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"; @@ -4175,10 +4751,10 @@ "Send a live message - it will update for the recipient(s) as you type it" = "Envía un mensaje en vivo: se actualizará para el (los) destinatario(s) a medida que se escribe"; /* No comment provided by engineer. */ -"Send delivery receipts to" = "Enviar confirmaciones de entrega a"; +"Send contact request?" = "¿Enviar solicitud de contacto?"; /* No comment provided by engineer. */ -"send direct message" = "Enviar mensaje directo"; +"Send delivery receipts to" = "Enviar confirmaciones de entrega a"; /* No comment provided by engineer. */ "Send direct message to connect" = "Envía un mensaje para conectar"; @@ -4207,18 +4783,30 @@ /* No comment provided by engineer. */ "Send notifications" = "Enviar notificaciones"; +/* No comment provided by engineer. */ +"Send private reports" = "Envía informes privados"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Consultas y sugerencias"; /* No comment provided by engineer. */ "Send receipts" = "Enviar confirmaciones"; +/* No comment provided by engineer. */ +"Send request" = "Enviar solicitud"; + +/* No comment provided by engineer. */ +"Send request without message" = "Enviar solicitud sin mensaje"; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Envíalos desde la galería o desde teclados personalizados."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Se envían hasta 100 mensajes más recientes a los miembros nuevos."; +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Envía tu comentario privado a los grupos."; + /* alert message */ "Sender cancelled file transfer." = "El remitente ha cancelado la transferencia de archivos."; @@ -4258,9 +4846,6 @@ /* No comment provided by engineer. */ "Sent directly" = "Directamente"; -/* notification */ -"Sent file event" = "Evento de archivo enviado"; - /* message info title */ "Sent message" = "Mensaje saliente"; @@ -4307,10 +4892,10 @@ "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"; +"Server requires authorization to create queues, check password." = "El servidor requiere autorización para crear colas, comprueba la contraseña."; /* server test error */ -"Server requires authorization to upload, check password" = "El servidor requiere autorización para subir, comprueba la contraseña"; +"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!" = "¡Prueba no superada!"; @@ -4331,7 +4916,7 @@ "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 podrá deshacerse!"; +"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"; @@ -4339,6 +4924,9 @@ /* 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…"; @@ -4351,6 +4939,12 @@ /* 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 member admission" = "Admisión miembro"; + +/* 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"; @@ -4366,6 +4960,9 @@ /* No comment provided by engineer. */ "Set passphrase to export" = "Escribe la contraseña para exportar"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Añade mensaje de bienvenida y biografía del perfil."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "¡Guarda un mensaje para ser mostrado a los miembros nuevos!"; @@ -4382,7 +4979,7 @@ "Shape profile images" = "Dar forma a las imágenes de perfil"; /* alert action - chat item action */ +chat item action */ "Share" = "Compartir"; /* No comment provided by engineer. */ @@ -4406,8 +5003,14 @@ /* No comment provided by engineer. */ "Share link" = "Compartir enlace"; +/* alert button */ +"Share old address" = "Compartir dirección antigua"; + +/* alert button */ +"Share old link" = "Comparte enlace completo"; + /* No comment provided by engineer. */ -"Share profile" = "Comparte perfil"; +"Share profile" = "Perfil a compartir"; /* No comment provided by engineer. */ "Share SimpleX address on social media." = "Comparte tu dirección SimpleX en redes sociales."; @@ -4421,6 +5024,18 @@ /* No comment provided by engineer. */ "Share with contacts" = "Compartir con contactos"; +/* No comment provided by engineer. */ +"Share your address" = "Comparte tu dirección"; + +/* No comment provided by engineer. */ +"Short description" = "Descripción corta"; + +/* No comment provided by engineer. */ +"Short link" = "Enlace corto"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Direcciones SimpleX cortas"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Mostrar → en mensajes con enrutamiento privado."; @@ -4446,7 +5061,7 @@ "Show QR code" = "Mostrar código QR"; /* No comment provided by engineer. */ -"Show:" = "Mostrar:"; +"Show:" = "Muestra:"; /* No comment provided by engineer. */ "SimpleX" = "SimpleX"; @@ -4458,13 +5073,19 @@ "SimpleX Address" = "Dirección SimpleX"; /* No comment provided by engineer. */ -"SimpleX address and 1-time links are safe to share via any messenger." = "Compartir los enlaces de un uso y las direcciones SimpleX es seguro a través de cualquier medio."; +"SimpleX address and 1-time links are safe to share via any messenger." = "Compartir enlaces de un solo uso y direcciones SimpleX es seguro a través de cualquier medio."; /* No comment provided by engineer. */ -"SimpleX address or 1-time link?" = "¿Dirección SimpleX o enlace de un uso?"; +"SimpleX address or 1-time link?" = "¿Dirección SimpleX o enlace de un solo uso?"; + +/* alert title */ +"SimpleX address settings" = "Auto aceptar configuración"; + +/* 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 servidores operados por Flux en la aplicación"; +"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."; @@ -4505,6 +5126,9 @@ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protocolos de SimpleX auditados por Trail of Bits."; +/* simplex link type */ +"SimpleX relay link" = "Enlace de servidor SimpleX"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Modo incógnito simplificado"; @@ -4518,7 +5142,7 @@ "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 server" = "Servidor SMP"; @@ -4547,6 +5171,10 @@ /* 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."; @@ -4578,7 +5206,7 @@ "Stop 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 SimpleX. 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?"; @@ -4604,6 +5232,9 @@ /* No comment provided by engineer. */ "Stopping chat" = "Parando chat"; +/* No comment provided by engineer. */ +"Storage" = "Almacenamiento"; + /* No comment provided by engineer. */ "strike" = "tachado"; @@ -4614,7 +5245,7 @@ "Submit" = "Enviar"; /* No comment provided by engineer. */ -"Subscribed" = "Suscrito"; +"Subscribed" = "Suscritas"; /* No comment provided by engineer. */ "Subscription errors" = "Errores de suscripción"; @@ -4641,14 +5272,26 @@ "Tail" = "Cola"; /* No comment provided by engineer. */ -"Take picture" = "Tomar foto"; +"Take picture" = "Hacer foto"; /* No comment provided by engineer. */ "Tap button " = "Pulsa el botón "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "Pulsa Conectar para chatear"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "Pulsa Conectar para enviar solicitud"; + +/* No comment provided by engineer. */ +"Tap Connect to use bot" = "Pulsa Conectar para usar el bot"; + /* No comment provided by engineer. */ "Tap Create SimpleX address in the menu to create it later." = "Pulsa Crear dirección SimpleX en el menú para crearla más tarde."; +/* No comment provided by engineer. */ +"Tap Join group" = "Pulsa Unirme al grupo"; + /* No comment provided by engineer. */ "Tap to activate profile." = "Pulsa sobre un perfil para activarlo."; @@ -4662,7 +5305,7 @@ "Tap to join incognito" = "Pulsa para unirte en modo incógnito"; /* No comment provided by engineer. */ -"Tap to paste link" = "Pulsa para pegar el enlacePulsa para pegar enlace"; +"Tap to paste link" = "Pulsa aquí para pegar el enlace"; /* No comment provided by engineer. */ "Tap to scan" = "Pulsa para escanear"; @@ -4670,9 +5313,15 @@ /* No comment provided by engineer. */ "TCP connection" = "Conexión TCP"; +/* No comment provided by engineer. */ +"TCP connection bg timeout" = "Timeout conexión TCP sp"; + /* No comment provided by engineer. */ "TCP connection timeout" = "Timeout de la conexión TCP"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "Puerto TCP para mensajes"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4682,12 +5331,15 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* file error alert title */ "Temporary file error" = "Error en archivo temporal"; /* server test failure */ "Test failed at step %@." = "Prueba no superada en el paso %@."; +/* No comment provided by engineer. */ +"Test notifications" = "Probar notificaciones"; + /* No comment provided by engineer. */ "Test server" = "Probar servidor"; @@ -4704,7 +5356,10 @@ "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "¡Nuestro agradecimiento a todos los colaboradores, [puedes contribuir a través de Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)!"; /* No comment provided by engineer. */ -"Thanks to the users – contribute via Weblate!" = "¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate"; +"Thanks to the users – contribute via Weblate!" = "¡Agradecimiento a los colaboradores! Puedes contribuir a través de Weblate"; + +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "La dirección pasará a ser corta y tu perfil será compartido mediante la dirección."; /* 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."; @@ -4719,7 +5374,7 @@ "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."; @@ -4745,6 +5400,9 @@ /* 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." = "El ID del siguiente mensaje es incorrecto (menor o igual que el anterior).\nPuede ocurrir por algún bug o cuando la conexión está comprometida."; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "El enlace será corto y el perfil del grupo se compartirá mediante el enlace."; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "El mensaje se eliminará para todos los miembros."; @@ -4760,32 +5418,26 @@ /* 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."; - /* 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 same conditions will apply to operator(s): **%@**." = "Las mismas condiciones se aplicarán a el/los operador(es) **%@**."; - /* 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! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "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 del perfil **%@**."; +"The servers for new connections of your current chat profile **%@**." = "Servidores para conexiones nuevas en tu perfil **%@**."; /* No comment provided by engineer. */ -"The servers for new files of your current chat profile **%@**." = "Los servidores para archivos nuevos en tu perfil actual **%@**."; +"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."; +"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."; @@ -4808,6 +5460,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."; @@ -4824,7 +5479,7 @@ "This device name" = "Nombre del dispositivo"; /* No comment provided by engineer. */ -"This display name is invalid. Please choose another name." = "Éste nombre mostrado no es válido. Por favor, elije otro nombre."; +"This display name is invalid. Please choose another name." = "Éste nombre mostrado no es válido. Por favor, elige otro nombre."; /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Este grupo tiene más de %lld miembros, no se enviarán confirmaciones de entrega."; @@ -4833,17 +5488,23 @@ "This group no longer exists." = "Este grupo ya no existe."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "¡Este es tu propio enlace de un solo uso!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "¡Esta es tu propia dirección SimpleX!"; +"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. */ +"This setting is for your current profile **%@**." = "Esta configuración se aplica al perfil actual **%@**."; + +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Mensajes temporales activados sólo para los contactos nuevos."; + /* No comment provided by engineer. */ "Title" = "Título"; @@ -4872,7 +5533,7 @@ "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, 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 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"; @@ -4892,11 +5553,17 @@ /* No comment provided by engineer. */ "To send" = "Para enviar"; +/* alert message */ +"To send commands you must be connected." = "Para enviar comandos debes estar conectado."; + /* 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."; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "Para usar otro perfil tras el intento de conexión, elimina el chat y usa el enlace de nuevo."; + /* No comment provided by engineer. */ -"To use the servers of **%@**, accept conditions of use." = "Para usar los servidores de **%@**, acepta las condiciones de uso."; +"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."; @@ -4907,6 +5574,9 @@ /* 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"; @@ -4919,11 +5589,8 @@ /* 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: %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Intentando conectar con el servidor usado para recibir mensajes de este contacto."; +/* subscription status explanation */ +"Trying to connect to the server used to receive messages from this connection." = "Intentando conectar con el servidor usado para recibir mensajes de esta conexión."; /* No comment provided by engineer. */ "Turkish interface" = "Interfaz en turco"; @@ -4947,7 +5614,7 @@ "Unblock member" = "Desbloquear miembro"; /* No comment provided by engineer. */ -"Unblock member for all?" = "¿Desbloquear miembro para todos?"; +"Unblock member for all?" = "¿Desbloquear al miembro para todos?"; /* No comment provided by engineer. */ "Unblock member?" = "¿Desbloquear miembro?"; @@ -5001,7 +5668,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 el enlace haya sido 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."; +"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"; @@ -5015,10 +5682,7 @@ /* authentication reason */ "Unlock app" = "Desbloquear aplicación"; -/* No comment provided by engineer. */ -"unmute" = "activar sonido"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Activar audio"; /* No comment provided by engineer. */ @@ -5027,6 +5691,9 @@ /* 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."; @@ -5042,6 +5709,9 @@ /* No comment provided by engineer. */ "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"; @@ -5049,11 +5719,29 @@ "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."; +"Updating settings will re-connect the client to all servers." = "Para actualizar la configuración el cliente se reconectará a todos los servidores."; + +/* alert button */ +"Upgrade" = "Actualizar"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Actualizar dirección"; + +/* alert message */ +"Upgrade address?" = "¿Actualizar la dirección?"; /* No comment provided by engineer. */ "Upgrade and open chat" = "Actualizar y abrir Chat"; +/* alert message */ +"Upgrade group link?" = "¿Actualizar enlace de grupo?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Añadir enlace"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Actualiza tu dirección"; + /* No comment provided by engineer. */ "Upload errors" = "Errores en subida"; @@ -5081,25 +5769,28 @@ /* No comment provided by engineer. */ "Use chat" = "Usar Chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Usar perfil actual"; /* No comment provided by engineer. */ -"Use for files" = "Usar para archivos"; +"Use for files" = "Uso para archivos"; /* No comment provided by engineer. */ -"Use for messages" = "Usar para mensajes"; +"Use for messages" = "Uso para mensajes"; /* No comment provided by engineer. */ -"Use for new connections" = "Usar para conexiones nuevas"; +"Use for new connections" = "Para conexiones nuevas"; /* No comment provided by engineer. */ "Use from desktop" = "Usar desde ordenador"; /* No comment provided by engineer. */ -"Use iOS call interface" = "Usar interfaz de llamada de iOS"; +"Use incognito profile" = "Usar perfil incógnito"; /* No comment provided by engineer. */ +"Use iOS call interface" = "Usar interfaz de llamada de iOS"; + +/* new chat action */ "Use new incognito profile" = "Usar nuevo perfil incógnito"; /* No comment provided by engineer. */ @@ -5109,7 +5800,7 @@ "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 retransmisión desconocidos."; +"Use private routing with unknown servers." = "Usar enrutamiento privado con servidores de mensaje desconocidos."; /* No comment provided by engineer. */ "Use server" = "Usar servidor"; @@ -5123,12 +5814,21 @@ /* 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. */ "Use the app with one hand." = "Usa la aplicación con una sola mano."; +/* No comment provided by engineer. */ +"Use web port" = "Usar puerto web"; + /* No comment provided by engineer. */ "User selection" = "Selección de usuarios"; @@ -5279,6 +5979,9 @@ /* No comment provided by engineer. */ "Welcome message is too long" = "Mensaje de bienvenida demasiado largo"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Da la bienvenida a tus contactos 👋"; + /* No comment provided by engineer. */ "What's new" = "Novedades"; @@ -5348,6 +6051,9 @@ /* No comment provided by engineer. */ "You accepted connection" = "Has aceptado la conexión"; +/* snd group event chat item */ +"you accepted this member" = "has aceptado al miembro"; + /* No comment provided by engineer. */ "You allow" = "Permites"; @@ -5360,38 +6066,35 @@ /* No comment provided by engineer. */ "You are already connected with %@." = "Ya estás conectado con %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Ya estás conectando con %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "¡Ya estás conectando mediante este enlace de un solo uso!"; /* No comment provided by engineer. */ "You are already in group %@." = "Ya estás en el grupo %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Ya estás uniéndote al grupo %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "¡Ya estás uniéndote al grupo mediante este enlace!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Ya estás uniéndote al grupo mediante este enlace."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "¡En proceso de unirte al grupo!\n¿Repetir solicitud de admisión?"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Estás conectado al servidor usado para recibir mensajes de este contacto."; - -/* No comment provided by engineer. */ -"you are invited to group" = "has sido invitado a un grupo"; +/* subscription status explanation */ +"You are connected to the server used to receive messages from this connection." = "Estás conectado al servidor usado para recibir mensajes de esta conexión."; /* No comment provided by engineer. */ "You are invited to group" = "Has sido invitado a un grupo"; +/* subscription status explanation */ +"You are not connected to the server used to receive messages from this connection (no subscription)." = "No estás conectado al servidor usado para recibir mensajes de esta conexión (no suscrito)."; + /* No comment provided by engineer. */ -"You are not connected to these servers. Private routing is used to deliver messages to them." = "No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado."; +"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"; @@ -5405,9 +6108,6 @@ /* 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 operators in Network & servers settings." = "Puedes configurar los operadores desde Servidores y Redes."; - /* No comment provided by engineer. */ "You can configure servers via settings." = "Puedes configurar los servidores a través de su configuración."; @@ -5442,7 +6142,7 @@ "You can set lock screen notification preview via settings." = "Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración."; /* No comment provided by engineer. */ -"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Puedes compartir un enlace o código QR para que cualquiera pueda unirse al grupo. Si decides eliminarlo más tarde, los miembros del grupo se mantendrán."; +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Puedes compartir el enlace o el código QR para que cualquiera pueda unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo."; /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Puedes compartir esta dirección con tus contactos para que puedan conectar con **%@**."; @@ -5460,9 +6160,12 @@ "You can use markdown to format messages:" = "Puedes usar la sintaxis markdown para dar formato a tus mensajes:"; /* alert message */ -"You can view invitation link again in connection details." = "Podrás ver el enlace de invitación en detalles de conexión."; +"You can view invitation link again in connection details." = "Puedes ver el enlace de invitación de nuevo en los detalles de la conexión."; -/* No comment provided by engineer. */ +/* alert message */ +"You can view your reports in Chat with admins." = "Puedes ver tus informes en Chat con administradores."; + +/* alert title */ "You can't send messages!" = "¡No puedes enviar mensajes!"; /* chat item text */ @@ -5483,10 +6186,7 @@ /* 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!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Ya has solicitado la conexión\n¿Repetir solicitud?"; /* No comment provided by engineer. */ @@ -5514,7 +6214,7 @@ "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." = "Necesitas permitir que tus contacto llamen para poder llamarles."; +"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."; @@ -5534,9 +6234,15 @@ /* 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 able to send messages **only after your request is accepted**." = "Podrás enviar mensajes **después de que tu solicitud sea aceptada**."; + /* 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 revisa más tarde."; @@ -5552,17 +6258,14 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Te conectarás con todos los miembros del grupo."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos."; /* No comment provided by engineer. */ -"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes de este chat. El historial del chat se conserva."; +"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes del chat. El historial del chat se conserva."; /* No comment provided by engineer. */ -"You will stop receiving messages from this group. Chat history will be preserved." = "Dejarás de recibir mensajes de este grupo. El historial del chat se conservará."; +"You will stop receiving messages from this group. Chat history will be preserved." = "Dejarás de recibir mensajes del grupo. El historial del chat se conservará."; /* No comment provided by engineer. */ "You won't lose your contacts if you later delete your address." = "Si lo eliminas más tarde tus contactos no se perderán."; @@ -5576,6 +6279,9 @@ /* 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 business contact" = "Mi contacto empresarial"; + /* No comment provided by engineer. */ "Your calls" = "Llamadas"; @@ -5591,8 +6297,14 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Mis perfiles"; +/* alert message */ +"Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Tu chat ha sido movido a %@ pero ha ocurrido un error inesperado mientras se te redirigía al perfil."; + /* No comment provided by engineer. */ -"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."; +"Your connection was moved to %@ but an error happened when switching 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" = "Mi contacto"; /* 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 (%@)."; @@ -5612,6 +6324,9 @@ /* No comment provided by engineer. */ "Your current profile" = "Tu perfil actual"; +/* No comment provided by engineer. */ +"Your group" = "Mi grupo"; + /* No comment provided by engineer. */ "Your ICE servers" = "Servidores ICE"; @@ -5628,14 +6343,14 @@ "Your profile **%@** will be shared." = "El perfil **%@** será compartido."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil."; +"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 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 se comparte sólo 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."; - /* No comment provided by engineer. */ "Your random profile" = "Tu perfil aleatorio"; @@ -5651,6 +6366,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Mi dirección SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Servidores SMP"; - diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index f0987f3e1b..884be40cc1 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (voidaan kopioida)"; @@ -16,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)"; @@ -160,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"; @@ -205,7 +181,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 päivä"; /* time interval */ @@ -214,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. */ @@ -260,8 +239,9 @@ "above, then choose:" = "edellä, valitse sitten:"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Hyväksy"; /* No comment provided by engineer. */ @@ -270,8 +250,8 @@ /* notification body */ "Accept contact request from %@?" = "Hyväksy kontaktipyyntö %@:ltä?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Hyväksy tuntematon"; /* call status */ @@ -530,7 +510,8 @@ "Can't invite contacts!" = "Kontakteja ei voi kutsua!"; /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Peruuta"; /* feature offered item */ @@ -570,7 +551,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 */ @@ -672,16 +653,13 @@ /* server test step */ "Connect" = "Yhdistä"; -/* No comment provided by engineer. */ -"Connect incognito" = "Yhdistä Incognito"; - /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "ole yhteydessä SimpleX Chat -kehittäjiin."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Yhdistä linkin kautta"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Yhdistä kertalinkillä"; /* No comment provided by engineer. */ @@ -717,7 +695,7 @@ /* No comment provided by engineer. */ "Connection" = "Yhteys"; -/* No comment provided by engineer. */ +/* alert title */ "Connection error" = "Yhteysvirhe"; /* No comment provided by engineer. */ @@ -729,7 +707,7 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Yhteyspyyntö lähetetty!"; -/* No comment provided by engineer. */ +/* alert title */ "Connection timeout" = "Yhteyden aikakatkaisu"; /* connection information */ @@ -789,12 +767,12 @@ /* 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"; -/* No comment provided by engineer. */ -"Create secret group" = "Luo salainen ryhmä"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Luo SimpleX-osoite"; @@ -888,7 +866,8 @@ /* message decrypt error item */ "Decryption error" = "Salauksen purkuvirhe"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "oletusarvo (%@)"; /* No comment provided by engineer. */ @@ -898,8 +877,7 @@ "default (yes)" = "oletusarvo (kyllä)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Poista"; /* No comment provided by engineer. */ @@ -965,7 +943,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. */ @@ -1091,7 +1069,7 @@ /* No comment provided by engineer. */ "Don't enable" = "Älä salli"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Älä näytä uudelleen"; /* No comment provided by engineer. */ @@ -1124,7 +1102,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. */ @@ -1265,7 +1243,7 @@ /* No comment provided by engineer. */ "Error changing role" = "Virhe roolin vaihdossa"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Virhe asetuksen muuttamisessa"; /* No comment provided by engineer. */ @@ -1283,19 +1261,19 @@ /* No comment provided by engineer. */ "Error decrypting file" = "Virhe tiedoston salauksen purussa"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Virhe keskustelujen tietokannan poistamisessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Virhe keskutelun poistamisessa!"; /* No comment provided by engineer. */ "Error deleting connection" = "Virhe yhteyden poistamisessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Virhe tietokannan poistamisessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Virhe vanhan tietokannan poistamisessa"; /* No comment provided by engineer. */ @@ -1313,10 +1291,10 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Virhe tietokannan salauksessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Virhe vietäessä keskustelujen tietokantaa"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Virhe keskustelujen tietokannan tuonnissa"; /* No comment provided by engineer. */ @@ -1325,7 +1303,7 @@ /* alert title */ "Error receiving file" = "Virhe tiedoston vastaanottamisessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Virhe poistettaessa jäsentä"; /* No comment provided by engineer. */ @@ -1379,7 +1357,9 @@ /* No comment provided by engineer. */ "Error: " = "Virhe: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Virhe: %@"; /* No comment provided by engineer. */ @@ -1391,9 +1371,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"; @@ -1451,6 +1428,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "Löydä keskustelut nopeammin"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; + /* No comment provided by engineer. */ "Fix" = "Korjaa"; @@ -1779,9 +1759,9 @@ "Join" = "Liity"; /* No comment provided by engineer. */ -"join as %@" = "Liity %@:nä"; +"Join as %@" = "Liity %@:nä"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Liity ryhmään"; /* No comment provided by engineer. */ @@ -1988,7 +1968,7 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Useita keskusteluprofiileja"; -/* swipe action */ +/* notification label action */ "Mute" = "Mykistä"; /* No comment provided by engineer. */ @@ -2003,10 +1983,10 @@ /* No comment provided by engineer. */ "Network settings" = "Verkkoasetukset"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "Verkon tila"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "ei koskaan"; /* notification */ @@ -2097,8 +2077,9 @@ "observer" = "tarkkailija"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "pois"; /* blur media */ @@ -2110,7 +2091,9 @@ /* feature offered item */ "offered %@: %@" = "tarjottu %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2173,7 +2156,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Vain kontaktisi voi lähettää ääniviestejä."; -/* No comment provided by engineer. */ +/* new chat action */ "Open chat" = "Avaa keskustelu"; /* authentication reason */ @@ -2227,7 +2210,7 @@ /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Tarkista verkkoyhteytesi %@:lla ja yritä uudelleen."; /* No comment provided by engineer. */ @@ -2260,9 +2243,6 @@ /* No comment provided by engineer. */ "Polish interface" = "Puolalainen käyttöliittymä"; -/* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Säilytä viimeinen viestiluonnos liitteineen."; @@ -2368,9 +2348,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "vahvistus saatu…"; -/* notification */ -"Received file event" = "Tiedoston vastaanottotapahtuma"; - /* message info title */ "Received message" = "Vastaanotettu viesti"; @@ -2401,14 +2378,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Pienempi akun käyttö"; -/* reject incoming call via notification - swipe action */ +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Hylkää"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Hylkää (lähettäjälle EI ilmoiteta)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Hylkää yhteyspyyntö"; /* call status */ @@ -2502,7 +2480,7 @@ "Run chat" = "Käynnistä chat"; /* alert button - chat item action */ +chat item action */ "Save" = "Tallenna"; /* alert button */ @@ -2661,9 +2639,6 @@ /* copied message info */ "Sent at: %@" = "Lähetetty klo: %@"; -/* notification */ -"Sent file event" = "Lähetetty tiedosto tapahtuma"; - /* message info title */ "Sent message" = "Lähetetty viesti"; @@ -2671,10 +2646,10 @@ "Sent messages will be deleted after set time." = "Lähetetyt viestit poistetaan asetetun ajan kuluttua."; /* server test error */ -"Server requires authorization to create queues, check password" = "Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana"; +"Server requires authorization to create queues, check password." = "Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana"; /* server test error */ -"Server requires authorization to upload, check password" = "Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana"; +"Server requires authorization to upload, check password." = "Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana"; /* No comment provided by engineer. */ "Server test failed!" = "Palvelintesti epäonnistui!"; @@ -2710,7 +2685,7 @@ "Settings" = "Asetukset"; /* alert action - chat item action */ +chat item action */ "Share" = "Jaa"; /* No comment provided by engineer. */ @@ -2929,13 +2904,10 @@ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa."; -/* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Toinen kuittaus, joka uupui! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Lähettäjälle EI ilmoiteta"; /* No comment provided by engineer. */ @@ -3001,12 +2973,6 @@ /* No comment provided by engineer. */ "Transport isolation" = "Kuljetuksen eristäminen"; -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Yritetään muodostaa yhteyttä palvelimeen, jota käytetään tämän kontaktin viestien vastaanottamiseen (virhe: %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta."; - /* No comment provided by engineer. */ "Turn off" = "Sammuta"; @@ -3058,7 +3024,7 @@ /* authentication reason */ "Unlock app" = "Avaa sovellus"; -/* swipe action */ +/* notification label action */ "Unmute" = "Poista mykistys"; /* swipe action */ @@ -3091,7 +3057,7 @@ /* No comment provided by engineer. */ "Use chat" = "Käytä chattia"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Käytä nykyistä profiilia"; /* No comment provided by engineer. */ @@ -3100,7 +3066,7 @@ /* No comment provided by engineer. */ "Use iOS call interface" = "Käytä iOS:n puhelujen käyttöliittymää"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Käytä uutta incognito-profiilia"; /* No comment provided by engineer. */ @@ -3235,12 +3201,6 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Olet jo muodostanut yhteyden %@:n kanssa."; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta."; - -/* No comment provided by engineer. */ -"you are invited to group" = "sinut on kutsuttu ryhmään"; - /* No comment provided by engineer. */ "You are invited to group" = "Sinut on kutsuttu ryhmään"; @@ -3283,7 +3243,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Voit käyttää markdownia viestien muotoiluun:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Et voi lähettää viestejä!"; /* chat item text */ @@ -3410,10 +3370,10 @@ "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. 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 random profile" = "Satunnainen profiilisi"; @@ -3427,6 +3387,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "SimpleX-osoitteesi"; -/* No comment provided by engineer. */ -"Your SMP servers" = "SMP-palvelimesi"; - diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 6b973e75d0..dbfac375d1 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (peut être copié)"; @@ -22,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)"; @@ -82,6 +61,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**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."; @@ -142,6 +124,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ est vérifié·e"; +/* No comment provided by engineer. */ +"%@ server" = "Serveur %@"; + +/* No comment provided by engineer. */ +"%@ servers" = "Serveurs %@"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ envoyé"; @@ -190,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"; @@ -232,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"; @@ -280,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 jour"; /* time interval */ @@ -289,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"; @@ -328,6 +328,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Abandonner le changement d'adresse ?"; +/* No comment provided by engineer. */ +"About operators" = "À propos des opérateurs"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "À propos de SimpleX Chat"; @@ -338,35 +341,69 @@ "Accent" = "Principale"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Accepter"; +/* alert action */ +"Accept as member" = "Accepter en tant que membre"; + +/* alert action */ +"Accept as observer" = "Accepter en tant qu'observateur"; + +/* No comment provided by engineer. */ +"Accept conditions" = "Accepter les conditions"; + /* No comment provided by engineer. */ "Accept connection request?" = "Accepter la demande de connexion ?"; +/* alert title */ +"Accept contact request" = "Accepter la demande de contact"; + /* notification body */ "Accept contact request from %@?" = "Accepter la demande de contact de %@ ?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Accepter en incognito"; +/* alert title */ +"Accept member" = "Accepter le membre"; + /* 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 friends" = "Ajouter des amis"; + +/* No comment provided by engineer. */ +"Add list" = "Ajouter une liste"; + +/* placeholder for sending contact request */ +"Add message" = "Ajouter un message"; + /* No comment provided by engineer. */ "Add profile" = "Ajouter un profil"; @@ -376,12 +413,27 @@ /* 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"; @@ -397,6 +449,12 @@ /* 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"; @@ -421,12 +479,18 @@ /* 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."; @@ -439,6 +503,9 @@ /* 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 !"; @@ -451,6 +518,12 @@ /* 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 servers" = "Tous les serveurs"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tous vos contacts resteront connectés."; @@ -475,6 +548,9 @@ /* No comment provided by engineer. */ "Allow downgrade" = "Autoriser la rétrogradation"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Permettre des fichiers et des médias seulement si votre contact les permet."; + /* 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)"; @@ -496,6 +572,9 @@ /* 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."; @@ -523,16 +602,19 @@ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Autorise votre contact à envoyer des messages éphémères."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Permettre à vos contacts d'envoyer des fichiers et des médias."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Autorise vos contacts à envoyer des messages vocaux."; /* No comment provided by engineer. */ "Already connected?" = "Déjà connecté ?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Déjà en connexion !"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Groupe déjà rejoint !"; /* pref value */ @@ -550,6 +632,9 @@ /* 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"; @@ -592,12 +677,30 @@ /* No comment provided by engineer. */ "Apply to" = "Appliquer à"; +/* No comment provided by engineer. */ +"Archive" = "Archiver"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archiver les rapports %lld ?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archiver tous les rapports ?"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiver et téléverser"; /* No comment provided by engineer. */ "Archive contacts to chat later." = "Archiver les contacts pour discuter plus tard."; +/* No comment provided by engineer. */ +"Archive report" = "Archiver le rapport"; + +/* No comment provided by engineer. */ +"Archive report?" = "Archiver le rapport ?"; + +/* swipe action */ +"Archive reports" = "Archiver les rapports"; + /* No comment provided by engineer. */ "Archived contacts" = "Contacts archivés"; @@ -649,9 +752,6 @@ /* 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"; @@ -679,6 +779,9 @@ /* 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."; @@ -691,6 +794,9 @@ /* 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 ✅"; @@ -724,7 +830,8 @@ /* 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. */ @@ -757,9 +864,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)!" = "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"; @@ -800,7 +919,8 @@ "Can't message member" = "Impossible d'envoyer un message à ce membre"; /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Annuler"; /* No comment provided by engineer. */ @@ -827,6 +947,12 @@ /* 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 ?"; @@ -852,7 +978,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 */ @@ -870,6 +996,15 @@ /* chat item text */ "changing address…" = "changement d'adresse…"; +/* No comment provided by engineer. */ +"Chat" = "Discussions"; + +/* No comment provided by engineer. */ +"Chat already exists" = "La discussion existe déjà"; + +/* new chat sheet title */ +"Chat already exists!" = "La discussion existe déjà !"; + /* No comment provided by engineer. */ "Chat colors" = "Couleurs de chat"; @@ -915,9 +1050,21 @@ /* 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."; @@ -951,6 +1098,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 ?"; @@ -966,6 +1119,9 @@ /* 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"; @@ -978,9 +1134,33 @@ /* 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 : **%@**."; + +/* alert button */ +"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"; @@ -1011,15 +1191,15 @@ /* No comment provided by engineer. */ "Confirm upload" = "Confirmer la transmission"; +/* token status text */ +"Confirmed" = "Confirmé"; + /* server test step */ "Connect" = "Se connecter"; /* No comment provided by engineer. */ "Connect automatically" = "Connexion automatique"; -/* No comment provided by engineer. */ -"Connect incognito" = "Se connecter incognito"; - /* No comment provided by engineer. */ "Connect to desktop" = "Connexion au bureau"; @@ -1029,25 +1209,22 @@ /* 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 ?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Se connecter à soi-même ?\nIl s'agit de votre propre lien unique !"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Se connecter à soi-même ?\nC'est votre propre adresse SimpleX !"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Se connecter via l'adresse de contact"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Se connecter via un lien"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Se connecter via un lien unique"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Se connecter avec %@"; /* No comment provided by engineer. */ @@ -1059,9 +1236,6 @@ /* 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"; @@ -1111,6 +1285,9 @@ "Connection and servers status." = "État de la connexion et des serveurs."; /* No comment provided by engineer. */ +"Connection blocked" = "Connexion bloquée"; + +/* alert title */ "Connection error" = "Erreur de connexion"; /* No comment provided by engineer. */ @@ -1119,6 +1296,12 @@ /* 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"; @@ -1126,9 +1309,15 @@ "Connection request sent!" = "Demande de connexion envoyée !"; /* No comment provided by engineer. */ -"Connection terminated" = "Connexion terminée"; +"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"; + +/* alert title */ "Connection timeout" = "Délai de connexion"; /* No comment provided by engineer. */ @@ -1182,6 +1371,9 @@ /* 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"; @@ -1206,6 +1398,9 @@ /* No comment provided by engineer. */ "Create" = "Créer"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Créer un lien unique"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Création de groupes via un profil aléatoire."; @@ -1221,6 +1416,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/). 💻"; @@ -1230,9 +1428,6 @@ /* server test step */ "Create queue" = "Créer une file d'attente"; -/* No comment provided by engineer. */ -"Create secret group" = "Créer un groupe secret"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Créer une adresse SimpleX"; @@ -1257,6 +1452,9 @@ /* 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"; @@ -1359,7 +1557,8 @@ /* No comment provided by engineer. */ "decryption errors" = "Erreurs de déchiffrement"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "défaut (%@)"; /* No comment provided by engineer. */ @@ -1369,8 +1568,7 @@ "default (yes)" = "par défaut (oui)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Supprimer"; /* No comment provided by engineer. */ @@ -1394,12 +1592,21 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Supprimer et en informer le contact"; +/* No comment provided by engineer. */ +"Delete chat" = "Supprimer la discussion"; + +/* No comment provided by engineer. */ +"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"; /* 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"; @@ -1445,13 +1652,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. */ @@ -1475,6 +1685,9 @@ /* 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."; @@ -1505,6 +1718,9 @@ /* 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"; @@ -1571,12 +1787,21 @@ /* chat feature */ "Direct messages" = "Messages directs"; +/* No comment provided by engineer. */ +"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"; @@ -1637,6 +1862,9 @@ /* 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"; @@ -1644,13 +1872,19 @@ "Don't enable" = "Ne pas activer"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Ne manquez pas les messages importants."; + +/* alert action */ "Don't show again" = "Ne plus afficher"; +/* No comment provided by engineer. */ +"Done" = "Terminé"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Rétrograder et ouvrir le chat"; /* alert button - chat item action */ +chat item action */ "Download" = "Télécharger"; /* No comment provided by engineer. */ @@ -1692,6 +1926,9 @@ /* 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"; @@ -1704,12 +1941,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"; @@ -1821,6 +2061,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é"; @@ -1869,12 +2112,18 @@ /* 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 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"; @@ -1884,13 +2133,16 @@ /* No comment provided by engineer. */ "Error changing role" = "Erreur lors du changement de rôle"; -/* No comment provided by engineer. */ +/* alert title */ "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)"; + +/* alert message */ "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. */ @@ -1902,6 +2154,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"; @@ -1911,22 +2166,25 @@ /* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Erreur lors de la suppression de la base de données du chat"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Erreur lors de la suppression du chat !"; /* No comment provided by engineer. */ "Error deleting connection" = "Erreur lors de la suppression de la connexion"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Erreur lors de la suppression de la base de données"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Erreur lors de la suppression de l'ancienne base de données"; /* No comment provided by engineer. */ @@ -1947,18 +2205,21 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Erreur lors du chiffrement de la base de données"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "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 migrating settings" = "Erreur lors de la migration des paramètres"; @@ -1974,12 +2235,21 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Erreur de reconnexion des serveurs"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "Erreur lors de l'inscription aux notifications"; + +/* alert title */ "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 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"; @@ -1992,6 +2262,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"; @@ -2019,7 +2292,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Erreur lors de l'arrêt du chat"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Erreur lors du changement de profil"; /* alertTitle */ @@ -2028,12 +2301,18 @@ /* 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"; @@ -2049,7 +2328,9 @@ /* No comment provided by engineer. */ "Error: " = "Erreur : "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Erreur : %@"; /* No comment provided by engineer. */ @@ -2061,11 +2342,11 @@ /* No comment provided by engineer. */ "Errors" = "Erreurs"; -/* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Même s'il est désactivé dans la conversation."; +/* 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"; @@ -2076,6 +2357,9 @@ /* 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"; @@ -2100,18 +2384,30 @@ /* 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é."; @@ -2169,6 +2465,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "Recherche de message plus rapide"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; + /* No comment provided by engineer. */ "Fix" = "Réparer"; @@ -2187,9 +2486,21 @@ /* 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"; @@ -2220,8 +2531,8 @@ /* 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."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Le serveur de redirection %1$@ n'a pas réussi à se connecter au serveur de destination %2$@. 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 : %@."; @@ -2271,7 +2582,7 @@ /* No comment provided by engineer. */ "Group already exists" = "Le groupe existe déjà"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Ce groupe existe déjà !"; /* No comment provided by engineer. */ @@ -2361,6 +2672,12 @@ /* time unit */ "hours" = "heures"; +/* 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 SimpleX works" = "Comment SimpleX fonctionne"; @@ -2529,7 +2846,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Nom d'affichage invalide !"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Lien invalide"; /* No comment provided by engineer. */ @@ -2565,6 +2882,9 @@ /* 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"; @@ -2626,24 +2946,18 @@ "Join" = "Rejoindre"; /* No comment provided by engineer. */ -"join as %@" = "rejoindre entant que %@"; +"Join as %@" = "rejoindre entant que %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Rejoindre le groupe"; /* No comment provided by engineer. */ "Join group conversations" = "Participez aux conversations de groupe"; -/* No comment provided by engineer. */ -"Join group?" = "Rejoindre le groupe ?"; - /* No comment provided by engineer. */ "Join incognito" = "Rejoindre en incognito"; -/* No comment provided by engineer. */ -"Join with current profile" = "Rejoindre avec le profil actuel"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Rejoindre votre groupe ?\nVoici votre lien pour le groupe %@ !"; /* No comment provided by engineer. */ @@ -2679,6 +2993,12 @@ /* 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"; @@ -2775,12 +3095,18 @@ /* 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 !"; @@ -2958,16 +3284,16 @@ /* 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. */ "Multiple chat profiles" = "Différents profils de chat"; -/* No comment provided by engineer. */ -"mute" = "muet"; - -/* swipe action */ +/* notification label action */ "Mute" = "Muet"; /* No comment provided by engineer. */ @@ -2982,19 +3308,25 @@ /* 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. */ +/* alert title */ "Network status" = "État du réseau"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "jamais"; /* No comment provided by engineer. */ @@ -3015,6 +3347,9 @@ /* 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 %@"; @@ -3036,6 +3371,9 @@ /* 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."; @@ -3081,6 +3419,12 @@ /* 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"; @@ -3099,6 +3443,18 @@ /* 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"; @@ -3120,6 +3476,9 @@ /* 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\")"; @@ -3127,8 +3486,9 @@ "observer" = "observateur"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "off"; /* blur media */ @@ -3140,7 +3500,9 @@ /* feature offered item */ "offered %@: %@" = "propose %1$@ : %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3164,6 +3526,9 @@ /* 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 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**."; @@ -3209,16 +3574,22 @@ /* 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"; + +/* new chat action */ "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"; + +/* new chat action */ "Open group" = "Ouvrir le groupe"; /* authentication reason */ @@ -3230,6 +3601,15 @@ /* 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"; @@ -3242,6 +3622,9 @@ /* No comment provided by engineer. */ "Or show this code" = "Ou montrez ce code"; +/* No comment provided by engineer. */ +"Or to share privately" = "Ou à partager en privé"; + /* No comment provided by engineer. */ "other" = "autre"; @@ -3281,9 +3664,6 @@ /* No comment provided by engineer. */ "Password to show" = "Mot de passe à entrer"; -/* past/unknown group member */ -"Past member %@" = "Ancien membre %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Coller l'adresse du bureau"; @@ -3332,7 +3712,7 @@ /* 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."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Veuillez vérifier votre connexion réseau avec %@ et réessayer."; /* No comment provided by engineer. */ @@ -3374,15 +3754,15 @@ /* 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"; - /* No comment provided by engineer. */ "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 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"; @@ -3392,6 +3772,9 @@ /* 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"; @@ -3410,7 +3793,7 @@ /* No comment provided by engineer. */ "Private routing" = "Routage privé"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Erreur de routage privé"; /* No comment provided by engineer. */ @@ -3542,9 +3925,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "confimation reçu…"; -/* notification */ -"Received file event" = "Événement de fichier reçu"; - /* message info title */ "Received message" = "Message reçu"; @@ -3605,14 +3985,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Réduction de la consommation de batterie"; -/* reject incoming call via notification - swipe action */ +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Rejeter"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Rejeter le contact (l'expéditeur N'en est PAS informé)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Rejeter la demande de contact"; /* call status */ @@ -3666,24 +4047,21 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Renégocier le chiffrement ?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Répéter la demande de connexion ?"; - /* No comment provided by engineer. */ "Repeat download" = "Répéter le téléchargement"; /* No comment provided by engineer. */ "Repeat import" = "Répéter l'importation"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Répéter la requête d'adhésion ?"; - /* No comment provided by engineer. */ "Repeat upload" = "Répéter l'envoi"; /* chat item action */ "Reply" = "Répondre"; +/* chat list item title */ +"requested to connect" = "demande à se connecter"; + /* No comment provided by engineer. */ "Required" = "Requis"; @@ -3729,12 +4107,15 @@ /* No comment provided by engineer. */ "Restore database error" = "Erreur de restauration de la base de données"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Réessayer"; /* chat item action */ "Reveal" = "Révéler"; +/* No comment provided by engineer. */ +"Review conditions" = "Vérifier les conditions"; + /* No comment provided by engineer. */ "Revoke" = "Révoquer"; @@ -3757,7 +4138,7 @@ "Safer groups" = "Groupes plus sûrs"; /* alert button - chat item action */ +chat item action */ "Save" = "Enregistrer"; /* alert button */ @@ -3916,9 +4297,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "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"; @@ -3997,9 +4375,6 @@ /* 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é"; @@ -4021,6 +4396,9 @@ /* 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"; @@ -4030,14 +4408,23 @@ /* 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 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 téléverser, 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 !"; @@ -4109,15 +4496,21 @@ "Shape profile images" = "Images de profil modelable"; /* alert action - chat item 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 ?"; @@ -4130,6 +4523,9 @@ /* No comment provided by engineer. */ "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"; @@ -4175,6 +4571,18 @@ /* 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 ?"; + +/* alert title */ +"SimpleX address settings" = "Paramètres de réception automatique"; + +/* 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."; @@ -4250,6 +4658,9 @@ /* 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"; @@ -4352,6 +4763,9 @@ /* 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."; @@ -4385,7 +4799,7 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* file error alert title */ "Temporary file error" = "Erreur de fichier temporaire"; /* server test failure */ @@ -4412,6 +4826,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'application peut vous avertir lorsque vous recevez des messages ou des demandes de contact - veuillez ouvrir les paramètres pour les activer."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "L'application protège votre vie privée en utilisant des opérateurs différents pour chaque conversation."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "L'application demandera de confirmer les téléchargements à partir de serveurs de fichiers inconnus (sauf .onion)."; @@ -4421,6 +4838,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 !"; @@ -4458,17 +4878,23 @@ "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."; +"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é ! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "L'expéditeur N'en sera PAS informé"; /* 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."; @@ -4478,6 +4904,9 @@ /* 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 **%@**."; @@ -4514,12 +4943,6 @@ /* No comment provided by engineer. */ "This group no longer exists." = "Ce groupe n'existe plus."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Voici votre propre lien unique !"; - -/* 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."; @@ -4541,6 +4964,9 @@ /* No comment provided by engineer. */ "To make a new connection" = "Pour établir une nouvelle connexion"; +/* No comment provided by engineer. */ +"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."; @@ -4553,6 +4979,9 @@ /* 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."; @@ -4565,9 +4994,15 @@ /* 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."; @@ -4589,12 +5024,6 @@ /* 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 : %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact."; - /* No comment provided by engineer. */ "Turkish interface" = "Interface en turc"; @@ -4625,6 +5054,9 @@ /* rcv group event chat item */ "unblocked %@" = "%@ débloqué"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Messages non distribués"; + /* No comment provided by engineer. */ "Unexpected migration state" = "État de la migration inattendu"; @@ -4682,10 +5114,7 @@ /* authentication reason */ "Unlock app" = "Déverrouiller l'app"; -/* No comment provided by engineer. */ -"unmute" = "démuter"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Démute"; /* No comment provided by engineer. */ @@ -4743,11 +5172,20 @@ "Use .onion hosts" = "Utiliser les hôtes .onions"; /* No comment provided by engineer. */ -"Use chat" = "Utiliser le chat"; +"Use %@" = "Utiliser %@"; /* No comment provided by engineer. */ +"Use chat" = "Utiliser le chat"; + +/* new chat action */ "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"; @@ -4757,7 +5195,7 @@ /* No comment provided by engineer. */ "Use iOS call interface" = "Utiliser l'interface d'appel d'iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Utiliser un nouveau profil incognito"; /* No comment provided by engineer. */ @@ -4772,6 +5210,9 @@ /* 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 ?"; @@ -4856,9 +5297,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"; @@ -4940,6 +5387,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "lorsque l'IP est masquée"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Lorsque plusieurs opérateurs sont activés, aucun d'entre eux ne dispose de métadonnées permettant de savoir qui communique avec qui."; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Lorsque vous partagez un profil incognito avec quelqu'un, ce profil sera utilisé pour les groupes auxquels il vous invite."; @@ -5004,32 +5454,26 @@ "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 %@."; + +/* new chat sheet message */ "You are already connecting to %@." = "Vous êtes déjà en train de vous connecter à %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Vous êtes déjà connecté(e) via ce lien unique !"; /* No comment provided by engineer. */ "You are already in group %@." = "Vous êtes déjà dans le groupe %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Vous êtes déjà en train de rejoindre le groupe %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Vous êtes déjà en train de rejoindre le groupe via ce lien !"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Vous êtes déjà en train de rejoindre le groupe via ce lien."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Vous êtes déjà membre de ce groupe !\nRépéter la demande d'adhésion ?"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Vous êtes connecté·e au serveur utilisé pour recevoir les messages de ce contact."; - -/* No comment provided by engineer. */ -"you are invited to group" = "vous êtes invité·e au groupe"; - /* No comment provided by engineer. */ "You are invited to group" = "Vous êtes invité·e au groupe"; @@ -5048,6 +5492,9 @@ /* 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"; @@ -5072,6 +5519,9 @@ /* 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."; @@ -5096,7 +5546,7 @@ /* alert message */ "You can view invitation link again in connection details." = "Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Vous ne pouvez pas envoyer de messages !"; /* chat item text */ @@ -5117,10 +5567,7 @@ /* 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 !"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Vous avez déjà demandé une connexion !\nRépéter la demande de connexion ?"; /* No comment provided by engineer. */ @@ -5187,10 +5634,10 @@ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Il vous sera demandé de vous authentifier lorsque vous démarrez ou reprenez l'application après 30 secondes en arrière-plan."; /* No comment provided by engineer. */ -"You will connect to all group members." = "Vous vous connecterez à tous les membres du groupe."; +"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 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."; +"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é."; @@ -5223,7 +5670,7 @@ "Your chat profiles" = "Vos profils de chat"; /* No comment provided by engineer. */ -"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."; +"Your connection was moved to %@ but an error happened when switching 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(%@)."; @@ -5258,27 +5705,27 @@ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Votre profil **%@** sera partagé."; +/* No comment provided by engineer. */ +"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 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."; - /* No comment provided by engineer. */ "Your random profile" = "Votre profil aléatoire"; /* 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"; /* No comment provided by engineer. */ "Your SimpleX address" = "Votre adresse SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Vos serveurs SMP"; - diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 2ba51d1e13..451bdfc699 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (másolható)"; @@ -14,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!" = "- értesíti az ismerősöket a törlésről (nem kötelező)\n- profil nevek 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." = "- 5 perc hosszúságú hangüzenetek.\n- egyedi üzenet-eltűnési időkorlát.\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 időkorlát beállítása az üzenetek eltűnéséhez.\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)"; @@ -44,10 +26,7 @@ "(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)"; @@ -56,7 +35,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Create 1-time link**: to create and share a new invitation link." = "**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. */ "**Create group**: to create a new group." = "**Csoport létrehozása:** új csoport létrehozásához."; @@ -68,19 +47,19 @@ "**e2e encrypted** video call" = "**e2e titkosított** videóhívás"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken 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 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 az ismerőseitő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." = "**Megjegyzés:** 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 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 elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, 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."; @@ -89,7 +68,7 @@ "**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 eltávolításra kerül."; +"**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*"; @@ -101,7 +80,7 @@ "## 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#" = "#titok#"; @@ -140,7 +119,7 @@ "%@ is connected!" = "%@ kapcsolódott!"; /* No comment provided by engineer. */ -"%@ is not verified" = "%@ nem hitelesített"; +"%@ is not verified" = "%@ nincs hitelesítve"; /* No comment provided by engineer. */ "%@ is verified" = "%@ hitelesítve"; @@ -176,7 +155,7 @@ "%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." = "%d fájlt nem sikerült letölteni."; +"%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."; @@ -199,6 +178,9 @@ /* time interval */ "%d sec" = "%d mp"; +/* delete after time */ +"%d seconds(s)" = "%d másodperc"; + /* integrity error chat item */ "%d skipped message(s)" = "%d üzenet kihagyva"; @@ -212,10 +194,10 @@ "%lld %@" = "%lld %@"; /* No comment provided by engineer. */ -"%lld contact(s) selected" = "%lld ismerős kiválasztva"; +"%lld contact(s) selected" = "%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 file(s) with total size of %@" = "%lld fájl, %@ összméretben"; /* No comment provided by engineer. */ "%lld group events" = "%lld csoportesemény"; @@ -230,7 +212,7 @@ "%lld messages blocked by admin" = "%lld üzenetet letiltott az adminisztrátor"; /* No comment provided by engineer. */ -"%lld messages marked deleted" = "%lld törlésre megjelölt üzenet"; +"%lld messages marked deleted" = "%lld üzenet megjelölve törlésre"; /* No comment provided by engineer. */ "%lld messages moderated by %@" = "%@ %lld üzenetet moderált"; @@ -242,34 +224,31 @@ "%lld new interface languages" = "%lld új kezelőfelületi nyelv"; /* No comment provided by engineer. */ -"%lld second(s)" = "%lld másodperc"; +"%lld seconds" = "%lld mp"; /* No comment provided by engineer. */ -"%lld seconds" = "%lld másodperc"; +"%lldd" = "%lldnap"; /* No comment provided by engineer. */ -"%lldd" = "%lldd"; - -/* No comment provided by engineer. */ -"%lldh" = "%lldh"; +"%lldh" = "%lldó"; /* No comment provided by engineer. */ "%lldk" = "%lldk"; /* No comment provided by engineer. */ -"%lldm" = "%lldm"; +"%lldm" = "%lldp"; /* No comment provided by engineer. */ -"%lldmth" = "%lldmth"; +"%lldmth" = "%lldhónap"; /* No comment provided by engineer. */ -"%llds" = "%llds"; +"%llds" = "%lldmp"; /* No comment provided by engineer. */ -"%lldw" = "%lldw"; +"%lldw" = "%lldhét"; /* No comment provided by engineer. */ -"%u messages failed to decrypt." = "%u üzenet visszafejtése sikertelen."; +"%u messages failed to decrypt." = "Nem sikerült visszafejteni %u üzenetet."; /* No comment provided by engineer. */ "%u messages skipped." = "%u üzenet kihagyva."; @@ -289,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 nap"; /* time interval */ @@ -298,17 +278,22 @@ /* 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"; -/* No comment provided by engineer. */ -"1-time link" = "Egyszer használható meghívó-hivatkozás"; +/* delete after time */ +"1 year" = "1 é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ó-hivatkozás csak *egyetlen ismerőssel használható* - személyesen vagy bármilyen üzenetküldőn keresztül megosztható."; +"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"; @@ -320,61 +305,77 @@ "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**." = "**Az összes csevegési profiljához az alkalmazásban** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva."; +"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 lesz használva."; /* No comment provided by engineer. */ -"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "**Az összes ismerősé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."; +"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 lesz használva.\n**Megjegyzés:** ha sok kapcsolata van, akkor 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 operators" = "Az üzemeltetőkről"; /* No comment provided by engineer. */ -"About SimpleX Chat" = "SimpleX Chat névjegye"; +"About SimpleX Chat" = "A SimpleX Chat névjegye"; /* No comment provided by engineer. */ "above, then choose:" = "gombra fent, majd válassza ki:"; /* No comment provided by engineer. */ -"Accent" = "Kiemelés"; +"Accent" = "Kiemelőszín"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Elfogadás"; +/* alert action */ +"Accept as member" = "Befogadás tagként"; + +/* alert action */ +"Accept as observer" = "Befogadás megfigyelőként"; + /* No comment provided by engineer. */ "Accept conditions" = "Feltételek elfogadása"; /* No comment provided by engineer. */ -"Accept connection request?" = "Kapcsolatkérés elfogadása?"; +"Accept connection request?" = "Elfogadja a kapcsolódási kérést?"; + +/* alert title */ +"Accept contact request" = "Partneri kapcsolatkérés elfogadása"; /* notification body */ -"Accept contact request from %@?" = "Elfogadja %@ kapcsolatkérését?"; +"Accept contact request from %@?" = "Elfogadja %@ partneri kapcsolatkérését?"; -/* accept contact request via notification - swipe action */ -"Accept incognito" = "Fogadás inkognitóban"; +/* alert action +swipe action */ +"Accept incognito" = "Elfogadás inkognitóban"; + +/* alert title */ +"Accept member" = "Tag befogadása"; + +/* rcv group event chat item */ +"accepted %@" = "befogadta őt: %@"; /* call status */ -"accepted call" = "elfogadott hívás"; +"accepted call" = "fogadott hívás"; /* No comment provided by engineer. */ "Accepted conditions" = "Elfogadott feltételek"; @@ -382,21 +383,33 @@ /* chat list item title */ "accepted invitation" = "elfogadott meghívó"; -/* No comment provided by engineer. */ -"Acknowledged" = "Nyugtázva"; +/* rcv group event chat item */ +"accepted you" = "befogadta Önt"; /* No comment provided by engineer. */ -"Acknowledgement errors" = "Nyugtázott hibák"; +"Acknowledged" = "Visszaigazolt"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Visszaigazolási hibák"; + +/* token status text */ +"Active" = "Aktív"; /* No comment provided by engineer. */ "Active connections" = "Aktív kapcsolatok száma"; /* No comment provided by engineer. */ -"Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára."; +"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"; + +/* placeholder for sending contact request */ +"Add message" = "Üzenet hozzáadása"; + /* No comment provided by engineer. */ "Add profile" = "Profil hozzáadása"; @@ -407,40 +420,43 @@ "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" = "Csapattagok hozzáadása"; +"Add team members" = "Munkatársak hozzáadása"; /* No comment provided by engineer. */ "Add to another device" = "Hozzáadás egy másik eszközhöz"; +/* No comment provided by engineer. */ +"Add to list" = "Hozzáadás listához"; + /* No comment provided by engineer. */ "Add welcome message" = "Üdvözlőüzenet hozzáadása"; /* No comment provided by engineer. */ -"Add your team members to the conversations." = "Adja hozzá csapattagjait a beszélgetésekhez."; +"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"; +"Added media & file servers" = "Hozzáadott fájl- és médiakiszolgá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és"; +"Additional accent" = "További kiemelőszín"; /* No comment provided by engineer. */ -"Additional accent 2" = "További kiemelés 2"; +"Additional accent 2" = "További kiemelőszín 2"; /* No comment provided by engineer. */ -"Additional secondary" = "További másodlagos"; +"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 kliens megszakítja a cím módosítását, és a régi fogadási címet fogja használni."; /* No comment provided by engineer. */ -"Address or 1-time link?" = "Cím vagy egyszer használható meghívó-hivatkozás?"; +"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"; @@ -455,7 +471,7 @@ "Admins can block a member for all." = "Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak."; /* No comment provided by engineer. */ -"Admins can create the links to join groups." = "Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való 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"; @@ -469,17 +485,26 @@ /* chat item text */ "agreeing encryption…" = "titkosítás elfogadása…"; +/* member criteria value */ +"all" = "összes"; + +/* No comment provided by engineer. */ +"All" = "Összes"; + /* No comment provided by engineer. */ "All app data is deleted." = "Az összes alkalmazásadat törölve."; /* No comment provided by engineer. */ -"All chats and messages will be deleted - this cannot be undone!" = "Az összes csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza!"; +"All chats and messages will be deleted - this cannot be undone!" = "Az összes csevegés és üzenet törölve lesz – ez a művelet nem vonható vissza!"; + +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Az összes csevegés el lesz távolítva a(z) %@ nevű listáról, és a lista is törölve lesz."; /* No comment provided by engineer. */ -"All data is erased when it is entered." = "A jelkód megadása után az összes adat törlésre kerül."; +"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 biztonságban van az eszközén."; +"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."; @@ -488,52 +513,61 @@ "all members" = "összes tag"; /* No comment provided by engineer. */ -"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Az összes üzenetet és fájlt **végpontok közötti titkosítással** küldi, a közvetlen üzenetekben pedig kvantumrezisztens biztonsággal."; +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Az összes üzenet és fájl **végpontok közötti titkosítással**, a közvetlen üzenetek továbbá kvantumbiztos titkosítással is rendelkeznek."; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone!" = "Az összes üzenet törlésre kerül – ez a művelet nem vonható vissza!"; +"All messages will be deleted - this cannot be undone!" = "Az összes üzenet törölve lesz – ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Az összes üzenet tö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! The messages will be deleted ONLY for you." = "Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek."; /* No comment provided by engineer. */ -"All new messages from %@ will be hidden!" = "Az összes új üzenet elrejtésre kerül tőle: %@!"; +"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." = "Az összes ismerősével kapcsolatban marad."; +"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 will remain connected. Profile update will be sent to your contacts." = "Az ismerőseivel kapcsolatban marad. A profil-változtatások frissítésre kerülnek az ismerősöknél."; +"All servers" = "Összes kiszolgáló"; /* 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 ismerőse, -beszélgetése és -fájlja biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra."; +"All your contacts will remain connected." = "Az összes partnerével kapcsolatban marad."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára."; + +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra."; /* No comment provided by engineer. */ "Allow" = "Engedélyezés"; /* No comment provided by engineer. */ -"Allow calls only if your contact allows them." = "A hívások kezdeményezése csak abban az esetben van engedélyezve, ha az 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 calls?" = "Hívások engedélyezése?"; +"Allow calls?" = "Engedélyezi a hívásokat?"; /* No comment provided by engineer. */ -"Allow disappearing messages only if your contact allows it to you." = "Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi az Ön számá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 downgrade" = "Visszafejlesztés engedélyezése"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. (24 óra)"; +"Allow files and media only if your contact allows them." = "A fájlok és a médiatartalmak küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi."; /* No comment provided by engineer. */ -"Allow message reactions only if your contact allows them." = "Az üzenetreakciók küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi."; +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra)"; /* No comment provided by engineer. */ -"Allow message reactions." = "Üzenetreakciók engedélyezése."; +"Allow message reactions only if your contact allows them." = "A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha a partnere is engedélyezi."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "A reakciók hozzáadása az üzenetekhez engedélyezve van."; /* No comment provided by engineer. */ "Allow sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között engedélyezve van."; @@ -545,45 +579,51 @@ "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)"; +"Allow to irreversibly delete sent messages. (24 hours)" = "Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra)"; /* No comment provided by engineer. */ -"Allow to send files and media." = "Fájlok és médiatartalmak küldésének engedélyezése."; +"Allow to report messsages to moderators." = "Az üzenetek jelentése a moderátorok felé engedélyezve van."; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "A fájlok és a médiatartalmak küldése engedélyezve van."; /* No comment provided by engineer. */ "Allow to send SimpleX links." = "A SimpleX-hivatkozások küldése engedélyezve van."; /* No comment provided by engineer. */ -"Allow to send voice messages." = "Hangüzenetek küldésének engedélyezése."; +"Allow to send voice messages." = "A hangüzenetek küldése engedélyezve van."; /* No comment provided by engineer. */ -"Allow voice messages only if your contact allows them." = "A hangüzenetek küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi."; +"Allow voice messages only if your contact allows them." = "A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi."; /* No comment provided by engineer. */ -"Allow voice messages?" = "Hangüzenetek engedélyezése?"; +"Allow voice messages?" = "Engedélyezi a hangüzeneteket?"; /* No comment provided by engineer. */ -"Allow your contacts adding message reactions." = "Az üzenetreakciók küldése engedélyezve van az ismerősei 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 az ismerősei számára."; +"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 az ismerősei számára. (24 óra)"; +"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 az ismerősei számára."; +"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 az ismerősei számára."; +"Allow your contacts to send files and media." = "A fájlok és a médiatartalmak küldése engedélyezve van a partnerei számára."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "A hangüzenetek küldése engedélyezve van a partnerei számára."; /* No comment provided by engineer. */ "Already connected?" = "Már kapcsolódott?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Kapcsolódás folyamatban!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "A csatlakozás folyamatban van a csoporthoz!"; /* pref value */ @@ -593,14 +633,17 @@ "Always use private routing." = "Mindig használjon privát útválasztást."; /* No comment provided by engineer. */ -"Always use relay" = "Mindig használjon közvetítő-kiszolgálót"; +"Always use relay" = "Mindig használjon továbbítókiszolgálót"; /* No comment provided by engineer. */ -"An empty chat profile with the provided name is created, and the app opens as usual." = "Egy üres csevegési profil jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik."; +"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"; @@ -608,7 +651,7 @@ "Anybody can host servers." = "Bárki üzemeltethet kiszolgálókat."; /* No comment provided by engineer. */ -"App build: %@" = "Az alkalmazás build száma: %@"; +"App build: %@" = "Alkalmazás összeállítási száma: %@"; /* No comment provided by engineer. */ "App data migration" = "Alkalmazásadatok átköltöztetése"; @@ -616,23 +659,26 @@ /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével)."; +/* No comment provided by engineer. */ +"App group:" = "Alkalmazáscsoport:"; + /* No comment provided by engineer. */ "App icon" = "Alkalmazásikon"; /* No comment provided by engineer. */ -"App passcode" = "Alkalmazás jelkód"; +"App passcode" = "Alkalmazásjelkó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ásjelkó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ó"; +"App version" = "Alkalmazás verziója"; /* No comment provided by engineer. */ -"App version: v%@" = "Alkalmazás verzió: v%@"; +"App version: v%@" = "Alkalmazás verziója: v%@"; /* No comment provided by engineer. */ "Appearance" = "Megjelenés"; @@ -641,25 +687,46 @@ "Apply" = "Alkalmaz"; /* No comment provided by engineer. */ -"Apply to" = "Alkalmazás erre"; +"Apply to" = "Használat ehhez"; + +/* 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." = "Az ismerősök archiválása a későbbi csevegéshez."; +"Archive contacts to chat later." = "A partnerek archiválása a későbbi csevegéshez."; /* No comment provided by engineer. */ -"Archived contacts" = "Archivált ismerősök"; +"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"; +"attempts" = "kísérletek"; /* No comment provided by engineer. */ "Audio & video calls" = "Hang- és videóhívások"; @@ -671,10 +738,10 @@ "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 hívások kezdeményezése le van tiltva ebben a csevegésben."; +"Audio/video calls are prohibited." = "A hívások kezdeményezése le van tiltva."; /* PIN entry */ "Authentication cancelled" = "Hitelesítés visszavonva"; @@ -695,14 +762,11 @@ "Auto-accept" = "Automatikus elfogadás"; /* No comment provided by engineer. */ -"Auto-accept contact requests" = "Kapcsolatkérések automatikus elfogadása"; +"Auto-accept contact requests" = "Partneri kapcsolatkérések automatikus elfogadása"; /* No comment provided by engineer. */ "Auto-accept images" = "Képek automatikus elfogadása"; -/* alert title */ -"Auto-accept settings" = "Beállítások automatikus elfogadása"; - /* No comment provided by engineer. */ "Back" = "Vissza"; @@ -710,44 +774,56 @@ "Background" = "Háttér"; /* No comment provided by engineer. */ -"Bad desktop address" = "Hibás számítógép cím"; +"Bad desktop address" = "Hibás a számítógép címe"; /* integrity error chat item */ -"bad message hash" = "hibás az üzenet hasító értéke"; +"bad message hash" = "hibás az üzenet kivonata"; /* No comment provided by engineer. */ -"Bad message hash" = "Hibás az üzenet hasító értéke"; +"Bad message hash" = "Hibás az üzenet kivonata"; /* integrity error chat item */ -"bad message ID" = "téves üzenet ID"; +"bad message ID" = "hibás az üzenet azonosítója"; /* No comment provided by engineer. */ -"Bad message ID" = "Téves üzenet ID"; +"Bad message ID" = "Hibás az üzenet azonosítója"; /* No comment provided by engineer. */ "Better calls" = "Továbbfejlesztett hívásélmény"; /* No comment provided by engineer. */ -"Better groups" = "Javított csoportok"; +"Better groups" = "Továbbfejlesztett csoportok"; + +/* No comment provided by engineer. */ +"Better groups performance" = "Továbbfejlesztett, gyorsabb csoportok"; /* No comment provided by engineer. */ "Better message dates." = "Továbbfejlesztett üzenetdátumok."; /* No comment provided by engineer. */ -"Better messages" = "Jobb üzenetek"; +"Better messages" = "Továbbfejlesztett üzenetek"; /* No comment provided by engineer. */ -"Better networking" = "Jobb hálózatkezelés"; +"Better networking" = "Továbbfejlesztett 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. */ +"Bio" = "Névjegy"; + +/* alert title */ +"Bio too large" = "A névjegy túl hosszú"; + /* No comment provided by engineer. */ "Black" = "Fekete"; @@ -755,31 +831,32 @@ "Block" = "Letiltás"; /* No comment provided by engineer. */ -"Block for all" = "Letiltás az összes tag számára"; +"Block for all" = "Letiltás"; /* No comment provided by engineer. */ "Block group members" = "Csoporttagok letiltása"; /* No comment provided by engineer. */ -"Block member" = "Tag letiltása"; +"Block member" = "Letiltás"; /* No comment provided by engineer. */ -"Block member for all?" = "Az összes tag számára letiltja ezt a tagot?"; +"Block member for all?" = "Az összes tag számára letiltja a tagot?"; /* No comment provided by engineer. */ -"Block member?" = "Tag letiltása?"; +"Block member?" = "Letiltja a tagot?"; /* marked deleted chat item preview text */ "blocked" = "letiltva"; /* rcv group event chat item */ -"blocked %@" = "letiltotta %@-t"; +"blocked %@" = "letiltotta őt: %@"; -/* marked deleted chat item preview text */ +/* 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" = "Az adminisztrátor letiltotta"; +"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."; @@ -791,10 +868,13 @@ "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."; +"Bot" = "Bot"; /* 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 add message reactions." = "Mindkét fél hozzáadhat az üzenetekhez reakciókat."; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Mindkét fél tud hívásokat kezdeményezni."; @@ -802,6 +882,9 @@ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Mindkét fél küldhet eltűnő üzeneteket."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Mindkét fél küldhet fájlokat és médiatartalmakat."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Mindkét fél küldhet hangüzeneteket."; @@ -814,9 +897,18 @@ /* No comment provided by engineer. */ "Business chats" = "Üzleti csevegések"; +/* No comment provided by engineer. */ +"Business connection" = "Üzleti kapcsolat"; + +/* 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"; @@ -842,22 +934,29 @@ "Camera not available" = "A kamera nem elérhető"; /* No comment provided by engineer. */ -"Can't call contact" = "Nem lehet felhívni az ismerőst"; +"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 az ismerőst!"; +/* alert title */ +"Can't change profile" = "Nem lehet módosítani a profilt"; /* No comment provided by engineer. */ -"Can't invite contacts!" = "Nem lehet meghívni az ismerősöket!"; +"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"; +/* No comment provided by engineer. */ +"can't send messages" = "nem lehet üzeneteket küldeni"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Mégse"; /* No comment provided by engineer. */ @@ -876,59 +975,62 @@ "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."; +"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"; + +/* alert title */ +"Change automatic message deletion?" = "Módosítja az automatikus üzenettörlést?"; /* authentication reason */ -"Change chat profiles" = "Csevegési profilok megváltoztatása"; +"Change chat profiles" = "Csevegési profilok módosítása"; /* No comment provided by engineer. */ -"Change database passphrase?" = "Adatbázis-jelmondat megváltoztatása?"; +"Change database passphrase?" = "Módosítja az adatbázis jelmondatát?"; /* authentication reason */ -"Change lock mode" = "Zárolási mód megváltoztatása"; +"Change lock mode" = "Zárolási mód módosítása"; /* No comment provided by engineer. */ -"Change member role?" = "Tag szerepkörének megváltoztatása?"; +"Change member role?" = "Módosítja a tag szerepkörét?"; /* authentication reason */ -"Change passcode" = "Jelkód megváltoztatása"; +"Change passcode" = "Jelkód módosítása"; /* No comment provided by engineer. */ -"Change receiving address" = "A fogadó cím megváltoztatása"; +"Change receiving address" = "Fogadási cím módosítása"; /* No comment provided by engineer. */ -"Change receiving address?" = "Megváltoztatja a fogadó címet?"; +"Change receiving address?" = "Módosítja a fogadási címet?"; /* No comment provided by engineer. */ -"Change role" = "Szerepkör megváltoztatása"; +"Change role" = "Szerepkör módosítása"; /* authentication reason */ -"Change self-destruct mode" = "Önmegsemmisítő mód megváltoztatása"; +"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 az Ön 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 megváltoztatása nála: %@…"; +"changing address for %@…" = "cím módosítása %@ számára…"; /* chat item text */ -"changing address…" = "cím megváltoztatása…"; +"changing address…" = "cím módosítása…"; /* No comment provided by engineer. */ "Chat" = "Csevegés"; @@ -936,7 +1038,7 @@ /* No comment provided by engineer. */ "Chat already exists" = "A csevegés már létezik"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "A csevegés már létezik!"; /* No comment provided by engineer. */ @@ -961,13 +1063,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 ezt az adatbázist már használta egy másik eszközön, akkor a csevegés elindítása előtt vissza kell állítania azt."; /* No comment provided by engineer. */ -"Chat list" = "Csevegőlista"; +"Chat list" = "Csevegési lista"; /* No comment provided by engineer. */ "Chat migrated!" = "A csevegés átköltöztetve!"; @@ -976,7 +1078,7 @@ "Chat preferences" = "Csevegési beállítások"; /* alert message */ -"Chat preferences were changed." = "A csevegési beállítások megváltoztak."; +"Chat preferences were changed." = "A csevegési beállítások módosultak."; /* No comment provided by engineer. */ "Chat profile" = "Csevegési profil"; @@ -985,14 +1087,26 @@ "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örlésre kerül - ezt a műveletet nem lehet visszavonni!"; +"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örlésre kerül az Ön számára - ezt a műveletet nem lehet visszavonni!"; +"Chat will be deleted for you - this cannot be undone!" = "A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza!"; + +/* chat toolbar */ +"Chat with admins" = "Csevegés az adminisztrátorokkal"; + +/* No comment provided by engineer. */ +"Chat with member" = "Csevegés a taggal"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Csevegés a tagokkal mielőtt csatlakoznának."; /* No comment provided by engineer. */ "Chats" = "Csevegések"; +/* No comment provided by engineer. */ +"Chats with members" = "Csevegés a tagokkal"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Üzenetek ellenőrzése 20 percenként."; @@ -1015,13 +1129,13 @@ "Choose from library" = "Választás a könyvtárból"; /* No comment provided by engineer. */ -"Chunks deleted" = "Törölt fájltöredékek"; +"Chunks deleted" = "Törölt töredékek"; /* No comment provided by engineer. */ -"Chunks downloaded" = "Letöltött fájltöredékek"; +"Chunks downloaded" = "Letöltött töredékek"; /* No comment provided by engineer. */ -"Chunks uploaded" = "Feltöltött fájltöredékek"; +"Chunks uploaded" = "Feltöltött töredékek"; /* swipe action */ "Clear" = "Kiürítés"; @@ -1030,10 +1144,16 @@ "Clear conversation" = "Üzenetek kiürítése"; /* No comment provided by engineer. */ -"Clear conversation?" = "Üzenetek kiürítése?"; +"Clear conversation?" = "Kiüríti az üzeneteket?"; /* No comment provided by engineer. */ -"Clear private notes?" = "Privát jegyzetek kiüríté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"; @@ -1045,13 +1165,16 @@ "Color mode" = "Színmód"; /* No comment provided by engineer. */ -"colored" = "színes"; +"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ökével."; +"Compare security codes with your contacts." = "Biztonsági kódok összehasonlítása a partnerekével."; /* No comment provided by engineer. */ "complete" = "befejezett"; @@ -1060,7 +1183,7 @@ "Completed" = "Elkészült"; /* No comment provided by engineer. */ -"Conditions accepted on: %@." = "Feltételek elfogadva ekkor: %@."; +"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: **%@**."; @@ -1068,32 +1191,29 @@ /* 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. */ +/* alert button */ "Conditions of use" = "Használati feltételek"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltető számára."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for 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 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 ekkor lesznek elfogadva: %@."; +"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 elfogadásra kerülnek az engedélyezett üzemeltető számára: %@."; +"Conditions will be automatically accepted for enabled operators on: %@." = "A feltételek automatikusan el lesznek fogadva az engedélyezett üzemeltetők számára a következő időpontban: %@."; /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-kiszolgálók beállítása"; +/* No comment provided by engineer. */ +"Configure server operators" = "Kiszolgálóüzemeltetők beállítása"; + /* No comment provided by engineer. */ "Confirm" = "Megerősítés"; /* No comment provided by engineer. */ -"Confirm contact deletion?" = "Biztosan törli az ismerőst?"; +"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"; @@ -1119,6 +1239,9 @@ /* 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"; @@ -1126,7 +1249,7 @@ "Connect automatically" = "Kapcsolódás automatikusan"; /* No comment provided by engineer. */ -"Connect incognito" = "Kapcsolódás inkognitóban"; +"Connect faster! 🚀" = "Gyorsabb kapcsolódás! 🚀"; /* No comment provided by engineer. */ "Connect to desktop" = "Társítás számítógéppel"; @@ -1135,27 +1258,24 @@ "connect to SimpleX Chat developers." = "kapcsolódás a SimpleX Chat fejlesztőkhöz."; /* No comment provided by engineer. */ -"Connect to your friends faster." = "Kapcsolódjon gyorsabban az ismerőseihez."; +"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?"; +/* new chat sheet title */ +"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódik saját magához?\nEz a saját egyszer használható meghívója!"; -/* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az Ön egyszer használható meghívó-hivatkozása!"; +/* new chat sheet title */ +"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 to yourself?\nThis is your own SimpleX address!" = "Kapcsolódás saját magához?\nEz az Ön SimpleX-címe!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Kapcsolódás a kapcsolattartási címen keresztül"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Kapcsolódás egy hivatkozáson keresztül"; -/* No comment provided by engineer. */ -"Connect via one-time link" = "Kapcsolódás egyszer használható meghívó-hivatkozáson keresztül"; +/* new chat sheet title */ +"Connect via one-time link" = "Kapcsolódás egyszer használható meghívón keresztül"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Kapcsolódás a következővel: %@"; /* No comment provided by engineer. */ @@ -1167,9 +1287,6 @@ /* No comment provided by engineer. */ "Connected desktop" = "Társított számítógép"; -/* rcv group event chat item */ -"connected directly" = "közvetlenül kapcsolódott"; - /* No comment provided by engineer. */ "Connected servers" = "Kapcsolódott kiszolgálók"; @@ -1189,10 +1306,10 @@ "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ó)"; +"connecting (introduction invitation)" = "kapcsolódás (bemutatkozó meghívó)"; /* call status */ "connecting call" = "kapcsolódási hívás…"; @@ -1204,7 +1321,7 @@ "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 az ismerőshöz, várjon vagy ellenőrizze később!"; +"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"; @@ -1219,6 +1336,9 @@ "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"; + +/* alert title */ "Connection error" = "Kapcsolódási hiba"; /* No comment provided by engineer. */ @@ -1227,11 +1347,20 @@ /* chat list item title (it should not be shown */ "connection established" = "kapcsolat létrehozva"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "A kiszolgáló üzemeltetője letiltotta a kapcsolatot:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "A kapcsolat nem áll készen."; + /* No comment provided by engineer. */ "Connection notifications" = "Kapcsolódási értesítések"; /* No comment provided by engineer. */ -"Connection request sent!" = "Kapcsolatkérés elküldve!"; +"Connection request sent!" = "Kapcsolódá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"; @@ -1239,7 +1368,7 @@ /* No comment provided by engineer. */ "Connection terminated" = "Kapcsolat megszakítva"; -/* No comment provided by engineer. */ +/* alert title */ "Connection timeout" = "Időtúllépés kapcsolódáskor"; /* No comment provided by engineer. */ @@ -1252,46 +1381,64 @@ "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" = "Az ismerős már létezik"; +"Contact already exists" = "A partner már létezik"; /* No comment provided by engineer. */ -"Contact deleted!" = "Ismerős törölve!"; +"contact deleted" = "partner törölve"; /* 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 disabled" = "partner letiltva"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "a partner e2e titkosítással rendelkezik"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "a partner nem rendelkezik e2e titkosítással"; /* notification */ -"Contact hidden:" = "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 deleted." = "Törölt ismerős."; +"Contact is deleted." = "Törölt partner."; /* No comment provided by engineer. */ "Contact name" = "Csak név"; /* No comment provided by engineer. */ -"Contact preferences" = "Ismerős beállításai"; +"contact not ready" = "a partner nem áll készen"; /* No comment provided by engineer. */ -"Contact will be deleted - this cannot be undone!" = "Az ismerős törlésre fog kerülni - ez a művelet nem vonható vissza!"; +"Contact preferences" = "Partnerbeállítások"; /* No comment provided by engineer. */ -"Contacts" = "Ismerősök"; +"Contact requests from groups" = "Partneri kapcsolatkérések a csoportokból"; /* No comment provided by engineer. */ -"Contacts can mark messages for deletion; you will be able to view them." = "Az ismerősei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat."; +"contact should accept…" = "a partnernek el kell fogadnia…"; + +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "A partner törölve lesz – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"Contacts" = "Partnerek"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "A partnerei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat."; + +/* blocking reason */ +"Content violates conditions of use" = "A tartalom sérti a használati feltételeket"; /* No comment provided by engineer. */ "Continue" = "Folytatás"; @@ -1306,22 +1453,22 @@ "Copy error" = "Másolási hiba"; /* No comment provided by engineer. */ -"Core version: v%@" = "Alapverziószám: v%@"; +"Core version: 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: %@?"; +"Correct name to %@?" = "Helyesbíti a nevet a következőre: %@?"; /* No comment provided by engineer. */ "Create" = "Létrehozás"; /* No comment provided by engineer. */ -"Create 1-time link" = "Egyszer használható meghívó-hivatkozás létrehozása"; +"Create 1-time link" = "Egyszer használható meghívó létrehozása"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Csoport létrehozása véletlenszerűen létrehozott profillal."; +"Create a group using a random profile." = "Csoport létrehozása véletlenszerű profillal."; /* server test step */ "Create file" = "Fájl létrehozása"; @@ -1335,6 +1482,9 @@ /* No comment provided by engineer. */ "Create link" = "Hivatkozás létrehozása"; +/* No comment provided by engineer. */ +"Create list" = "Lista létrehozása"; + /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Új profil létrehozása a [számítógép-alkalmazásban](https://simplex.chat/downloads/). 💻"; @@ -1342,25 +1492,25 @@ "Create profile" = "Profil létrehozása"; /* server test step */ -"Create queue" = "Sorbaállítás létrehozása"; - -/* No comment provided by engineer. */ -"Create secret group" = "Titkos csoport létrehozása"; +"Create queue" = "Várólista létrehozása"; /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX-cím létrehozása"; /* No comment provided by engineer. */ -"Create your profile" = "Saját profil létrehozása"; +"Create your address" = "Saját cím létrehozása"; + +/* No comment provided by engineer. */ +"Create your profile" = "Profil létrehozása"; /* No comment provided by engineer. */ "Created" = "Létrehozva"; /* No comment provided by engineer. */ -"Created at" = "Létrehozva ekkor:"; +"Created at" = "Létrehozva"; /* copied message info */ -"Created at: %@" = "Létrehozva ekkor: %@"; +"Created at: %@" = "Létrehozva: %@"; /* No comment provided by engineer. */ "Creating archive link" = "Archívum hivatkozás létrehozása"; @@ -1372,7 +1522,7 @@ "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 ezen a hivatkozáson keresztül vizsgálhatja felül:"; +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "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:"; /* No comment provided by engineer. */ "Current Passcode" = "Jelenlegi jelkód"; @@ -1384,16 +1534,16 @@ "Current profile" = "Jelenlegi profil"; /* No comment provided by engineer. */ -"Currently maximum supported file size is %@." = "Jelenleg a maximális támogatott fájlméret %@."; +"Currently maximum supported file size is %@." = "Jelenleg támogatott legnagyobb fájl méret: %@."; /* dropdown time picker choice */ -"custom" = "egyedi"; +"custom" = "egyéni"; /* No comment provided by engineer. */ -"Custom time" = "Személyreszabott idő"; +"Custom time" = "Egyéni időköz"; /* No comment provided by engineer. */ -"Customizable message shape." = "Testreszabható üzenetbuborékok."; +"Customizable message shape." = "Személyre szabható üzenetbuborékok."; /* No comment provided by engineer. */ "Customize theme" = "Téma személyre szabása"; @@ -1411,10 +1561,10 @@ "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ítve lesz és a kulcstartóban kerül tárolásra.\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" = "Az datbázis titkosítási jelmondata 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ázishiba"; @@ -1426,13 +1576,13 @@ "Database ID: %d" = "Adatbázis-azonosító: %d"; /* No comment provided by engineer. */ -"Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitel-izolációs beállítások."; +"Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitelelkülönítési 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, ami 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őtt változtassa meg."; +"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"; @@ -1441,7 +1591,7 @@ "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 kulcstartó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." = "A csevegés megnyitásához adja meg az adatbázis jelmondatát."; @@ -1450,16 +1600,16 @@ "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 kulcstartóban kerül tárolásra.\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 átköltöztetésre 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"; @@ -1471,12 +1621,13 @@ "Decentralized" = "Decentralizált"; /* message decrypt error item */ -"Decryption error" = "Titkosítás visszafejtési hiba"; +"Decryption error" = "Titkosítás-visszafejtési hiba"; /* No comment provided by engineer. */ "decryption errors" = "visszafejtési hibák"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "alapértelmezett (%@)"; /* No comment provided by engineer. */ @@ -1486,12 +1637,11 @@ "default (yes)" = "alapértelmezett (igen)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Törlés"; /* No comment provided by engineer. */ -"Delete %lld messages of members?" = "Tagok %lld üzenetének törlése?"; +"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?"; @@ -1500,7 +1650,7 @@ "Delete address" = "Cím törlése"; /* No comment provided by engineer. */ -"Delete address?" = "Cím törlése?"; +"Delete address?" = "Törli a címet?"; /* No comment provided by engineer. */ "Delete after" = "Törlés ennyi idő után"; @@ -1509,28 +1659,34 @@ "Delete all files" = "Az összes fájl törlése"; /* No comment provided by engineer. */ -"Delete and notify contact" = "Törlés, és az ismerős értesítése"; +"Delete and notify contact" = "Törlés, és a partner értesítése"; /* No comment provided by engineer. */ "Delete chat" = "Csevegés törlése"; +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Csevegési üzenetek törlése az eszközrő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?"; + +/* alert title */ +"Delete chat with member?" = "Törli a taggal való csevegést?"; /* No comment provided by engineer. */ -"Delete chat?" = "Csevegés törlése?"; +"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?"; +"Delete contact?" = "Törli a partnert?"; /* No comment provided by engineer. */ "Delete database" = "Adatbázis törlése"; @@ -1542,7 +1698,7 @@ "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ájlokat és a médiatartalmakat?"; /* No comment provided by engineer. */ "Delete files for all chat profiles" = "Fájlok törlése az összes csevegési profilból"; @@ -1557,24 +1713,27 @@ "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ének 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. */ @@ -1584,25 +1743,28 @@ "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 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őben lévő ismerőskérelem törlése?"; +"Delete pending connection?" = "Törli a függőben lévő kapcsolatot?"; /* No comment provided by engineer. */ "Delete profile" = "Profil törlése"; /* server test step */ -"Delete queue" = "Sorbaállítás törlése"; +"Delete queue" = "Várólista törlése"; + +/* No comment provided by engineer. */ +"Delete report" = "Jelentés törlése"; /* No comment provided by engineer. */ "Delete up to 20 messages at once." = "Legfeljebb 20 üzenet egyszerre való törlése."; /* No comment provided by engineer. */ -"Delete user profile?" = "Felhasználói profil törlése?"; +"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"; @@ -1614,16 +1776,16 @@ "Deleted" = "Törölve"; /* No comment provided by engineer. */ -"Deleted at" = "Törölve ekkor:"; +"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"; +"deleted group" = "törölte a csoportot"; /* No comment provided by engineer. */ "Deletion errors" = "Törlési hibák"; @@ -1635,31 +1797,37 @@ "Delivery" = "Kézbesítés"; /* No comment provided by engineer. */ -"Delivery receipts are disabled!" = "A kézbesítési jelentések ki vannak kapcsolva!"; +"Delivery receipts are disabled!" = "A kézbesítési jelentések le vannak tiltva!"; /* No comment provided by engineer. */ "Delivery receipts!" = "Kézbesítési jelentések!"; +/* No comment provided by engineer. */ +"Deprecated options" = "Elavult beállítások"; + /* No comment provided by engineer. */ "Description" = "Leírás"; +/* alert title */ +"Description too large" = "A leírás túl hosszú"; + /* No comment provided by engineer. */ "Desktop address" = "Számítógép címe"; /* No comment provided by engineer. */ -"Desktop app version %@ is not compatible with this app." = "A számítógép-alkalmazás verziója %@ nem kompatibilis ezzel az alkalmazással."; +"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."; +"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: %@"; +"Destination server error: %@" = "Célkiszolgáló-hiba: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval."; +"Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbítókiszolgálóval."; /* No comment provided by engineer. */ "Detailed statistics" = "Részletes statisztikák"; @@ -1686,10 +1854,10 @@ "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, profilképek és átvitel-izoláció."; +"Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitelelkülönítés."; /* connection level description */ "direct" = "közvetlen"; @@ -1701,13 +1869,19 @@ "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 közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban."; +"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)"; +"Disable (keep overrides)" = "Letiltás (egyéni beállítások megtartása)"; + +/* 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 az összes tag számára"; +"Disable for all" = "Letiltás"; /* authentication reason */ "Disable SimpleX Lock" = "SimpleX-zár kikapcsolása"; @@ -1728,22 +1902,22 @@ "Disappearing messages are prohibited in this chat." = "Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited." = "Az eltűnő üzenetek küldése le van tiltva 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"; @@ -1752,19 +1926,22 @@ "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 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 az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; +"Do NOT send messages directly, even if your or destination server does not support private routing." = "NE küldjön üzeneteket közvetlenül, még akkor sem, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; /* No comment provided by engineer. */ -"Do not use credentials with proxy." = "Ne használja a hitelesítőadatokat proxyval."; +"Do not use credentials with proxy." = "Ne használja a hitelesítési adatokat proxyval."; /* No comment provided by engineer. */ -"Do NOT use private routing." = "Ne használjon privát útválasztást."; +"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 SimpleX-et segélyhívásokhoz."; +"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"; @@ -1773,13 +1950,19 @@ "Don't enable" = "Ne engedélyezze"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Ne maradjon le a fontos üzenetekről."; + +/* alert action */ "Don't show again" = "Ne mutasd újra"; +/* No comment provided by engineer. */ +"Done" = "Kész"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Visszafejlesztés és a csevegés megnyitása"; /* alert button - chat item action */ +chat item action */ "Download" = "Letöltés"; /* No comment provided by engineer. */ @@ -1807,7 +1990,7 @@ "Downloading link details" = "Letöltési hivatkozás részletei"; /* No comment provided by engineer. */ -"Duplicate display name!" = "Duplikált megjelenített név!"; +"Duplicate display name!" = "Duplikált megjelenítendő név!"; /* integrity error chat item */ "duplicate message" = "duplikált üzenet"; @@ -1828,22 +2011,28 @@ "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. */ +"Empty message!" = "Üres üzenet!"; /* No comment provided by engineer. */ "Enable" = "Engedélyezés"; /* No comment provided by engineer. */ -"Enable (keep overrides)" = "Engedélyezés (felülírások megtartásával)"; +"Enable (keep overrides)" = "Engedélyezés (egyéni beállítások megtartása)"; + +/* alert title */ +"Enable automatic message deletion?" = "Engedélyezi az automatikus üzenettörlést?"; /* No comment provided by engineer. */ -"Enable automatic message deletion?" = "Automatikus üzenet törlés engedélyezése?"; +"Enable camera access" = "Kamera-hozzáférés engedélyezése"; /* No comment provided by engineer. */ -"Enable camera access" = "Kamera hozzáférés engedélyezése"; +"Enable disappearing messages by default." = "Eltűnő üzenetek engedélyezése alapértelmezetten."; /* No comment provided by engineer. */ -"Enable Flux" = "Flux engedélyezése"; +"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"; @@ -1852,7 +2041,7 @@ "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"; @@ -1861,7 +2050,7 @@ "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"; @@ -1882,10 +2071,10 @@ "Enabled" = "Engedélyezve"; /* No comment provided by engineer. */ -"Enabled for" = "Számukra engedélyezve:"; +"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"; @@ -1894,13 +2083,13 @@ "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" = "A tárolt fájlok- és a médiatartalmak titkosítása"; +"Encrypt stored files & media" = "Tárolt fájlok és médiatartalmak titkosítása"; /* No comment provided by engineer. */ "Encrypted database" = "Titkosított adatbázis"; @@ -1909,7 +2098,7 @@ "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ázishiba"; @@ -1927,34 +2116,37 @@ "Encrypted message: unexpected error" = "Titkosított üzenet: váratlan hiba"; /* chat item text */ -"encryption agreed" = "titkosítás elfogadva"; +"encryption agreed" = "titkosítása elfogadva"; /* chat item text */ "encryption agreed for %@" = "titkosítás elfogadva %@ számára"; /* chat item text */ -"encryption ok" = "titkosítás rendben"; +"encryption ok" = "titkosítása rendben van"; /* 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"; @@ -1966,19 +2158,19 @@ "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"; @@ -1987,10 +2179,10 @@ "Enter this device name…" = "Adja meg ennek az eszköznek a nevét…"; /* placeholder */ -"Enter welcome message…" = "Üdvözlőüzenet megadása…"; +"Enter welcome message…" = "Adja meg az üdvözlőüzenetet…"; /* placeholder */ -"Enter welcome message… (optional)" = "Üdvözlőüzenet megadása… (nem kötelező)"; +"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…"; @@ -2002,206 +2194,256 @@ "Error" = "Hiba"; /* No comment provided by engineer. */ -"Error aborting address change" = "Hiba a cím 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 a feltételek elfogadásakor"; +"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 kapcsolatkérés elfogadásakor"; - -/* No comment provided by engineer. */ -"Error adding member(s)" = "Hiba a tag(ok) hozzáadásakor"; +"Error accepting contact request" = "Hiba történt a partneri kapcsolatkérés elfogadásakor"; /* alert title */ -"Error adding server" = "Hiba a kiszolgáló hozzáadásakor"; +"Error accepting member" = "Hiba a tag befogadásakor"; /* No comment provided by engineer. */ -"Error changing address" = "Hiba a cím megváltoztatásakor"; - -/* No comment provided by engineer. */ -"Error changing connection profile" = "Hiba a kapcsolati profilra való váltáskor"; - -/* No comment provided by engineer. */ -"Error changing role" = "Hiba a szerepkör megváltoztatásakor"; - -/* No comment provided by engineer. */ -"Error changing setting" = "Hiba a beállítás megváltoztatásakor"; - -/* No comment provided by engineer. */ -"Error changing to incognito!" = "Hiba az inkognitóprofilra való váltáskor!"; - -/* No comment provided by engineer. */ -"Error connecting to forwarding server %@. Please try later." = "Hiba 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 a cím létrehozásakor"; - -/* No comment provided by engineer. */ -"Error creating group" = "Hiba a csoport létrehozásakor"; - -/* No comment provided by engineer. */ -"Error creating group link" = "Hiba a csoporthivatkozás létrehozásakor"; - -/* No comment provided by engineer. */ -"Error creating member contact" = "Hiba az ismerőssel történő kapcsolat létrehozásában"; - -/* No comment provided by engineer. */ -"Error creating message" = "Hiba az üzenet létrehozásakor"; - -/* No comment provided by engineer. */ -"Error creating profile!" = "Hiba a profil létrehozásakor!"; - -/* No comment provided by engineer. */ -"Error decrypting file" = "Hiba a fájl visszafejtésekor"; - -/* No comment provided by engineer. */ -"Error deleting chat database" = "Hiba a csevegési adatbázis törlésekor"; - -/* No comment provided by engineer. */ -"Error deleting chat!" = "Hiba 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 database" = "Hiba az adatbázis törlésekor"; - -/* No comment provided by engineer. */ -"Error deleting old database" = "Hiba a régi adatbázis törlésekor"; - -/* No comment provided by engineer. */ -"Error deleting token" = "Hiba a token törlésekor"; - -/* No comment provided by engineer. */ -"Error deleting user profile" = "Hiba a felhasználó-profil törlésekor"; - -/* No comment provided by engineer. */ -"Error downloading the archive" = "Hiba 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!"; - -/* No comment provided by engineer. */ -"Error enabling notifications" = "Hiba az értesítések engedélyezésekor"; - -/* No comment provided by engineer. */ -"Error encrypting database" = "Hiba az adatbázis titkosításakor"; - -/* No comment provided by engineer. */ -"Error exporting chat database" = "Hiba a csevegési adatbázis exportálásakor"; - -/* No comment provided by engineer. */ -"Error exporting theme: %@" = "Hiba a téma exportálásakor: %@"; - -/* No comment provided by engineer. */ -"Error importing chat database" = "Hiba a csevegési adatbázis importálásakor"; - -/* No comment provided by engineer. */ -"Error joining group" = "Hiba a csoporthoz való csatlakozáskor"; +"Error adding member(s)" = "Hiba történt a tag(ok) hozzáadásakor"; /* alert title */ -"Error loading servers" = "Hiba a kiszolgálók betöltésekor"; +"Error adding server" = "Hiba történt a kiszolgáló hozzáadásakor"; /* No comment provided by engineer. */ -"Error migrating settings" = "Hiba a beallítások átköltöztetésekor"; +"Error adding short link" = "Hiba történt a rövid hivatkozás hozzáadásakor"; /* No comment provided by engineer. */ -"Error opening chat" = "Hiba a csevegés megnyitásakor"; +"Error changing address" = "Hiba történt a cím módosításakor"; /* alert title */ -"Error receiving file" = "Hiba a fájl fogadásakor"; +"Error changing chat profile" = "Hiba a csevegési profil módosításakor"; /* No comment provided by engineer. */ -"Error reconnecting server" = "Hiba a kiszolgálóhoz való újrakapcsolódáskor"; +"Error changing connection profile" = "Hiba történt a kapcsolati profilra való váltáskor"; /* No comment provided by engineer. */ -"Error reconnecting servers" = "Hiba a kiszolgálókhoz való újrakapcsolódáskor"; - -/* No comment provided by engineer. */ -"Error removing member" = "Hiba a tag eltávolításakor"; - -/* No comment provided by engineer. */ -"Error resetting statistics" = "Hiba a statisztikák visszaállításakor"; - -/* No comment provided by engineer. */ -"Error saving group profile" = "Hiba a csoportprofil mentésekor"; - -/* No comment provided by engineer. */ -"Error saving ICE servers" = "Hiba az ICE-kiszolgálók mentésekor"; - -/* No comment provided by engineer. */ -"Error saving passcode" = "Hiba a jelkód mentésekor"; - -/* No comment provided by engineer. */ -"Error saving passphrase to keychain" = "Hiba a jelmondat kulcstartóba történő mentésekor"; +"Error changing role" = "Hiba történt a szerepkör módosításakor"; /* alert title */ -"Error saving servers" = "Hiba a kiszolgálók mentésekor"; +"Error changing setting" = "Hiba történt a beállítás módosításakor"; + +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Hiba történt az inkognitóprofilra való váltáskor!"; + +/* No comment provided by engineer. */ +"Error checking token status" = "Hiba történt a token állapotának ellenőrzésekor"; + +/* alert message */ +"Error connecting to forwarding server %@. Please try later." = "Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később."; + +/* subscription status explanation */ +"Error connecting to the server used to receive messages from this connection: %@" = "Hiba történt a kapcsolódáskor ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál: %@"; + +/* No comment provided by engineer. */ +"Error creating address" = "Hiba történt a cím létrehozásakor"; + +/* No comment provided by engineer. */ +"Error creating group" = "Hiba történt a csoport létrehozásakor"; + +/* No comment provided by engineer. */ +"Error creating group link" = "Hiba történt a csoporthivatkozás létrehozásakor"; + +/* alert title */ +"Error creating list" = "Hiba történt a lista létrehozásakor"; + +/* No comment provided by engineer. */ +"Error creating member contact" = "Hiba történt a partnerrel történő kapcsolat létrehozásában"; + +/* No comment provided by engineer. */ +"Error creating message" = "Hiba történt az üzenet létrehozásakor"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "Hiba történt a profil létrehozásakor!"; + +/* No comment provided by engineer. */ +"Error creating report" = "Hiba történt a jelentés létrehozásakor"; + +/* No comment provided by engineer. */ +"Error decrypting file" = "Hiba történt a fájl visszafejtésekor"; + +/* alert title */ +"Error deleting chat" = "Hiba a csevegés törlésekor"; + +/* alert title */ +"Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor"; + +/* alert title */ +"Error deleting chat!" = "Hiba történt a csevegés törlésekor!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "Hiba történt a kapcsolat törlésekor"; + +/* alert title */ +"Error deleting database" = "Hiba történt az adatbázis törlésekor"; + +/* alert title */ +"Error deleting old database" = "Hiba történt a régi adatbázis törlésekor"; + +/* No comment provided by engineer. */ +"Error deleting token" = "Hiba történt a token törlésekor"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "Hiba történt a felhasználói profil törlésekor"; + +/* No comment provided by engineer. */ +"Error downloading the archive" = "Hiba történt az archívum letöltésekor"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "Hiba történt a kézbesítési jelentések engedélyezésekor!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "Hiba történt az értesítések engedélyezésekor"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "Hiba történt az adatbázis titkosításakor"; + +/* alert title */ +"Error exporting chat database" = "Hiba történt a csevegési adatbázis exportálásakor"; + +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Hiba történt a téma exportálásakor: %@"; + +/* alert title */ +"Error importing chat database" = "Hiba történt a csevegési adatbázis importálásakor"; + +/* No comment provided by engineer. */ +"Error joining group" = "Hiba történt a csoporthoz való csatlakozáskor"; + +/* alert title */ +"Error loading servers" = "Hiba történt a kiszolgálók betöltésekor"; + +/* No comment provided by engineer. */ +"Error migrating settings" = "Hiba történt a beállítások átköltöztetésekor"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Hiba történt a csevegés megnyitásakor"; + +/* No comment provided by engineer. */ +"Error opening group" = "Hiba történt a csoport megnyitásakor"; + +/* alert title */ +"Error receiving file" = "Hiba történt a fájl fogadásakor"; + +/* No comment provided by engineer. */ +"Error reconnecting server" = "Hiba történt a kiszolgálóhoz való újrakapcsolódáskor"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Hiba történt a kiszolgálókhoz való újrakapcsolódáskor"; + +/* alert title */ +"Error registering for notifications" = "Hiba történt az értesítések regisztrálásakor"; + +/* alert title */ +"Error rejecting contact request" = "Hiba történt a partneri kapcsolatkérés elutasításakor"; + +/* alert title */ +"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ói jelszó 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ásakor: %@"; +"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 auto-accept" = "Hiba az automatikus elfogadás beállításakor"; /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Hiba történt a kézbesítési jelentések beállításakor!"; /* 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"; - -/* No comment provided by engineer. */ -"Error switching profile" = "Hiba a profilváltáskor"; - -/* alertTitle */ -"Error switching profile!" = "Hiba a profilváltásakor!"; - -/* No comment provided by engineer. */ -"Error synchronizing connection" = "Hiba a kapcsolat szinkronizálásakor"; - -/* No comment provided by engineer. */ -"Error updating group link" = "Hiba a csoporthivatkozás frissítésekor"; - -/* No comment provided by engineer. */ -"Error updating message" = "Hiba az üzenet frissítésekor"; +"Error stopping chat" = "Hiba történt a csevegés megállításakor"; /* alert title */ -"Error updating server" = "Hiba a kiszolgáló frissítésekor"; +"Error switching profile" = "Hiba történt a profilváltáskor"; + +/* alertTitle */ +"Error switching profile!" = "Hiba történt a profilváltáskor!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "Hiba történt a kapcsolat szinkronizálásakor"; + +/* No comment provided by engineer. */ +"Error testing server connection" = "Hiba történt a kiszolgáló kapcsolatának tesztelésekor"; + +/* No comment provided by engineer. */ +"Error updating group link" = "Hiba történt a csoporthivatkozás frissítésekor"; + +/* No comment provided by engineer. */ +"Error updating message" = "Hiba történt az üzenet frissítésekor"; + +/* alert title */ +"Error updating server" = "Hiba történt a kiszolgáló frissítésekor"; /* No comment provided by engineer. */ "Error updating settings" = "Hiba történt a beállítások frissítésekor"; /* No comment provided by engineer. */ -"Error updating user privacy" = "Hiba a felhasználói adatvédelem 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 hitelesítésekor:"; +"Error verifying passphrase:" = "Hiba történt a jelmondat hitelesítésekor:"; /* No comment provided by engineer. */ "Error: " = "Hiba: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Hiba: %@"; +/* server test error */ +"Error: %@." = "Hiba: %@."; + /* No comment provided by engineer. */ "Error: no database file" = "Hiba: nincs adatbázisfájl"; @@ -2217,9 +2459,6 @@ /* 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"; @@ -2229,6 +2468,9 @@ /* 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"; @@ -2251,31 +2493,43 @@ "Failed to remove passphrase" = "Nem sikerült eltávolítani a jelmondatot"; /* No comment provided by engineer. */ -"Fast and no wait until the sender is online!" = "Gyors és nem kell várni, amíg a feladó online lesz!"; +"Fast and no wait until the sender is online!" = "Gyors és nem kell várni, amíg az üzenetküldő online lesz!"; + +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Gyorsabb csoporttörlés."; /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés."; +/* No comment provided by engineer. */ +"Faster sending messages." = "Gyorsabb üzenetküldés."; + /* swipe action */ "Favorite" = "Kedvenc"; /* No comment provided by engineer. */ +"Favorites" = "Kedvencek"; + +/* file error alert title */ "File error" = "Fájlhiba"; /* alert message */ "File errors:\n%@" = "Fájlhiba:\n%@"; /* file error text */ -"File 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 is blocked by server operator:\n%@." = "A kiszolgáló üzemeltetője letiltotta a fájlt:\n%@."; /* file error text */ -"File server error: %@" = "Fájlkiszolgáló hiba: %@"; +"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állapot"; +"File status" = "Fájl állapota"; /* copied message info */ -"File status: %@" = "Fájlállapot: %@"; +"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."; @@ -2299,13 +2553,16 @@ "Files and media" = "Fájlok és médiatartalmak"; /* No comment provided by engineer. */ -"Files and media are prohibited." = "A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban."; +"Files and media are prohibited in this chat." = "A fájlok és a médiatartalmak küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Files and media not allowed" = "A fájlok- és médiatartalmak nincsenek engedélyezve"; +"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 prohibited!" = "A fájlok- és a médiatartalmak küldése le van tiltva!"; +"Files and media not allowed" = "A fájlok és a médiatartalmak küldése nincs engedélyezve"; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "A fájlok és a médiatartalmak küldése le van tiltva!"; /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Olvasatlan és kedvenc csevegésekre való szűrés."; @@ -2322,6 +2579,18 @@ /* No comment provided by engineer. */ "Find chats faster" = "Csevegési üzenetek gyorsabb megtalálása"; +/* No comment provided by engineer. */ +"Fingerprint in destination server address does not match certificate: %@." = "A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in forwarding server address does not match certificate: %@." = "A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in server address does not match certificate: %@." = "A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; + +/* server test error */ +"Fingerprint in server address does not match certificate." = "A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal."; + /* No comment provided by engineer. */ "Fix" = "Javítás"; @@ -2335,13 +2604,13 @@ "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 better metadata privacy." = "a metaadatok jobb védelme érdekében."; +"For all moderators" = "Az összes moderátor számára"; /* servers error */ "For chat profile %@:" = "A(z) %@ nevű csevegési profilhoz:"; @@ -2350,7 +2619,10 @@ "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 az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni."; +"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"; @@ -2362,7 +2634,7 @@ "Forward" = "Továbbítás"; /* alert title */ -"Forward %d message(s)?" = "%d üzenet továbbítása?"; +"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"; @@ -2371,7 +2643,7 @@ "Forward messages" = "Üzenetek továbbítása"; /* alert message */ -"Forward messages without files?" = "Üzenetek továbbítása fájlok nélkül?"; +"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."; @@ -2383,25 +2655,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 csatlakozni a(z) %@ célkiszolgálóhoz. Próbálja meg később."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "A(z) %1$@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később."; /* No comment provided by engineer. */ -"Forwarding server address is incompatible with network settings: %@." = "A továbbító-kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; +"Forwarding server address is incompatible with network settings: %@." = "A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; /* No comment provided by engineer. */ -"Forwarding server version is incompatible with network settings: %@." = "A továbbító-kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@."; +"Forwarding server version is incompatible with network settings: %@." = "A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@."; /* snd error text */ -"Forwarding server: %@\nDestination server error: %@" = "Továbbító-kiszolgáló: %1$@\nCélkiszolgáló hiba:%2$@"; +"Forwarding server: %@\nDestination server error: %@" = "Továbbítókiszolgáló: %1$@\nCélkiszolgáló-hiba: %2$@"; /* snd error text */ -"Forwarding server: %@\nError: %@" = "Továbbító-kiszolgáló: %1$@\nHiba: %2$@"; +"Forwarding server: %@\nError: %@" = "Továbbítókiszolgáló: %1$@\nHiba: %2$@"; /* No comment provided by engineer. */ "Found desktop" = "Megtalált számítógép"; @@ -2416,14 +2688,17 @@ "Full name (optional)" = "Teljes név (nem kötelező)"; /* No comment provided by engineer. */ -"Fully decentralized – visible only to members." = "Teljesen decentralizált - csak a tagok számára látható."; +"Fully decentralized – visible only to members." = "Teljesen decentralizált – csak a tagok számára látható."; /* No comment provided by engineer. */ -"Fully re-implemented - work in background!" = "Teljesen újra implementálva - háttérben történő működés!"; +"Fully re-implemented - work in background!" = "Teljesen újra implementálva – háttérben történő működés!"; /* No comment provided by engineer. */ "Further reduced battery usage" = "Tovább csökkentett akkumulátor-használat"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Kapjon értesítést, ha megemlítik."; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-ek és matricák"; @@ -2433,13 +2708,16 @@ /* message preview */ "Good morning!" = "Jó reggelt!"; +/* shown on group welcome message */ +"group" = "csoport"; + /* No comment provided by engineer. */ "Group" = "Csoport"; /* No comment provided by engineer. */ "Group already exists" = "A csoport már létezik"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "A csoport már létezik!"; /* No comment provided by engineer. */ @@ -2449,7 +2727,7 @@ "Group display name" = "A csoport megjelenített neve"; /* No comment provided by engineer. */ -"Group full name (optional)" = "Csoport teljes neve (nem kötelező)"; +"Group full name (optional)" = "A csoport teljes neve (nem kötelező)"; /* No comment provided by engineer. */ "Group image" = "Csoport profilképe"; @@ -2463,6 +2741,9 @@ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "A csoportmeghívó már nem érvényes, a küldője eltávolította."; +/* No comment provided by engineer. */ +"group is deleted" = "csoport törölve"; + /* No comment provided by engineer. */ "Group link" = "Csoporthivatkozás"; @@ -2473,7 +2754,7 @@ "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" = "Csoportbeállítások"; @@ -2482,23 +2763,32 @@ "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."; +"Group profile is stored on members' devices, not on the servers." = "A csoportprofil a tagok eszközein tárolódik, nem a kiszolgálókon."; /* snd group event chat item */ "group profile updated" = "csoportprofil frissítve"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "Csoportprofil módosítva. Ha menti, akkor a frissített profil el lesz küldve a csoporttagoknak."; + /* No comment provided by engineer. */ "Group welcome message" = "A csoport üdvözlőüzenete"; /* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "A csoport törlésre kerül az összes 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. */ +"Groups" = "Csoportok"; /* No comment provided by engineer. */ "Help" = "Súgó"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Segítsen az adminisztrátoroknak a csoportjaik moderálásában."; + /* No comment provided by engineer. */ "Hidden" = "Se név, se üzenet"; @@ -2518,13 +2808,13 @@ "Hide profile" = "Profil elrejtése"; /* No comment provided by engineer. */ -"Hide:" = "Elrejtés:"; +"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"; @@ -2535,6 +2825,9 @@ /* No comment provided by engineer. */ "How it helps privacy" = "Hogyan segíti az adatvédelmet"; +/* alert button */ +"How it works" = "Hogyan működik"; + /* No comment provided by engineer. */ "How SimpleX works" = "Hogyan működik a SimpleX"; @@ -2542,10 +2835,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" = "Saját 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"; @@ -2557,13 +2850,13 @@ "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 eltávolításra kerül!"; +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen el lesz távolítva!"; /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot:"; /* No comment provided by engineer. */ -"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ha most kell használnia a csevegést, koppintson alább a **Befejezés később** lehetőségre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis átköltözteté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" = "Mellőzés"; @@ -2578,19 +2871,19 @@ "Immediately" = "Azonnal"; /* No comment provided by engineer. */ -"Immune to spam" = "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"; /* No comment provided by engineer. */ -"Import failed" = "Sikertelen importálás"; +"Import failed" = "Nem sikerült az importálás"; /* No comment provided by engineer. */ "Import theme" = "Téma importálása"; @@ -2605,16 +2898,16 @@ "Improved message delivery" = "Továbbfejlesztett üzenetkézbesítés"; /* No comment provided by engineer. */ -"Improved privacy and security" = "Fejlesztett adatvédelem és biztonság"; +"Improved privacy and security" = "Továbbfejlesztett adatvédelem és biztonság"; /* No comment provided by engineer. */ -"Improved server configuration" = "Javított kiszolgáló konfiguráció"; +"Improved server configuration" = "Továbbfejlesztett kiszolgálókonfiguráció"; /* No comment provided by engineer. */ "In order to continue, chat should be stopped." = "A folytatáshoz a csevegést meg kell szakítani."; /* No comment provided by engineer. */ -"In reply to" = "Válasz neki"; +"In reply to" = "Válaszul erre"; /* No comment provided by engineer. */ "In-call sounds" = "Bejövő hívás csengőhangja"; @@ -2622,6 +2915,12 @@ /* 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ó"; @@ -2632,7 +2931,7 @@ "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 az összes ismerőséhez ú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 címhivatkozáson keresztül"; @@ -2641,7 +2940,7 @@ "incognito via group link" = "inkognitó a csoporthivatkozáson keresztül"; /* chat list item description */ -"incognito via one-time link" = "inkognitó egy egyszer használható meghívó-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"; @@ -2677,10 +2976,10 @@ "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" = "Azonnal"; +"Instant" = "Azonnali"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések elrejtésre kerülnek!\n"; +"Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések el lesznek rejtve!\n"; /* No comment provided by engineer. */ "Interface" = "Kezelőfelület"; @@ -2688,6 +2987,21 @@ /* 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"; @@ -2703,7 +3017,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Érvénytelen megjelenítendő név!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Érvénytelen hivatkozás"; /* No comment provided by engineer. */ @@ -2728,7 +3042,7 @@ "Invitation expired!" = "A meghívó lejárt!"; /* group name */ -"invitation to group %@" = "meghívás a(z) %@ csoportba"; +"invitation to group %@" = "meghívás a(z) %@ nevű csoportba"; /* No comment provided by engineer. */ "invite" = "meghívás"; @@ -2752,16 +3066,16 @@ "invited %@" = "meghívta őt: %@"; /* chat list item title */ -"invited to connect" = "meghívta, hogy csatlakozzon"; +"invited to connect" = "függőben lévő kapcsolat"; /* rcv group event chat item */ -"invited via your group link" = "meghíva az Ön csoporthivatkozá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 kulcstartó 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 kulcstartó 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"; @@ -2773,16 +3087,16 @@ "Irreversible message deletion is prohibited in this chat." = "Az üzenetek végleges törlése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited." = "Az üzenetek végleges törlése le van tiltva ebben a csoportban."; +"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 névtelen 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 az ismerőse 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 egy régi adatbázis biztonsági mentését használta."; /* 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 egy régi adatbázis biztonsági mentését használta.\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."; @@ -2803,31 +3117,25 @@ "Join" = "Csatlakozás"; /* No comment provided by engineer. */ -"join as %@" = "csatlakozás mint: %@"; +"Join as %@" = "Csatlakozás mint: %@"; + +/* new chat sheet title */ +"Join group" = "Csatlakozás a csoporthoz"; /* No comment provided by engineer. */ -"Join group" = "Csatlakozás csoporthoz"; - -/* No comment provided by engineer. */ -"Join group conversations" = "Csatlakozás csoportos beszélgetésekhez"; - -/* No comment provided by engineer. */ -"Join group?" = "Csatlakozik a csoporthoz?"; +"Join group conversations" = "Csatlakozás a csoportbeszélgetésekhez"; /* No comment provided by engineer. */ "Join incognito" = "Csatlakozás inkognitóban"; -/* No comment provided by engineer. */ -"Join with current profile" = "Csatlakozás a jelenlegi profillal"; - -/* No comment provided by engineer. */ -"Join your group?\nThis is your link for group %@!" = "Csatlakozik a csoportjához?\nEz az Ön hivatkozása a(z) %@ nevű csoporthoz!"; +/* new chat action */ +"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"; +"Keep" = "Megtartás"; /* No comment provided by engineer. */ "Keep conversation" = "Beszélgetés megtartása"; @@ -2836,7 +3144,10 @@ "Keep the app open to use it from desktop" = "A számítógépről való használathoz tartsd nyitva az alkalmazást"; /* alert title */ -"Keep unused invitation?" = "Fel nem használt meghívó megtartása?"; +"Keep unused invitation?" = "Megtartja a fel nem használt meghívót?"; + +/* No comment provided by engineer. */ +"Keep your chats clean" = "Tartsa tisztán a csevegéseit"; /* No comment provided by engineer. */ "Keep your connections" = "Kapcsolatok megtartása"; @@ -2860,17 +3171,20 @@ "Leave chat" = "Csevegés elhagyása"; /* No comment provided by engineer. */ -"Leave chat?" = "Csevegés elhagyása?"; +"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"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Kevesebb adatforgalom a mobilhálózatokon."; + /* email subject */ "Let's talk in SimpleX Chat" = "Beszélgessünk a SimpleX Chatben"; @@ -2889,6 +3203,15 @@ /* No comment provided by engineer. */ "Linked desktops" = "Társított számítógépek"; +/* swipe action */ +"List" = "Lista"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Az összes lista nevének és emodzsijának különbözőnek kell lennie."; + +/* No comment provided by engineer. */ +"List name..." = "Lista neve…"; + /* No comment provided by engineer. */ "LIVE" = "ÉLŐ"; @@ -2898,6 +3221,9 @@ /* No comment provided by engineer. */ "Live messages" = "Élő üzenetek"; +/* in progress text */ +"Loading profile…" = "Profil betöltése…"; + /* No comment provided by engineer. */ "Local name" = "Helyi név"; @@ -2917,7 +3243,7 @@ "Make profile private!" = "Tegye priváttá a profilját!"; /* No comment provided by engineer. */ -"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva."; +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva."; /* No comment provided by engineer. */ "Mark deleted for everyone" = "Jelölje meg az összes tag számára töröltként"; @@ -2938,7 +3264,7 @@ "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"; +"Media & file servers" = "Fájl- és médiakiszolgálók"; /* blur media */ "Medium" = "Közepes"; @@ -2949,50 +3275,74 @@ /* No comment provided by engineer. */ "Member" = "Tag"; +/* past/unknown group member */ +"Member %@" = "%@ ismeretlen vagy már nem 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$@"; + +/* No comment provided by engineer. */ +"Member admission" = "Tagbefogadás"; /* rcv group event chat item */ "member connected" = "kapcsolódott"; +/* No comment provided by engineer. */ +"member has old version" = "a tag régi verziót használ"; + /* item status text */ "Member inactive" = "Inaktív tag"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "A tag szerepeköre meg fog változni a következőre: \"%@\". A csevegés tagjai értesítést fognak kapni."; +"Member is deleted - can't accept request" = "A tag törölve lett – nem lehet elfogadni a kérést"; + +/* chat feature */ +"Member reports" = "Tagok jelentései"; /* 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 csoportban az összes tag értesítve lesz."; +"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 \"%@\". 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 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 will be removed from chat - this cannot be undone!" = "A tag el lesz távolítva a csevegésből - ezt a műveletet nem lehet visszavonni!"; +"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 group - this cannot be undone!" = "A tag eltávolítása a csoportból - ez a művelet nem vonható vissza!"; +"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. */ -"Members can add message reactions." = "Csoporttagok üzenetreakciókat adhatnak hozzá."; +"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!"; + +/* alert message */ +"Member will join the group, accept member?" = "A tag csatlakozni akar a csoporthoz, befogadja a tagot?"; /* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; +"Members can add message reactions." = "A tagok reakciókat adhatnak hozzá az üzenetekhez."; /* No comment provided by engineer. */ -"Members can send direct messages." = "A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket."; +"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 send disappearing messages." = "A csoport tagjai küldhetnek eltűnő üzeneteket."; +"Members can report messsages to moderators." = "A tagok jelenthetik az üzeneteket a moderátorok felé."; /* No comment provided by engineer. */ -"Members can send files and media." = "A csoport tagjai küldhetnek fájlokat és médiatartalmakat."; +"Members can send direct messages." = "A tagok küldhetnek egymásnak közvetlen üzeneteket."; /* No comment provided by engineer. */ -"Members can send SimpleX links." = "A csoport tagjai küldhetnek SimpleX-hivatkozásokat."; +"Members can send disappearing messages." = "A tagok küldhetnek eltűnő üzeneteket."; /* No comment provided by engineer. */ -"Members can send voice messages." = "A csoport tagjai küldhetnek hangüzeneteket."; +"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"; @@ -3015,41 +3365,44 @@ /* item status text */ "Message forwarded" = "Továbbított üzenet"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Az üzenet azonnal megjelenik, amint a kapcsolódás gombra koppint."; + /* 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" = "Üzenet-sorbaállítási információ"; +"Message queue info" = "Üzenet várólista információi"; /* chat feature */ "Message reactions" = "Üzenetreakciók"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this chat." = "Az üzenetreakciók küldése le van tiltva ebben a csevegésben."; +"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." = "Az üzenetreakciók küldése le van tiltva ebben a csoportban."; +"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"; +"Message reception" = "Üzenetfogadás"; /* No comment provided by engineer. */ "Message servers" = "Üzenetkiszolgálók"; /* No comment provided by engineer. */ -"Message shape" = "Üzenetbuborék formája"; +"Message shape" = "Üzenetbuborék alakja"; /* No comment provided by engineer. */ "Message source remains private." = "Az üzenet forrása titokban marad."; /* No comment provided by engineer. */ -"Message status" = "Üzenetállapot"; +"Message status" = "Üzenet állapota"; /* copied message info */ -"Message status: %@" = "Üzenetállapot: %@"; +"Message status: %@" = "Üzenet állapota: %@"; /* No comment provided by engineer. */ "Message text" = "Név és üzenet"; @@ -3064,7 +3417,13 @@ "Messages & files" = "Üzenetek és fájlok"; /* No comment provided by engineer. */ -"Messages from %@ will be shown!" = "A(z) %@ által írt üzenetek megjelennek!"; +"Messages are protected by **end-to-end encryption**." = "Az üzenetek **végpontok közötti titkosítással** vannak védve."; + +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "%@ összes üzenete meg fog jelenni!"; + +/* alert message */ +"Messages in this chat will never be deleted." = "Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve."; /* No comment provided by engineer. */ "Messages received" = "Fogadott üzenetek"; @@ -3073,13 +3432,13 @@ "Messages sent" = "Elküldött üzenetek"; /* alert message */ -"Messages were deleted after you selected them." = "Az üzeneteket törölték miután kiválasztotta őket."; +"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 üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással**, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi."; +"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve."; /* 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érülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi."; +"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve."; /* No comment provided by engineer. */ "Migrate device" = "Eszköz átköltöztetése"; @@ -3106,7 +3465,7 @@ "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 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)."; @@ -3130,17 +3489,23 @@ "moderated" = "moderált"; /* No comment provided by engineer. */ -"Moderated at" = "Moderálva ekkor:"; +"Moderated at" = "Moderálva"; /* copied message info */ -"Moderated at: %@" = "Moderálva ekkor: %@"; +"Moderated at: %@" = "Moderálva: %@"; /* marked deleted chat item preview text */ -"moderated by %@" = "moderálva lett %@ által"; +"moderated by %@" = "%@ moderálta ezt az üzenetet"; + +/* member role */ +"moderator" = "moderátor"; /* 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!"; @@ -3151,17 +3516,17 @@ "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. */ -"Multiple chat profiles" = "Több csevegőprofil"; +"Multiple chat profiles" = "Több csevegési profil"; -/* No comment provided by engineer. */ -"mute" = "némítás"; - -/* swipe action */ +/* notification label action */ "Mute" = "Némítás"; +/* notification label action */ +"Mute all" = "Összes némítása"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Némítás, ha inaktív!"; @@ -3172,37 +3537,40 @@ "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."; +"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ózati üzemeltető"; +"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. */ +/* alert title */ "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 kapcsolatkérés"; +"New contact request" = "Új partneri kapcsolatkérés"; /* notification */ "New contact:" = "Új kapcsolat:"; @@ -3211,11 +3579,14 @@ "New desktop app!" = "Új számítógép-alkalmazás!"; /* No comment provided by engineer. */ -"New display name" = "Új megjelenítési név"; +"New display name" = "Új megjelenítendő név"; /* notification */ "New events" = "Új események"; +/* No comment provided by engineer. */ +"New group role: Moderator" = "Új szerepkör: Moderátor"; + /* No comment provided by engineer. */ "New in %@" = "Újdonságok a(z) %@ verzióban"; @@ -3225,6 +3596,9 @@ /* No comment provided by engineer. */ "New member role" = "Új tag szerepköre"; +/* rcv group event chat item */ +"New member wants to join the group." = "Új tag szeretne csatlakozni a csoporthoz."; + /* notification */ "new message" = "új üzenet"; @@ -3241,10 +3615,10 @@ "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."; +"New SOCKS credentials will be used every time you start the app." = "Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítési adatok lesznek használva."; /* 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."; +"New SOCKS credentials will be used for each server." = "Az összes kiszolgálóhoz új, SOCKS-hitelesítési adatok lesznek használva."; /* pref value */ "no" = "nem"; @@ -3256,16 +3630,28 @@ "No app password" = "Nincs alkalmazás jelszó"; /* No comment provided by engineer. */ -"No contacts selected" = "Nincs kiválasztva ismerős"; +"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 chats with members" = "Nincsenek csevegések a tagokkal"; + +/* 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 kiszüléktoken!"; +"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."; @@ -3286,10 +3672,13 @@ "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 media & file servers." = "Nincsenek fájl- és médiakiszolgálók."; + +/* No comment provided by engineer. */ +"No message" = "Nincs üzenet"; /* servers error */ -"No message servers." = "Nincsenek üzenet-kiszolgálók."; +"No message servers." = "Nincsenek üzenetkiszolgálók."; /* No comment provided by engineer. */ "No network connection" = "Nincs hálózati kapcsolat"; @@ -3303,6 +3692,9 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Nincs engedély a hangüzenet rögzítésére"; +/* alert title */ +"No private routing session" = "Nincs privát útválasztási munkamenet"; + /* No comment provided by engineer. */ "No push server" = "Helyi"; @@ -3313,25 +3705,40 @@ "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ó-kiszolgálók."; +"No servers to receive files." = "Nincsenek fájlfogadási kiszolgálók."; /* servers error */ -"No servers to receive messages." = "Nincsenek üzenetfogadó-kiszolgálók."; +"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."; +"No servers to send files." = "Nincsenek fájlküldési kiszolgálók."; + +/* No comment provided by engineer. */ +"no subscription" = "nincs előfizetés"; /* copied message info in history */ "no text" = "nincs szöveg"; +/* alert title */ +"No token!" = "Nincs token!"; + /* No comment provided by engineer. */ -"No user identifiers." = "Nincsenek felhasználó-azonosítók."; +"No unread chats" = "Nincsenek olvasatlan csevegések"; + +/* No comment provided by engineer. */ +"No user identifiers." = "Nincsenek felhasználói azonosítók."; /* No comment provided by engineer. */ "Not compatible!" = "Nem kompatibilis!"; /* No comment provided by engineer. */ -"Nothing selected" = "Nincs kiválasztva semmi"; +"not synchronized" = "nincs szinkronizálva"; + +/* 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!"; @@ -3342,9 +3749,15 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Az értesítések le vannak tiltva!"; +/* alert title */ +"Notifications error" = "Értesítési hiba"; + /* No comment provided by engineer. */ "Notifications privacy" = "Értesítési adatvédelem"; +/* alert title */ +"Notifications status" = "Értesítések állapota"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat („megfigyelő” szerepkör)"; @@ -3352,20 +3765,23 @@ "observer" = "megfigyelő"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "kikapcsolva"; /* blur media */ "Off" = "Kikapcsolva"; /* feature offered item */ -"offered %@" = "%@ ajánlotta"; +"offered %@" = "felajánlotta a következőt: %@"; /* feature offered item */ -"offered %@: %@" = "ajánlotta %1$@: %2$@-kor"; +"offered %@: %@" = "felajánlotta a következőt: %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Rendben"; /* No comment provided by engineer. */ @@ -3378,40 +3794,46 @@ "on" = "bekapcsolva"; /* No comment provided by engineer. */ -"One-time invitation link" = "Egyszer használható 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.\nRequires compatible VPN." = "Onion-kiszolgálók **szükségesek** a kapcsolódáshoz.\nKompatibilis VPN 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.\nRequires compatible VPN." = "Onion-kiszolgálók használata, ha azok rendelkezésre állnak.\nVPN 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 chat owners can change preferences." = "Csak a csevegés tulajdonosai módosíthatják a beállításokat."; +"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." = "Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket."; +"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."; +"Only group owners can change group preferences." = "Csak a csoport tulajdonosai módosíthatják a csoportbeállításokat."; /* No comment provided by engineer. */ -"Only group owners can enable files and media." = "Csak a csoporttulajdonosok engedélyezhetik a fájlok- és a médiatartalmak küldését."; +"Only group owners can enable files and media." = "Csak a csoport tulajdonosai engedélyezhetik a fájlok és a médiatartalmak küldését."; /* No comment provided by engineer. */ -"Only group owners can enable voice messages." = "Csak a csoporttulajdonosok engedélyezhetik a hangüzenetek küldését."; +"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 add message reactions." = "Csak Ön adhat hozzá üzenetreakciókat."; +"Only sender and moderators see it" = "Csak az üzenet küldője és a moderátorok látják"; /* 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 you and moderators see it" = "Csak Ön és a moderátorok látják"; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Csak Ön adhat hozzá reakciókat az üzenetekhez."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra)"; /* No comment provided by engineer. */ "Only you can make calls." = "Csak Ön tud hívásokat indítani."; @@ -3419,48 +3841,81 @@ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "Csak Ön tud eltűnő üzeneteket küldeni."; +/* No comment provided by engineer. */ +"Only you can send files and media." = "Csak Ön küldhet fájlokat és médiatartalmakat."; + /* No comment provided by engineer. */ "Only you can send voice messages." = "Csak Ön tud hangüzeneteket küldeni."; /* No comment provided by engineer. */ -"Only your contact can add message reactions." = "Csak az ismerőse tud üzenetreakciókat 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 az ismerőse tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra)"; +"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 az ismerőse tud hívást indítani."; +"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 az ismerőse tud eltűnő üzeneteket küldeni."; +"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 az ismerőse tud hangüzeneteket küldeni."; +"Only your contact can send files and media." = "Csak a partnere küldhet fájlokat és médiatartalmakat."; /* 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" = "Változások megnyitása"; +"Open changes" = "Módosítások megtekintése"; -/* No comment provided by engineer. */ +/* new chat action */ "Open chat" = "Csevegés megnyitása"; /* authentication reason */ -"Open chat console" = "Csevegés konzol megnyitása"; +"Open chat console" = "Csevegési konzol megnyitása"; + +/* alert action */ +"Open clean link" = "Tiszta hivatkozás megnyitása"; /* No comment provided by engineer. */ "Open conditions" = "Feltételek megnyitása"; -/* No comment provided by engineer. */ +/* alert action */ +"Open full link" = "Teljes hivatkozás megnyitása"; + +/* new chat action */ "Open group" = "Csoport megnyitása"; +/* alert title */ +"Open link?" = "Megnyitja a hivatkozást?"; + /* 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"; + +/* new chat action */ +"Open new chat" = "Új csevegés megnyitása"; + +/* new chat action */ +"Open new group" = "Új csoport megnyitása"; /* No comment provided by engineer. */ "Open Settings" = "Beállítások megnyitása"; +/* No comment provided by engineer. */ +"Open to accept" = "Megnyitás az elfogadáshoz"; + +/* No comment provided by engineer. */ +"Open to connect" = "Megnyitás a kapcsolódáshoz"; + +/* No comment provided by engineer. */ +"Open to join" = "Megnyitás a csatlakozáshoz"; + +/* No comment provided by engineer. */ +"Open to use bot" = "Megnyitás a bot használatához"; + /* No comment provided by engineer. */ "Opening app…" = "Az alkalmazás megnyitása…"; @@ -3468,7 +3923,7 @@ "Operator" = "Üzemeltető"; /* alert title */ -"Operator server" = "Kiszolgáló üzemeltető"; +"Operator server" = "Kiszolgáló-üzemeltető"; /* No comment provided by engineer. */ "Or import archive file" = "Vagy archívumfájl importálása"; @@ -3488,6 +3943,9 @@ /* 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"; @@ -3510,13 +3968,13 @@ "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!"; @@ -3525,10 +3983,7 @@ "Password" = "Jelszó"; /* No comment provided by engineer. */ -"Password to show" = "Jelszó megjelenítése"; - -/* past/unknown group member */ -"Past member %@" = "(Már nem tag) %@"; +"Password to show" = "Jelszó a megjelenítéshez"; /* No comment provided by engineer. */ "Paste desktop address" = "Számítógép címének beillesztése"; @@ -3543,13 +3998,22 @@ "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. */ +"pending" = "függőben"; /* No comment provided by engineer. */ "Pending" = "Függőben"; /* No comment provided by engineer. */ -"Periodic" = "Rendszeresen"; +"pending approval" = "jóváhagyásra vár"; + +/* No comment provided by engineer. */ +"pending review" = "függőben lévő áttekintés"; + +/* No comment provided by engineer. */ +"Periodic" = "Időszakos"; /* message decrypt error item */ "Permanent decryption error" = "Végleges visszafejtési hiba"; @@ -3567,22 +4031,22 @@ "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 az ismerősét, hogy engedélyezze a hívásokat."; +"Please ask your contact to enable calls." = "Kérje meg a partnerét, hogy engedélyezze a hívásokat."; /* No comment provided by engineer. */ -"Please ask your contact to enable sending voice messages." = "Kérje meg az ismerősét, hogy engedélyezze a hangüzenetek küldését."; +"Please ask your contact to enable sending voice messages." = "Kérje meg a partnerét, hogy engedélyezze a hangüzenetek küldését."; /* No comment provided by engineer. */ "Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Ellenőrizze, hogy a hordozható eszköz és a számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint a számítógép tűzfalában engedélyezve van-e a kapcsolat.\nMinden további problémát osszon meg a fejlesztőkkel."; /* No comment provided by engineer. */ -"Please check that you used the correct link or ask your contact to send you another one." = "Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg az ismerősét, hogy küldjön egy másikat."; +"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."; + +/* alert message */ +"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 your network connection with %@ and try again." = "Ellenőrizze a hálózati kapcsolatát a következővel: %@, és próbálja újra."; - -/* No comment provided by engineer. */ -"Please check yours and your contact preferences." = "Ellenőrizze a saját- és az ismerőse beállításait."; +"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."; @@ -3594,13 +4058,13 @@ "Please contact group admin." = "Lépjen kapcsolatba a csoport adminisztrátorával."; /* No comment provided by engineer. */ -"Please enter correct current passphrase." = "Adja meg a helyes, jelenlegi 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."; @@ -3612,7 +4076,19 @@ "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."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérését."; + +/* 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"; @@ -3620,14 +4096,11 @@ /* No comment provided by engineer. */ "Port" = "Port"; -/* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt."; /* No comment provided by engineer. */ -"Preset server address" = "Előre beállított kiszolgáló címe"; +"Preset server address" = "Az előre beállított kiszolgáló címe"; /* No comment provided by engineer. */ "Preset servers" = "Előre beállított kiszolgálók"; @@ -3642,14 +4115,23 @@ "Privacy & security" = "Adatvédelem és biztonság"; /* No comment provided by engineer. */ -"Privacy for your customers." = "Az Ön ügyfeleinek adatvédelme."; +"Privacy for your customers." = "Saját ügyfeleinek adatvédelme."; /* No comment provided by engineer. */ -"Privacy redefined" = "Adatvédelem újraértelmezve"; +"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 kiszolgálók ü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"; @@ -3662,8 +4144,11 @@ /* No comment provided by engineer. */ "Private routing" = "Privát útválasztás"; -/* No comment provided by engineer. */ -"Private routing error" = "Privát útválasztáshiba"; +/* alert title */ +"Private routing error" = "Privát útválasztási hiba"; + +/* alert title */ +"Private routing timeout" = "Privát útválasztás időtúllépése"; /* No comment provided by engineer. */ "Profile and server connections" = "Profil és kiszolgálókapcsolatok"; @@ -3681,19 +4166,22 @@ "Profile theme" = "Profiltéma"; /* alert message */ -"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 update will be sent to your contacts." = "A profilfrissítés el lesz küldve a partnerei számára."; /* No comment provided by engineer. */ "Prohibit audio/video calls." = "A hívások kezdeményezése le van tiltva."; /* No comment provided by engineer. */ -"Prohibit irreversible message deletion." = "Az ü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." = "Az üzenetreakciók küldése le van tiltva."; +"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 reporting messages to moderators." = "Az üzenetek a moderátorok felé történő jelentésének megtiltása."; /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között le van tiltva."; @@ -3702,7 +4190,7 @@ "Prohibit sending disappearing messages." = "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édiatartalmak küldésének letiltása."; +"Prohibit sending files and media." = "A fájlok és a médiatartalmak küldése le van tiltva."; /* No comment provided by engineer. */ "Prohibit sending SimpleX links." = "A SimpleX-hivatkozások küldése le van tiltva."; @@ -3720,13 +4208,16 @@ "Protect your chat profiles with a password!" = "Védje meg a csevegési profiljait egy jelszóval!"; /* No comment provided by engineer. */ -"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben.\nEngedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben."; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben."; + +/* No comment provided by engineer. */ +"Protocol background timeout" = "Protokoll időtúllépése a háttérben"; /* No comment provided by engineer. */ "Protocol timeout" = "Protokoll időtúllépése"; /* No comment provided by engineer. */ -"Protocol timeout per KB" = "Protokoll időtúllépése KB-onként"; +"Protocol timeout per KB" = "Protokoll időtúllépése kB-onként"; /* No comment provided by engineer. */ "Proxied" = "Proxyzott"; @@ -3744,16 +4235,16 @@ "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"; +"Reachable chat toolbar" = "Könnyen elérhető csevegési eszköztár"; /* chat item menu */ "React…" = "Reagálj…"; @@ -3774,7 +4265,7 @@ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; /* No comment provided by engineer. */ -"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "További információ a [GitHub tárolóban](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "További információ a [GitHub-tárolónkban](https://github.com/simplex-chat/simplex-chat#readme)."; /* No comment provided by engineer. */ "Receipts are disabled" = "A kézbesítési jelentések le vannak tiltva"; @@ -3786,17 +4277,14 @@ "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ájlesemény"; - /* message info title */ "Received message" = "Fogadott üzenetbuborék színe"; @@ -3810,13 +4298,13 @@ "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."; +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Az üzenetfogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása akkor fejeződik be, amikor az üzenetküldési kiszolgáló online lesz."; /* 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"; +"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)."; @@ -3837,62 +4325,81 @@ "Reconnect all servers" = "Újrakapcsolódás az összes kiszolgálóhoz"; /* No comment provided by engineer. */ -"Reconnect all servers?" = "Újrakapcsolódás az összes kiszolgálóhoz?"; +"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ódás a kiszolgálóhoz?"; +"Reconnect server?" = "Újrakapcsolódik a kiszolgálóhoz?"; /* No comment provided by engineer. */ -"Reconnect servers?" = "Újrakapcsolódás a kiszolgálókhoz?"; +"Reconnect servers?" = "Újrakapcsolódik a kiszolgálókhoz?"; /* No comment provided by engineer. */ -"Record updated at" = "A bejegyzés frissítve"; +"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átor-használat"; -/* reject incoming call via notification - swipe action */ +/* 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"; + +/* alert action +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)"; +"Reject (sender NOT notified)" = "Elutasítás (a kérés küldője NEM fog értesítést kapni)"; + +/* alert title */ +"Reject contact request" = "Partneri kapcsolatkérés elutasítása"; + +/* alert title */ +"Reject member?" = "Elutasítja a tagot?"; /* No comment provided by engineer. */ -"Reject contact request" = "Kapcsolatkérés elutasítása"; +"rejected" = "elutasítva"; /* call status */ "rejected call" = "elutasított hívás"; /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "A közvetítő-kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet."; +"Relay server is only used if necessary. Another party can observe your IP address." = "A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét."; /* No comment provided by engineer. */ -"Relay server protects your IP address, but it can observe the duration of the call." = "A közvetítő-kiszolgáló megvédi az IP-címet, 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 IP-címét, de megfigyelheti a hívás időtartamát."; /* No comment provided by engineer. */ "Remove" = "Eltávolítás"; /* No comment provided by engineer. */ -"Remove archive?" = "Archívum eltávolítása?"; +"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 link tracking" = "Nyomonkövetési paraméterek eltávolítása a hivatkozásokból"; + /* No comment provided by engineer. */ "Remove member" = "Eltávolítás"; /* No comment provided by engineer. */ -"Remove member?" = "Biztosan eltávolítja?"; +"Remove member?" = "Eltávolítja a tagot?"; /* No comment provided by engineer. */ -"Remove passphrase from keychain?" = "Jelmondat eltávolítása a kulcstartóból?"; +"Remove passphrase from keychain?" = "Eltávolítja a jelmondatot a kulcstartóból?"; /* No comment provided by engineer. */ "removed" = "eltávolítva"; @@ -3903,6 +4410,9 @@ /* profile update event chat item */ "removed contact address" = "eltávolította a kapcsolattartási címet"; +/* No comment provided by engineer. */ +"removed from group" = "eltávolítva a csoportból"; + /* profile update event chat item */ "removed profile picture" = "eltávolította a profilképét"; @@ -3910,16 +4420,16 @@ "removed you" = "eltávolította Önt"; /* No comment provided by engineer. */ -"Renegotiate" = "Újraegyzetetés"; +"Removes messages and blocks members." = "Üzenetek eltávolítása és a tagok tiltása."; + +/* No comment provided by engineer. */ +"Renegotiate" = "Újraegyeztetés"; /* No comment provided by engineer. */ "Renegotiate encryption" = "Titkosítás újraegyeztetése"; /* No comment provided by engineer. */ -"Renegotiate encryption?" = "Titkosítás újraegyeztetése?"; - -/* No comment provided by engineer. */ -"Repeat connection request?" = "Kapcsolatkérés megismétlése?"; +"Renegotiate encryption?" = "Újraegyezteti a titkosítást?"; /* No comment provided by engineer. */ "Repeat download" = "Letöltés ismét"; @@ -3927,17 +4437,59 @@ /* No comment provided by engineer. */ "Repeat import" = "Importálás ismét"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Csatlakozáskérés megismétlése?"; - /* No comment provided by engineer. */ "Repeat upload" = "Feltöltés ismét"; /* chat item action */ "Reply" = "Válasz"; +/* chat item action */ +"Report" = "Jelentés"; + +/* report reason */ +"Report content: only group moderators will see it." = "Tartalom jelentése: csak a csoport moderátorai látják."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Tag profiljának jelentése: csak a csoport moderátorai látják."; + +/* report reason */ +"Report other: only group moderators will see it." = "Egyéb jelentés: csak a csoport moderátorai látják."; + +/* No comment provided by engineer. */ +"Report reason?" = "Jelentés indoklása?"; + +/* alert title */ +"Report sent to moderators" = "A jelentés el lett küldve a moderátoroknak"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Kéretlen tartalom jelentése: csak a csoport moderátorai látják."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Szabálysértés jelentése: csak a csoport moderátorai látják."; + +/* report in notification */ +"Report: %@" = "Jelentés: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Az üzenetek jelentése a moderátorok felé le van tiltva."; + +/* No comment provided by engineer. */ +"Reports" = "Jelentések"; + +/* No comment provided by engineer. */ +"request is sent" = "kérés elküldve"; + +/* No comment provided by engineer. */ +"request to join rejected" = "csatlakozási kérés elutasítva"; + +/* rcv group event chat item */ +"requested connection" = "partneri kapcsolatot kért"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "a(z) %@ nevű csoportból partneri kapcsolatot kért"; + /* chat list item title */ -"requested to connect" = "kérelmezve a kapcsolódáshoz"; +"requested to connect" = "függőben lévő kapcsolat"; /* No comment provided by engineer. */ "Required" = "Szükséges"; @@ -3952,7 +4504,7 @@ "Reset all statistics" = "Az összes statisztika visszaállítása"; /* No comment provided by engineer. */ -"Reset all statistics?" = "Az összes statisztika visszaállítása?"; +"Reset all statistics?" = "Visszaállítja az összes statisztikát?"; /* No comment provided by engineer. */ "Reset colors" = "Színek visszaállítása"; @@ -3979,22 +4531,34 @@ "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. */ +/* alert action */ "Retry" = "Újrapróbálkozás"; /* chat item action */ "Reveal" = "Felfedés"; +/* No comment provided by engineer. */ +"review" = "áttekintés"; + /* No comment provided by engineer. */ "Review conditions" = "Feltételek felülvizsgálata"; /* No comment provided by engineer. */ -"Review later" = "Felülvizsgálat később"; +"Review group members" = "Csoporttagok áttekintése"; + +/* admission stage */ +"Review members" = "Tagok áttekintése"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Tagok áttekintése a befogadás előtt (kopogtatás)."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "áttekintve a moderátorok által"; /* No comment provided by engineer. */ "Revoke" = "Visszavonás"; @@ -4003,7 +4567,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"; @@ -4018,14 +4582,20 @@ "Safer groups" = "Biztonságosabb csoportok"; /* alert button - chat item action */ +chat item action */ "Save" = "Mentés"; /* alert button */ -"Save (and notify contacts)" = "Mentés és az ismerősök értesítése"; +"Save (and notify contacts)" = "Mentés (és a partnerek értesítése)"; /* alert button */ -"Save and notify contact" = "Mentés és az ismerős értesítése"; +"Save (and notify members)" = "Mentés (és a tagok értesítése)"; + +/* alert title */ +"Save admission settings?" = "Menti a befogadási beállításokat?"; + +/* 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"; @@ -4039,6 +4609,12 @@ /* No comment provided by engineer. */ "Save group profile" = "Csoportprofil mentése"; +/* alert title */ +"Save group profile?" = "Menti a csoportprofilt?"; + +/* No comment provided by engineer. */ +"Save list" = "Lista mentése"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Jelmondat mentése és a csevegés megnyitása"; @@ -4046,7 +4622,7 @@ "Save passphrase in Keychain" = "Jelmondat mentése a kulcstartóba"; /* alert title */ -"Save preferences?" = "Beállítások mentése?"; +"Save preferences?" = "Menti a beállításokat?"; /* No comment provided by engineer. */ "Save profile password" = "Profiljelszó mentése"; @@ -4055,13 +4631,13 @@ "Save servers" = "Kiszolgálók mentése"; /* alert title */ -"Save servers?" = "Kiszolgálók mentése?"; +"Save servers?" = "Menti a kiszolgálókat?"; /* No comment provided by engineer. */ -"Save welcome message?" = "Üdvözlőüzenet mentése?"; +"Save welcome message?" = "Menti az üdvözlőüzenetet?"; /* alert title */ -"Save your profile?" = "Profil mentése?"; +"Save your profile?" = "Menti a profilt?"; /* No comment provided by engineer. */ "saved" = "mentett"; @@ -4070,16 +4646,16 @@ "Saved" = "Mentett"; /* No comment provided by engineer. */ -"Saved from" = "Elmentve innen:"; +"Saved from" = "Mentve innen"; /* No comment provided by engineer. */ -"saved from %@" = "elmentve innen: %@"; +"saved from %@" = "mentve 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. */ "Saving %lld messages" = "%lld üzenet mentése"; @@ -4097,13 +4673,13 @@ "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 az ismerősének alkalmazásábó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"; +"Scan server QR code" = "Kiszolgáló QR-kódjának beolvasása"; /* No comment provided by engineer. */ "search" = "keresés"; @@ -4112,7 +4688,7 @@ "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"; @@ -4121,7 +4697,7 @@ "sec" = "mp"; /* No comment provided by engineer. */ -"Secondary" = "Másodlagos"; +"Secondary" = "Másodlagos szín"; /* time unit */ "seconds" = "másodperc"; @@ -4130,7 +4706,7 @@ "secret" = "titok"; /* server test step */ -"Secure queue" = "Biztonságos sorbaállítás"; +"Secure queue" = "Biztonságos várólista"; /* No comment provided by engineer. */ "Secured" = "Biztosítva"; @@ -4142,44 +4718,44 @@ "Security code" = "Biztonsági kód"; /* chat item text */ -"security code changed" = "a biztonsági kód megváltozott"; +"security code changed" = "biztonsági kódja módosult"; /* chat item action */ -"Select" = "Kiválasztás"; +"Select" = "Kijelölés"; /* No comment provided by engineer. */ -"Select chat profile" = "Csevegési profil kiválasztása"; +"Select chat profile" = "Csevegési profil kijelölése"; /* No comment provided by engineer. */ -"Selected %lld" = "%lld kiválasztva"; +"Selected %lld" = "%lld kijelölve"; /* No comment provided by engineer. */ -"Selected chat preferences prohibit this message." = "A kiválasztott csevegési beállítások tiltják ezt az üzenetet."; +"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 contact request?" = "Elküldi a partneri kapcsolatkérést?"; /* No comment provided by engineer. */ "Send delivery receipts to" = "A kézbesítési jelentéseket a következő címre kell küldeni"; -/* No comment provided by engineer. */ -"send direct message" = "közvetlen üzenet küldése"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Közvetlen üzenet küldése a kapcsolódáshoz"; @@ -4190,7 +4766,7 @@ "Send errors" = "Üzenetküldési hibák"; /* No comment provided by engineer. */ -"Send link previews" = "Hivatkozás előnézetek küldése"; +"Send link previews" = "Hivatkozások előnézetének megjelenítése"; /* No comment provided by engineer. */ "Send live message" = "Élő üzenet küldése"; @@ -4199,68 +4775,77 @@ "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 az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; +"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 az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; +"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 questions and ideas" = "Ötletek és kérdések beküldése"; +"Send private reports" = "Privát jelentések küldése"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "Ötletek és javaslatok"; /* No comment provided by engineer. */ "Send receipts" = "Kézbesítési jelentések küldése"; /* No comment provided by engineer. */ -"Send them from gallery or custom keyboards." = "Küldje el őket galériából vagy egyedi billentyűzetekről."; +"Send request" = "Kérés küldése"; /* 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 request without message" = "Kérés küldése üzenet nélkül"; + +/* No comment provided by engineer. */ +"Send them from gallery or custom keyboards." = "Küldje el őket a galériából vagy az egyéni billentyűzetekről."; + +/* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "Legfeljebb az utolsó 100 üzenet elküldése az új tagok számára."; + +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Küldjön privát visszajelzést a csoportoknak."; /* alert message */ "Sender cancelled file transfer." = "A fájl küldője visszavonta az átvitelt."; /* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "A küldő törölhette a kapcsolatkérést."; +"Sender may have deleted the connection request." = "A kérés küldője törölhette a kapcsolódá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ő összes ismerőse számára."; +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "A kézbesítési jelentések küldése engedélyezve lesz az összes látható csevegési profilban lévő összes partnere számára."; /* No comment provided by engineer. */ -"Sending delivery receipts will be enabled for all contacts." = "A kézbesítési jelentés küldése az összes ismerőse számára engedélyezésre kerül."; +"Sending delivery receipts will be enabled for all contacts." = "A kézbesítési jelentések küldése az összes partnere számára engedélyezve lesz."; /* No comment provided by engineer. */ "Sending file will be stopped." = "A fájl küldése le fog állni."; /* No comment provided by engineer. */ -"Sending receipts is disabled for %lld contacts" = "A kézbesítési jelentések le vannak tiltva %lld ismerősnél"; +"Sending receipts is disabled for %lld contacts" = "A kézbesítési jelentések le vannak tiltva %lld partnernél"; /* No comment provided by engineer. */ "Sending receipts is disabled for %lld groups" = "A kézbesítési jelentések le vannak tiltva %lld csoportban"; /* No comment provided by engineer. */ -"Sending receipts is enabled for %lld contacts" = "A kézbesítési jelentések engedélyezve vannak %lld ismerősnél"; +"Sending receipts is enabled for %lld contacts" = "A kézbesítési jelentések engedélyezve vannak %lld partnernél"; /* No comment provided by engineer. */ "Sending receipts is enabled for %lld groups" = "A kézbesítési jelentések engedélyezve vannak %lld csoportban"; /* No comment provided by engineer. */ -"Sending via" = "Küldés ezen keresztül"; +"Sending via" = "Küldés a következőn 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ájlesemény"; - /* message info title */ "Sent message" = "Üzenetbuborék színe"; @@ -4268,7 +4853,7 @@ "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."; +"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"; @@ -4277,7 +4862,7 @@ "Sent total" = "Összes elküldött üzenet"; /* No comment provided by engineer. */ -"Sent via proxy" = "Proxyn keresztül küldve"; +"Sent via proxy" = "Proxyn keresztül küldött"; /* No comment provided by engineer. */ "Server" = "Kiszolgáló"; @@ -4295,22 +4880,22 @@ "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 megváltozott."; +"Server operator changed." = "A kiszolgáló üzemeltetője módosult."; /* No comment provided by engineer. */ -"Server operators" = "Kiszolgáló-üzemeltetők"; +"Server operators" = "Kiszolgálóüzemeltetők"; /* alert title */ -"Server protocol changed." = "A kiszolgáló-protokoll megváltozott."; +"Server protocol changed." = "A kiszolgálóprotokoll módosult."; /* queue info */ -"server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló üzenet-sorbaállítási információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; +"server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló várólista 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 jelszavát"; +"Server requires authorization to create queues, check password." = "A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát."; /* 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"; +"Server requires authorization to upload, check password." = "A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze a jelszavát."; /* No comment provided by engineer. */ "Server test failed!" = "Sikertelen kiszolgáló teszt!"; @@ -4331,7 +4916,7 @@ "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!"; +"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"; @@ -4340,7 +4925,10 @@ "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"; @@ -4349,10 +4937,16 @@ "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 member admission" = "Tagbefogadás beállítása"; + +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Üzenetek eltűnési idejének módosítása a csevegésekben."; /* profile update event chat item */ -"set new contact address" = "új kapcsolattartási cím 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épet állított be"; @@ -4367,7 +4961,10 @@ "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 profile bio and welcome message." = "Névjegy és üdvözlőüzenet beállítása a profilokhoz."; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Megjelenítendő üzenet beállítása az új tagok számára!"; /* No comment provided by engineer. */ "Set timeouts for proxy/VPN" = "Időtúllépések beállítása a proxy/VPN számára"; @@ -4376,20 +4973,20 @@ "Settings" = "Beállítások"; /* alert message */ -"Settings were changed." = "A beállítások megváltoztak."; +"Settings were changed." = "A beállítások módosultak."; /* No comment provided by engineer. */ -"Shape profile images" = "Profilkép alakzat"; +"Shape profile images" = "Profilkép alakzata"; /* alert action - chat item action */ +chat item action */ "Share" = "Megosztás"; /* No comment provided by engineer. */ -"Share 1-time link" = "Egyszer használható hivatkozás megosztása"; +"Share 1-time link" = "Egyszer használható meghívó megosztása"; /* No comment provided by engineer. */ -"Share 1-time link with a friend" = "Egyszer használható meghívó-hivatkozás megosztása egy baráttal"; +"Share 1-time link with a friend" = "Egyszer használható meghívó megosztása egy baráttal"; /* No comment provided by engineer. */ "Share address" = "Cím megosztása"; @@ -4398,13 +4995,19 @@ "Share address publicly" = "Cím nyilvános megosztása"; /* alert title */ -"Share address with contacts?" = "Megosztja a címet az ismerőseivel?"; +"Share address with contacts?" = "Megosztja a címet a partnereivel?"; /* No comment provided by engineer. */ "Share from other apps." = "Megosztás más alkalmazásokból."; /* No comment provided by engineer. */ -"Share link" = "Hivatkozás megosztása"; +"Share link" = "Megosztás"; + +/* alert button */ +"Share old address" = "Régi cím megosztása"; + +/* alert button */ +"Share old link" = "Régi (hosszú) hivatkozás megosztása"; /* No comment provided by engineer. */ "Share profile" = "Profil megosztása"; @@ -4413,13 +5016,25 @@ "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" = "Egyszer használható meghívó-hivatkozás megosztása"; +"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 SimpleX-ben"; +"Share to SimpleX" = "Megosztás a SimpleXben"; /* No comment provided by engineer. */ -"Share with contacts" = "Megosztás az ismerősökkel"; +"Share with contacts" = "Megosztás a partnerekkel"; + +/* No comment provided by engineer. */ +"Share your address" = "Saját cím megosztása"; + +/* No comment provided by engineer. */ +"Short description" = "Rövid leírás"; + +/* No comment provided by engineer. */ +"Short link" = "Rövid hivatkozás"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Rövid SimpleX-cím"; /* 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."; @@ -4431,22 +5046,22 @@ "Show developer options" = "Fejlesztői beállítások megjelenítése"; /* No comment provided by engineer. */ -"Show last messages" = "Szobák utolsó üzeneteinek megjelenítése a listanézetben"; +"Show last messages" = "Legutóbbi üzenetek előnézetének megjelenítése"; /* No comment provided by engineer. */ -"Show message status" = "Üzenetállapot 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"; +"Show preview" = "Értesítésekben megjelenő információk"; /* 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" = "SimpleX"; @@ -4458,10 +5073,16 @@ "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ó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül."; +"SimpleX address and 1-time links are safe to share via any messenger." = "A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó-alkalmazáson keresztül."; /* No comment provided by engineer. */ -"SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó-hivatkozás?"; +"SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó?"; + +/* alert title */ +"SimpleX address settings" = "Beállítások automatikus elfogadása"; + +/* 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."; @@ -4482,7 +5103,7 @@ "SimpleX links" = "SimpleX-hivatkozások"; /* No comment provided by engineer. */ -"SimpleX links are prohibited." = "A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban."; +"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"; @@ -4500,10 +5121,13 @@ "SimpleX Lock turned on" = "SimpleX-zár bekapcsolva"; /* simplex link type */ -"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó-hivatkozás"; +"SimpleX one-time invitation" = "Egyszer használható SimpleX meghívó"; /* No comment provided by engineer. */ -"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva."; +"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX protokollokat a Trail of Bits auditálta."; + +/* simplex link type */ +"SimpleX relay link" = "SimpleX továbbítókiszolgáló-hivatkozás"; /* No comment provided by engineer. */ "Simplified incognito mode" = "Egyszerűsített inkognitómód"; @@ -4533,7 +5157,7 @@ "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 került exportálásra:"; +"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."; @@ -4547,6 +5171,10 @@ /* 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."; @@ -4557,13 +5185,13 @@ "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 %@." = "Kezdve ettől %@."; +"Starting from %@." = "Statisztikagyűjtés kezdete: %@."; /* No comment provided by engineer. */ "starting…" = "indítás…"; @@ -4581,22 +5209,22 @@ "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?" = "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ájlfogadás megállítása?"; +"Stop receiving file?" = "Megállítja a fájlfogadást?"; /* No comment provided by engineer. */ -"Stop sending file?" = "Fájlküldés megállítása?"; +"Stop sending file?" = "Megállítja a fájlküldést?"; /* alert action */ "Stop sharing" = "Megosztás megállítása"; /* alert title */ -"Stop sharing address?" = "Címmegosztás megállítása?"; +"Stop sharing address?" = "Megállítja a címmegosztást?"; /* authentication reason */ "Stop SimpleX" = "SimpleX megállítása"; @@ -4604,6 +5232,9 @@ /* 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"; @@ -4629,13 +5260,13 @@ "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ó-hivatkozásokhoz."; +"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. */ "Tail" = "Farok"; @@ -4646,11 +5277,23 @@ /* No comment provided by engineer. */ "Tap button " = "Koppintson a "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "Koppintson a „Kapcsolódás” gombra a csevegéshez"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "Koppintson a „Kapcsolódás” gombra a kérés elküldéséhez"; + +/* No comment provided by engineer. */ +"Tap Connect to use bot" = "Koppintson a „Kapcsolódás” gombra a bot használatához"; + /* No comment provided by engineer. */ "Tap Create SimpleX address in the menu to create it later." = "Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz."; /* No comment provided by engineer. */ -"Tap to activate profile." = "A profil aktiválásához koppintson az ikonra."; +"Tap Join group" = "Koppintson a „Csatlakozás a csoporthoz” gombra"; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "Koppintson ide a profil aktiválásához."; /* No comment provided by engineer. */ "Tap to Connect" = "Koppintson ide a kapcsolódáshoz"; @@ -4659,19 +5302,25 @@ "Tap to join" = "Koppintson ide a csatlakozáshoz"; /* No comment provided by engineer. */ -"Tap to join incognito" = "Koppintson ide 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 ide a hivatkozás beillesztéséhez"; /* No comment provided by engineer. */ -"Tap to scan" = "Koppintson ide a QR-kód beolvasáshoz"; +"Tap to scan" = "Koppintson ide a QR-kód beolvasásához"; /* No comment provided by engineer. */ -"TCP connection" = "TCP kapcsolat"; +"TCP connection" = "TCP-kapcsolat"; /* No comment provided by engineer. */ -"TCP connection timeout" = "TCP kapcsolat időtúllépése"; +"TCP connection bg timeout" = "TCP-kapcsolat időtúllépése a háttérben"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "TCP-kapcsolat időtúllépése"; + +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-port az üzenetváltáshoz"; /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4682,11 +5331,14 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ -"Temporary file error" = "Ideiglenesfájl-hiba"; +/* 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"; @@ -4701,34 +5353,37 @@ "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 Weblate-en!"; +"Thanks to the users – contribute via Weblate!" = "Köszönet a felhasználóknak a Weblate-en való közreműködésért!"; + +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "A cím rövid lesz és a profil meg lesz osztva a címen keresztül."; /* 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 kapcsolatkéréseket kap – beállítások megnyitása az engedélyezéshez."; +"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 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ésben más-más üzemeltetőket használ."; +"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."; +"The attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be."; /* No comment provided by engineer. */ -"The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás."; +"The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-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, az Ön ismerőse lehet, hogy offline állapotban van."; +"The connection reached the limit of undelivered messages, your contact may be offline." = "A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van."; /* No comment provided by engineer. */ -"The connection you accepted will be cancelled!" = "Az Ön által elfogadott kérelem vissza lesz vonva!"; +"The connection you accepted will be cancelled!" = "Az Ön által elfogadott kapcsolat 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!"; +"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."; @@ -4737,37 +5392,34 @@ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!"; /* No comment provided by engineer. */ -"The future of messaging" = "A privát üzenetküldés következő generációja"; +"The future of messaging" = "Az üzenetváltás jövője"; /* No comment provided by engineer. */ -"The hash of the previous message is different." = "Az előző üzenet hasító értéke különbözik."; +"The hash of the previous message is different." = "Az előző üzenet kivonata különbözik."; /* No comment provided by engineer. */ -"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "A következő üzenet azonosítója hibás (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő."; +"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ő."; + +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "A hivatkozás rövid lesz és a csoportprofil meg lesz osztva a hivatkozáson keresztül."; /* No comment provided by engineer. */ -"The message will be deleted for all members." = "Az üzenet az összes tag számára törlésre kerül."; +"The message will be deleted for all members." = "Az üzenet az összes tag számára törölve lesz."; /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Az üzenet az összes tag számára moderáltként lesz megjelölve."; /* No comment provided by engineer. */ -"The messages will be deleted for all members." = "Az üzenetek az összes tag számára törlésre kerülnek."; +"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éskor, így törölhető."; +"The old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem lett eltávolítva az átköltöztetéskor, ezért törölhető."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "A profilja csak az ismerőseivel kerül megosztásra."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltetőre is: **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**."; +"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!"; @@ -4775,20 +5427,20 @@ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "A második jelölés, amit kihagytunk! ✅"; -/* No comment provided by engineer. */ -"The sender will NOT be notified" = "A feladó NEM fog értesítést kapni"; +/* alert message */ +"The sender will NOT be notified" = "A kérés küldője NEM fog értesítést kapni"; /* No comment provided by engineer. */ -"The servers for new connections of your current chat profile **%@**." = "A jelenlegi csevegési profilhoz tartozó új kapcsolatok 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 servers for new files of your current chat profile **%@**." = "Az Ön jelenlegi **%@** nevű csevegőprofiljához tartozó új fájlok kiszolgálói."; +"The servers for new files of your current chat profile **%@**." = "A jelenlegi **%@** nevű csevegési profiljához tartozó új fájlok kiszolgálói."; /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "A beillesztett szöveg nem egy SimpleX-hivatkozás."; /* No comment provided by engineer. */ -"The uploaded database archive will be permanently removed from the servers." = "A feltöltött adatbázis-archívum véglegesen eltávolításra kerül a kiszolgálókról."; +"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"; @@ -4797,52 +5449,61 @@ "These conditions will also apply for: **%@**." = "Ezek a feltételek lesznek elfogadva a következő számára is: **%@**."; /* No comment provided by engineer. */ -"These settings are for your current profile **%@**." = "Ezek a beállítások csak a jelenlegi (**%@**) profiljára vonatkoznak."; +"These settings are for your current profile **%@**." = "Ezek a beállítások csak a jelenlegi **%@** nevű csevegési profiljára vonatkoznak."; /* No comment provided by engineer. */ -"They can be overridden in contact and group settings." = "Ezek felülbírálhatók az ismerős- és csoportbeállításokban."; +"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örlésre kerül. Az alacsony felbontású képek viszont megmaradnak."; +"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 kiválasztottnál korábban küldött és fogadott üzenetek törlésre kerülnek. Ez több percet is igénybe vehet."; +"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, ismerősei, üzenetei és fájljai visszafordíthatatlanul törlésre kerülnek."; +"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 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 Ön egyszer használható meghívó-hivatkozása!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Ez az Ön SimpleX-címe!"; +"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 setting applies to messages in your current chat profile **%@**." = "Ez a beállítás csak a jelenlegi (**%@**) profiljában lévő üzenetekre vonatkozik."; +"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. */ +"This setting is for your current profile **%@**." = "Ez a beállítás csak a jelenlegi **%@** nevű csevegési profiljára vonatkozik."; + +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Az üzeneteltűnési idő csak az új partnerekre vonatkozik."; /* No comment provided by engineer. */ "Title" = "Cím"; @@ -4851,7 +5512,7 @@ "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."; @@ -4860,7 +5521,7 @@ "To make a new connection" = "Új kapcsolat létrehozásához"; /* No comment provided by engineer. */ -"To protect against your link being replaced, you can compare contact security codes." = "A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat az ismerősével."; +"To protect against your link being replaced, you can compare contact security codes." = "A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat a partnerével."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak."; @@ -4872,7 +5533,7 @@ "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." = "Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Adatainak védelme érdekében a SimpleX külön azonosítókat használ minden egyes kapcsolatához."; /* No comment provided by engineer. */ "To receive" = "A fogadáshoz"; @@ -4887,25 +5548,34 @@ "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 megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a **Csevegési profilok** menüben."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben."; /* No comment provided by engineer. */ "To send" = "A küldéshez"; +/* alert message */ +"To send commands you must be connected." = "A parancsok küldéséhez kapcsolódva kell lennie."; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges."; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "Másik profil használatához a kapcsolatfelvételi kísérlet után törölje a csevegést, és használja újra a hivatkozást."; + /* No comment provided by engineer. */ "To use the servers of **%@**, accept conditions of use." = "A(z) **%@** kiszolgálóinak használatához fogadja el a használati feltételeket."; /* No comment provided by engineer. */ -"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal."; +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal."; /* No comment provided by engineer. */ -"Toggle chat list:" = "Csevegőlista átváltása:"; +"Toggle chat list:" = "Csevegési lista ki/be:"; /* No comment provided by engineer. */ -"Toggle incognito when connecting." = "Inkognitómód használata kapcsolódáskor."; +"Toggle incognito when connecting." = "Inkognitó profil használata kapcsolódáskor ki/be."; + +/* token status */ +"Token status: %@." = "Token állapota: %@."; /* No comment provided by engineer. */ "Toolbar opacity" = "Eszköztár átlátszatlansága"; @@ -4914,16 +5584,13 @@ "Total" = "Összes kapcsolat"; /* No comment provided by engineer. */ -"Transport isolation" = "Átvitel-izoláció módja"; +"Transport isolation" = "Átvitelelkülönítés"; /* 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ősé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ősétől érkező üzenetek fogadására szolgál."; +/* subscription status explanation */ +"Trying to connect to the server used to receive messages from this connection." = "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"; @@ -4941,7 +5608,7 @@ "Unblock" = "Feloldás"; /* No comment provided by engineer. */ -"Unblock for all" = "Letiltás feloldása az összes tag számára"; +"Unblock for all" = "Feloldás"; /* No comment provided by engineer. */ "Unblock member" = "Tag feloldása"; @@ -4950,7 +5617,7 @@ "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 %@" = "feloldotta %@ letiltását"; @@ -4989,7 +5656,7 @@ "Unknown error" = "Ismeretlen hiba"; /* No comment provided by engineer. */ -"unknown servers" = "ismeretlen átjátszók"; +"unknown servers" = "ismeretlen kiszolgálók"; /* alert title */ "Unknown servers!" = "Ismeretlen kiszolgálók!"; @@ -4998,16 +5665,16 @@ "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."; +"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 megszakadá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 egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapcsolattartási 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"; +"Unlink" = "Leválasztá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"; @@ -5015,10 +5682,7 @@ /* authentication reason */ "Unlock app" = "Alkalmazás feloldása"; -/* No comment provided by engineer. */ -"unmute" = "némítás megszüntetése"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Némítás megszüntetése"; /* No comment provided by engineer. */ @@ -5028,32 +5692,56 @@ "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 database passphrase" = "Adatbázis-jelmondat megváltoztatása"; +"Update database passphrase" = "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?"; +"Update network settings?" = "Módosítja a hálózati beállításokat?"; /* No comment provided by engineer. */ -"Update settings?" = "Beállítások frissítése?"; +"Update settings?" = "Frissíti a beállításokat?"; + +/* No comment provided by engineer. */ +"Updated conditions" = "Frissített feltételek"; /* rcv group event chat item */ -"updated group profile" = "frissítette a csoport profilját"; +"updated group profile" = "frissítette a csoportprofilt"; /* profile update event chat item */ -"updated profile" = "frissített profil"; +"updated profile" = "frissítette a profilját"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár."; +/* alert button */ +"Upgrade" = "Frissítés"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Cím frissítése"; + +/* alert message */ +"Upgrade address?" = "Frissíti a címet?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "Fejlesztés és a csevegés megnyitása"; +/* alert message */ +"Upgrade group link?" = "Frissíti a csoporthivatkozást?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Hivatkozás frissítése"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Cím frissítése"; + /* No comment provided by engineer. */ "Upload errors" = "Feltöltési hibák"; @@ -5073,15 +5761,15 @@ "Uploading archive" = "Archívum feltöltése"; /* No comment provided by engineer. */ -"Use .onion hosts" = "Onion-kiszolgálók használata"; +"Use .onion hosts" = "Onion kiszolgálók használata"; /* No comment provided by engineer. */ "Use %@" = "%@ használata"; /* No comment provided by engineer. */ -"Use chat" = "Csevegés használata"; +"Use chat" = "SimpleX Chat használata"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Jelenlegi profil használata"; /* No comment provided by engineer. */ @@ -5091,25 +5779,28 @@ "Use for messages" = "Használat az üzenetekhez"; /* No comment provided by engineer. */ -"Use for new connections" = "Alkalmazás új kapcsolatokhoz"; +"Use for new connections" = "Használat új kapcsolatokhoz"; /* No comment provided by engineer. */ "Use from desktop" = "Társítás számítógéppel"; /* No comment provided by engineer. */ -"Use iOS call interface" = "Az iOS hívási felületét használata"; +"Use incognito profile" = "Inkognitóprofil használata"; /* No comment provided by engineer. */ +"Use iOS call interface" = "iOS hívási felület használata"; + +/* new chat action */ "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."; +"Use private routing with unknown servers when IP address is not protected." = "Privát útválasztás használata az 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."; +"Use private routing with unknown servers." = "Privát útválasztás használata az ismeretlen kiszolgálókhoz."; /* No comment provided by engineer. */ "Use server" = "Kiszolgáló használata"; @@ -5118,25 +5809,34 @@ "Use servers" = "Kiszolgálók használata"; /* No comment provided by engineer. */ -"Use SimpleX Chat servers?" = "SimpleX Chat-kiszolgálók használata?"; +"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 the app while in the call." = "Használja az alkalmazást hívás közben."; +"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 the app with one hand." = "Használja az alkalmazást egy kézzel."; +"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álókhoz."; /* No comment provided by engineer. */ -"User selection" = "Felhasználó kiválasztása"; +"Use the app while in the call." = "Alkalmazás használata hívás közben."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Alkalmazás egy kézzel való használata."; + +/* No comment provided by engineer. */ +"Use 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."; +"Using SimpleX Chat servers." = "SimpleX Chat kiszolgálók használatban."; /* No comment provided by engineer. */ "v%@" = "v%@"; @@ -5169,19 +5869,19 @@ "Via browser" = "Böngészőn keresztül"; /* chat list item description */ -"via contact address link" = "kapcsolattartási cím-hivatkozáson keresztül"; +"via contact address link" = "a kapcsolattartási címhivatkozáson keresztül"; /* chat list item description */ "via group link" = "a csoporthivatkozáson keresztül"; /* chat list item description */ -"via one-time link" = "egyszer használható meghívó-hivatkozáson keresztül"; +"via one-time link" = "egy egyszer használható meghívón keresztül"; /* No comment provided by engineer. */ -"via relay" = "közvetítő-kiszolgálón keresztül"; +"via relay" = "továbbítókiszolgálón keresztül"; /* No comment provided by engineer. */ -"Via secure quantum resistant protocol." = "Biztonságos kvantumrezisztens-protokollon keresztül."; +"Via secure quantum resistant protocol." = "Biztonságos kvantumbiztos protokollon keresztül."; /* No comment provided by engineer. */ "video" = "videó"; @@ -5199,7 +5899,7 @@ "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"; @@ -5223,13 +5923,13 @@ "Voice messages are prohibited in this chat." = "A hangüzenetek küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Voice messages are prohibited." = "A hangüzenetek küldése le van tiltva 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 a válaszra…"; @@ -5250,7 +5950,7 @@ "Waiting for video" = "Várakozás a videóra"; /* No comment provided by engineer. */ -"Wallpaper accent" = "Háttérkép kiemelés"; +"Wallpaper accent" = "Háttérkép kiemelőszíne"; /* No comment provided by engineer. */ "Wallpaper background" = "Háttérkép háttérszíne"; @@ -5259,7 +5959,7 @@ "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, továbbá üzenetkézbesítési hibákat okozhat"; +"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!"; @@ -5271,7 +5971,7 @@ "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"; @@ -5279,6 +5979,9 @@ /* No comment provided by engineer. */ "Welcome message is too long" = "Az üdvözlőüzenet túl hosszú"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Üdvözölje a partnereit 👋"; + /* No comment provided by engineer. */ "What's new" = "Újdonságok"; @@ -5292,10 +5995,10 @@ "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 hálózati üzemeltető van engedélyezve, egyikük sem rendelkezik olyan metaadatokkal ahhoz, hogy felderítse, ki kommunikál kivel."; +"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."; +"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Ha egy inkognitóprofilt oszt meg valamelyik partnerével, a rendszer ezt az inkognitóprofilt fogja használni azokban a csoportokban, ahová az adott partnere meghívja Önt."; /* No comment provided by engineer. */ "WiFi" = "Wi-Fi"; @@ -5316,22 +6019,22 @@ "With reduced battery usage." = "Csökkentett akkumulátor-használattal."; /* No comment provided by engineer. */ -"Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az IP-címe látható lesz a fájlkiszolgálók számára."; +"Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára."; /* alert message */ -"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe látható lesz a következő XFTP-közvetítő-kiszolgálók számára: %@."; +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@."; /* No comment provided by engineer. */ -"Wrong database passphrase" = "Hibás adatbázis-jelmondat"; +"Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat"; /* snd error text */ -"Wrong key or unknown connection - most likely this connection is deleted." = "Hibás kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött."; +"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." = "Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött."; +"Wrong key or unknown file chunk address - most likely file is deleted." = "Érvénytelen kulcs vagy ismeretlen fájltöredékcím – valószínűleg a fájl törlődött."; /* No comment provided by engineer. */ -"Wrong passphrase!" = "Hibás jelmondat!"; +"Wrong passphrase!" = "Érvénytelen jelmondat!"; /* No comment provided by engineer. */ "XFTP server" = "XFTP-kiszolgáló"; @@ -5346,49 +6049,49 @@ "You **must not** use the same database on two devices." = "**Nem szabad** ugyanazt az adatbázist használni egyszerre két eszközön."; /* No comment provided by engineer. */ -"You accepted connection" = "Kapcsolat létrehozása"; +"You accepted connection" = "Ön elfogadta a kapcsolatot"; + +/* snd group event chat item */ +"you accepted this member" = "Ön befogadta ezt a tagot"; /* No comment provided by engineer. */ "You allow" = "Ön engedélyezi"; /* No comment provided by engineer. */ -"You already have a chat profile with the same display name. Please choose another name." = "Már van egy csevegési profil ugyanezzel a megjelení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 %@." = "Ön már kapcsolódva van ehhez: %@."; +"You are already connected to %@." = "Ön már kapcsolódott a következőhöz: %@."; /* No comment provided by engineer. */ "You are already connected with %@." = "Ön már kapcsolódva van vele: %@."; -/* No comment provided by engineer. */ -"You are already connecting to %@." = "Már folyamatban van a kapcsolódás ehhez: %@."; +/* new chat sheet message */ +"You are already connecting to %@." = "A kapcsolódás már folyamatban van a következőhöz: %@."; -/* No comment provided by engineer. */ -"You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül!"; +/* new chat sheet message */ +"You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül!"; /* No comment provided by engineer. */ "You are already in group %@." = "Ön már a(z) %@ nevű csoport tagja."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "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áskérés megismétlése?"; +/* new chat sheet title */ +"You are already joining the group!\nRepeat join request?" = "A csatlakozás már folyamatban van a csoporthoz!\nMegismétli a csatlakozási kérést?"; + +/* subscription status explanation */ +"You are connected to the server used to receive messages from this connection." = "Ön 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 connected to the server used to receive messages from this contact." = "Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál."; +"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"; - -/* No comment provided by engineer. */ -"You are invited to group" = "Meghívást kapott a csoportba"; +/* subscription status explanation */ +"You are not connected to the server used to receive messages from this connection (no subscription)." = "Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs előfizetés)."; /* 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."; @@ -5400,14 +6103,11 @@ "you blocked %@" = "Ön letiltotta őt: %@"; /* No comment provided by engineer. */ -"You can accept calls from lock screen, without device and app authentication." = "Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazás-hitelesítés nélkül."; +"You can accept calls from lock screen, without device and app authentication." = "A lezárási képernyőről is fogadhat hívásokat, eszköz- és alkalmazáshitelesí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 operators in Network & servers settings." = "Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja."; - /* 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."; @@ -5415,43 +6115,43 @@ "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üjében."; +"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az „Adatvédelem és biztonság” menüben."; /* No comment provided by engineer. */ "You can give another try." = "Megpróbálhatja még egyszer."; /* No comment provided by engineer. */ -"You can hide or mute a user profile - swipe it to the right." = "Elrejtheti vagy lenémíthatja a felhasználó -profiljait - csúsztassa jobbra a profilt."; +"You can hide or mute a user profile - swipe it to the right." = "Elrejtheti vagy lenémíthatja a felhasználó -profiljait – csúsztassa jobbra a profilt."; /* No comment provided by engineer. */ -"You can make it visible to your SimpleX contacts via Settings." = "Láthatóvá teheti a SimpleXbeli ismerősei 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 chat with %@" = "Mostantól küldhet üzeneteket %@ számára"; /* No comment provided by engineer. */ -"You can send messages to %@ from Archived contacts." = "Az „Archivált ismerősökből” továbbra is küldhet üzeneteket neki: %@."; +"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 az ismerős nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást."; +"You can set connection name, to remember who the link was shared with." = "Beállíthatja a partner nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "A lezárási képernyő értesítési előnézetét az „Értesítések” menüben állíthatja be."; /* No comment provided by engineer. */ -"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Megoszthat egy hivatkozást vagy QR-kódot - így bárki csatlakozhat a csoporthoz. Ha a csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait."; +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoporthivatkozást később törli, akkor nem fogja elveszíteni a csoport meglévő tagjait."; /* No comment provided by engineer. */ -"You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek Önnel a(z) **%@** nevű profilján keresztül."; +"You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt a SimpleX-címet a partnereivel, hogy kapcsolatba léphessenek vele: **%@**."; /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el"; /* No comment provided by engineer. */ -"You can still view conversation with %@ in the list of chats." = "A(z) %@ nevű ismerősével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában."; +"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."; @@ -5460,22 +6160,25 @@ "You can use markdown to format messages:" = "Üzenetek formázása a szövegbe szúrt speciális karakterekkel:"; /* alert message */ -"You can view invitation link again in connection details." = "A meghívó-hivatkozást újra megtekintheti a kapcsolat részleteinél."; +"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!"; +/* alert message */ +"You can view your reports in Chat with admins." = "A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben."; + +/* alert title */ +"You can't send messages!" = "Ön nem tud üzeneteket küldeni!"; /* chat item text */ -"you changed address" = "cím megváltoztatva"; +"you changed address" = "Ön módosította a címet"; /* chat item text */ -"you changed address for %@" = "cím megváltoztatva nála: %@"; +"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öre megváltozott 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 %@" = "Ön megváltoztatta %1$@ szerepkörét 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 could not be verified; please try again." = "Nem sikerült hitelesíteni; próbálja meg újra."; @@ -5483,23 +6186,20 @@ /* No comment provided by engineer. */ "You decide who can connect." = "Ön dönti el, hogy kivel beszélget."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Már küldött egy kapcsolatkérést ezen a címen keresztül!"; +/* new chat sheet title */ +"You have already requested connection!\nRepeat connection request?" = "Ön már küldött egy kapcsolódási kérést!\nMegismétli a kapcsolódási kérést?"; /* No comment provided by engineer. */ -"You have already requested connection!\nRepeat connection request?" = "Már küldött egy kapcsolódási kérelmet!\nKapcsolatkérés megismétlése?"; +"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" = "Ön elhagyta a csoportot"; @@ -5508,16 +6208,16 @@ "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."; +"You may save the exported archive." = "Mentheti az exportált archívumot."; /* 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ősétől."; +"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 az ismerőse számára, hogy fel tudják hívni egymást."; +"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 az ismerőse számára, hogy hangüzeneteket küldhessenek egymásnak."; +"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"; @@ -5529,14 +6229,20 @@ "You sent group invitation" = "Csoportmeghívó elküldve"; /* chat list item description */ -"you shared one-time link" = "Ön egy egyszer használható meghívó-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" = "Ön egy egyszer használható meghívó-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 %@" = "Ön feloldotta %@ letiltását"; +/* No comment provided by engineer. */ +"You will be able to send messages **only after your request is accepted**." = "Csak azután tud üzeneteket küldeni, **miután a kérését elfogadták**."; + /* 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!"; @@ -5544,16 +6250,13 @@ "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 kapcsolatkérése 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 kapcsolódási kérését elfogadják, várjon, vagy ellenőrizze később!"; /* No comment provided by engineer. */ -"You will be connected when your contact's device is online, please wait or check later!" = "Akkor lesz kapcsolódva, amikor az ismerősének 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."; - -/* No comment provided by engineer. */ -"You will connect to all group members." = "Kapcsolódni fog a csoport összes tagjához."; +"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 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."; @@ -5565,16 +6268,19 @@ "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 a címé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: "; /* 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ősét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a 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" = "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"; +"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 business contact" = "Üzleti partner"; /* No comment provided by engineer. */ "Your calls" = "Hívások"; @@ -5586,32 +6292,41 @@ "Your chat database is not encrypted - set passphrase to encrypt it." = "A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz."; /* alert title */ -"Your chat preferences" = "Csevegési beállítások"; +"Your chat preferences" = "Az Ön csevegési beállításai"; /* No comment provided by engineer. */ "Your chat profiles" = "Csevegési profilok"; -/* No comment provided by engineer. */ -"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "A kapcsolata át lett helyezve ide: %@, de egy váratlan hiba történt a profilra való átirányításkor."; +/* alert message */ +"Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "A csevegés á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 (%@)." = "Az 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 error happened when switching 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." = "Az ismerősei engedélyezhetik a teljes üzenet törlést."; +"Your contact" = "Partner"; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "Az ismerősei továbbra is kapcsolódva maradnak."; +"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 credentials may be sent unencrypted." = "A hitelesítőadatai titkosítatlanul is elküldhetők."; +"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ési 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 group" = "Saját csoport"; + /* No comment provided by engineer. */ "Your ICE servers" = "Saját ICE-kiszolgálók"; @@ -5622,19 +6337,19 @@ "Your privacy" = "Adatvédelem"; /* No comment provided by engineer. */ -"Your profile" = "Profil"; +"Your profile" = "Saját 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. SimpleX servers cannot see your profile." = "A 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."; +"Your profile is stored on your device and only shared with your contacts." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX kiszolgálók nem láthatják a profilját."; /* alert message */ -"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja megváltozott. Ha elmenti, a frissített profil elküldésre kerül az összes ismerősének."; - -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "A profilja, az ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra."; +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja módosult. Ha menti, akkor a profilfrissítés el lesz küldve a partnerei számára."; /* No comment provided by engineer. */ "Your random profile" = "Véletlenszerű profil"; @@ -5643,7 +6358,7 @@ "Your server address" = "Saját SMP-kiszolgálójának címe"; /* No comment provided by engineer. */ -"Your servers" = "Az Ön kiszolgálói"; +"Your servers" = "Saját kiszolgálók"; /* No comment provided by engineer. */ "Your settings" = "Beállítások"; @@ -5651,6 +6366,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Profil SimpleX-címe"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Saját SMP-kiszolgálók"; - diff --git a/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings index 434f906b4e..8b56c51595 100644 --- a/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings @@ -8,11 +8,11 @@ "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 SimpleXnek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez."; /* Privacy - Photo Library Additions Usage Description */ -"NSPhotoLibraryAddUsageDescription" = "A SimpleXnek galéria-hozzáférésre van szüksége a rögzített és fogadott média mentéséhez"; +"NSPhotoLibraryAddUsageDescription" = "A SimpleXnek hozzáférésre van szüksége a galériához a rögzített és fogadott média mentéséhez"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 7c3a7e05de..511a6835e5 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (può essere copiato)"; @@ -22,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)"; @@ -199,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"; @@ -241,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"; @@ -289,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 giorno"; /* time interval */ @@ -298,12 +278,17 @@ /* 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"; @@ -343,6 +328,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Interrompere il cambio di indirizzo?"; +/* No comment provided by engineer. */ +"About operators" = "Info sugli operatori"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "Riguardo SimpleX Chat"; @@ -353,35 +341,60 @@ "Accent" = "Principale"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Accetta"; +/* alert action */ +"Accept as member" = "Accetta come membro"; + +/* alert action */ +"Accept as observer" = "Accetta come osservatore"; + /* No comment provided by engineer. */ "Accept conditions" = "Accetta le condizioni"; /* No comment provided by engineer. */ "Accept connection request?" = "Accettare la richiesta di connessione?"; +/* alert title */ +"Accept contact request" = "Accetta la richiesta di contatto"; + /* notification body */ "Accept contact request from %@?" = "Accettare la richiesta di contatto da %@?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Accetta in incognito"; +/* alert title */ +"Accept member" = "Accetta membro"; + +/* rcv group event chat item */ +"accepted %@" = "%@ accettato"; + /* call status */ "accepted call" = "chiamata accettata"; /* No comment provided by engineer. */ "Accepted conditions" = "Condizioni accettate"; +/* chat list item title */ +"accepted invitation" = "invito accettato"; + +/* rcv group event chat item */ +"accepted you" = "ti ha accettato/a"; + /* 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"; @@ -391,6 +404,12 @@ /* No comment provided by engineer. */ "Add friends" = "Aggiungi amici"; +/* No comment provided by engineer. */ +"Add list" = "Aggiungi elenco"; + +/* placeholder for sending contact request */ +"Add message" = "Aggiungi un messaggio"; + /* No comment provided by engineer. */ "Add profile" = "Aggiungi profilo"; @@ -406,6 +425,9 @@ /* 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"; @@ -463,12 +485,21 @@ /* chat item text */ "agreeing encryption…" = "concordando la crittografia…"; +/* member criteria value */ +"all" = "tutti"; + +/* 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."; @@ -496,6 +527,12 @@ /* 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."; @@ -520,6 +557,9 @@ /* No comment provided by engineer. */ "Allow downgrade" = "Consenti downgrade"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Consenti file e contenuti multimediali solo se il tuo contatto li consente."; + /* 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)"; @@ -541,6 +581,9 @@ /* 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."; @@ -568,16 +611,19 @@ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Permetti ai tuoi contatti di inviare messaggi a tempo."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Consenti ai tuoi contatti di inviare file e contenuti multimediali."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Permetti ai tuoi contatti di inviare messaggi vocali."; /* No comment provided by engineer. */ "Already connected?" = "Già connesso/a?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Già in connessione!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Già in ingresso nel gruppo!"; /* pref value */ @@ -595,6 +641,9 @@ /* 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"; @@ -610,6 +659,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"; @@ -637,15 +689,36 @@ /* 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"; @@ -694,9 +767,6 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Auto-accetta le immagini"; -/* alert title */ -"Auto-accept settings" = "Accetta automaticamente le impostazioni"; - /* No comment provided by engineer. */ "Back" = "Indietro"; @@ -724,6 +794,9 @@ /* 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."; @@ -736,12 +809,21 @@ /* 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. */ +"Bio" = "Bio"; + +/* alert title */ +"Bio too large" = "Bio troppo lunga"; + /* No comment provided by engineer. */ "Black" = "Nero"; @@ -769,7 +851,8 @@ /* 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. */ @@ -784,6 +867,9 @@ /* No comment provided by engineer. */ "bold" = "grassetto"; +/* No comment provided by engineer. */ +"Bot" = "Bot"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi."; @@ -796,6 +882,9 @@ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Sia tu che il tuo contatto potete inviare messaggi a tempo."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Sia tu che il tuo contatto potete inviare file e contenuti multimediali."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Sia tu che il tuo contatto potete inviare messaggi vocali."; @@ -808,9 +897,18 @@ /* No comment provided by engineer. */ "Business chats" = "Chat di lavoro"; +/* No comment provided by engineer. */ +"Business connection" = "Connessione lavorativa"; + +/* 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"; @@ -841,6 +939,9 @@ /* No comment provided by engineer. */ "Can't call member" = "Impossibile chiamare il membro"; +/* alert title */ +"Can't change profile" = "Impossibile cambiare profilo"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Impossibile invitare il contatto!"; @@ -850,8 +951,12 @@ /* No comment provided by engineer. */ "Can't message member" = "Impossibile inviare un messaggio al membro"; +/* No comment provided by engineer. */ +"can't send messages" = "impossibile inviare messaggi"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Annulla"; /* No comment provided by engineer. */ @@ -878,6 +983,9 @@ /* 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"; @@ -906,7 +1014,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 */ @@ -930,7 +1038,7 @@ /* No comment provided by engineer. */ "Chat already exists" = "La chat esiste già"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "La chat esiste già!"; /* No comment provided by engineer. */ @@ -984,9 +1092,21 @@ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "La chat verrà eliminata solo per te, non è reversibile!"; +/* chat toolbar */ +"Chat with admins" = "Chat con amministratori"; + +/* No comment provided by engineer. */ +"Chat with member" = "Chatta con il membro"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Chatta con i membri prima che si uniscano."; + /* No comment provided by engineer. */ "Chats" = "Chat"; +/* No comment provided by engineer. */ +"Chats with members" = "Chat con membri"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Controlla i messaggi ogni 20 min."; @@ -1026,6 +1146,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?"; @@ -1041,6 +1167,9 @@ /* 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"; @@ -1062,15 +1191,9 @@ /* 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. */ +/* alert button */ "Conditions of use" = "Condizioni d'uso"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for operator(s): **%@**." = "Le condizioni verranno accettate per gli operatori: **%@**."; - /* No comment provided by engineer. */ "Conditions will be accepted for the operator(s): **%@**." = "Le condizioni verranno accettate per gli operatori: **%@**."; @@ -1083,6 +1206,9 @@ /* 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"; @@ -1113,6 +1239,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Conferma caricamento"; +/* token status text */ +"Confirmed" = "Confermato"; + /* server test step */ "Connect" = "Connetti"; @@ -1120,7 +1249,7 @@ "Connect automatically" = "Connetti automaticamente"; /* No comment provided by engineer. */ -"Connect incognito" = "Connetti in incognito"; +"Connect faster! 🚀" = "Connettiti più velocemente! 🚀"; /* No comment provided by engineer. */ "Connect to desktop" = "Connetti al desktop"; @@ -1131,25 +1260,22 @@ /* 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?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Connettersi a te stesso?\nQuesto è il tuo link una tantum!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Connettersi a te stesso?\nQuesto è il tuo indirizzo SimpleX!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Connettere via indirizzo del contatto"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Connetti via link"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Connetti via link una tantum"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Connettersi con %@"; /* No comment provided by engineer. */ @@ -1161,9 +1287,6 @@ /* 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"; @@ -1213,6 +1336,9 @@ "Connection and servers status." = "Stato della connessione e dei server."; /* No comment provided by engineer. */ +"Connection blocked" = "Connessione bloccata"; + +/* alert title */ "Connection error" = "Errore di connessione"; /* No comment provided by engineer. */ @@ -1221,19 +1347,28 @@ /* 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. */ +/* alert title */ "Connection timeout" = "Connessione scaduta"; /* No comment provided by engineer. */ @@ -1254,9 +1389,15 @@ /* 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 deleted!" = "Contatto eliminato!"; +/* No comment provided by engineer. */ +"contact disabled" = "contatto disattivato"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "il contatto ha la crittografia e2e"; @@ -1275,9 +1416,18 @@ /* No comment provided by engineer. */ "Contact name" = "Nome del contatto"; +/* No comment provided by engineer. */ +"contact not ready" = "contatto non pronto"; + /* No comment provided by engineer. */ "Contact preferences" = "Preferenze del contatto"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Richieste di contatto dai gruppi"; + +/* No comment provided by engineer. */ +"contact should accept…" = "il contatto dovrebbe accettare…"; + /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "Il contatto verrà eliminato - non è reversibile!"; @@ -1287,6 +1437,9 @@ /* 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"; @@ -1329,6 +1482,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/). 💻"; @@ -1339,10 +1495,10 @@ "Create queue" = "Crea coda"; /* No comment provided by engineer. */ -"Create secret group" = "Crea gruppo segreto"; +"Create SimpleX address" = "Crea indirizzo SimpleX"; /* No comment provided by engineer. */ -"Create SimpleX address" = "Crea indirizzo SimpleX"; +"Create your address" = "Crea il tuo indirizzo"; /* No comment provided by engineer. */ "Create your profile" = "Crea il tuo profilo"; @@ -1470,7 +1626,8 @@ /* No comment provided by engineer. */ "decryption errors" = "errori di decifrazione"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "predefinito (%@)"; /* No comment provided by engineer. */ @@ -1480,8 +1637,7 @@ "default (yes)" = "predefinito (sì)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Elimina"; /* No comment provided by engineer. */ @@ -1508,12 +1664,18 @@ /* No comment provided by engineer. */ "Delete chat" = "Elimina chat"; +/* No comment provided by engineer. */ +"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"; /* No comment provided by engineer. */ "Delete chat profile?" = "Eliminare il profilo di chat?"; +/* alert title */ +"Delete chat with member?" = "Eliminare la chat con il membro?"; + /* No comment provided by engineer. */ "Delete chat?" = "Eliminare la chat?"; @@ -1562,13 +1724,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. */ @@ -1592,6 +1757,9 @@ /* 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."; @@ -1634,9 +1802,15 @@ /* No comment provided by engineer. */ "Delivery receipts!" = "Ricevute di consegna!"; +/* No comment provided by engineer. */ +"Deprecated options" = "Opzioni deprecate"; + /* No comment provided by engineer. */ "Description" = "Descrizione"; +/* alert title */ +"Description too large" = "Descrizione troppo lunga"; + /* No comment provided by engineer. */ "Desktop address" = "Indirizzo desktop"; @@ -1700,6 +1874,12 @@ /* 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"; @@ -1760,6 +1940,9 @@ /* 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"; @@ -1767,13 +1950,19 @@ "Don't enable" = "Non attivare"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Non perdere messaggi importanti."; + +/* alert action */ "Don't show again" = "Non mostrare più"; +/* No comment provided by engineer. */ +"Done" = "Fatto"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Esegui downgrade e apri chat"; /* alert button - chat item action */ +chat item action */ "Download" = "Scarica"; /* No comment provided by engineer. */ @@ -1824,20 +2013,26 @@ /* No comment provided by engineer. */ "Edit group profile" = "Modifica il profilo del gruppo"; +/* No comment provided by engineer. */ +"Empty message!" = "Messaggio vuoto!"; + /* No comment provided by engineer. */ "Enable" = "Attiva"; /* 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" = "Attiva Flux"; +"Enable disappearing messages by default." = "Attiva i messaggi a tempo in modo predefinito."; + +/* 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"; @@ -1950,6 +2145,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"; @@ -2004,30 +2202,45 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Errore nell'accettazione della richiesta di contatto"; +/* alert title */ +"Error accepting member" = "Errore di accettazione del membro"; + /* 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 adding short link" = "Errore di aggiunta link breve"; + /* No comment provided by engineer. */ "Error changing address" = "Errore nella modifica dell'indirizzo"; +/* alert title */ +"Error changing chat profile" = "Errore cambiando il profilo di chat"; + /* 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. */ +/* alert title */ "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"; + +/* alert message */ "Error connecting to forwarding server %@. Please try later." = "Errore di connessione al server di inoltro %@. Riprova più tardi."; +/* subscription status explanation */ +"Error connecting to the server used to receive messages from this connection: %@" = "Errore di connessione al server usato per ricevere messaggi da questa connessione: %@"; + /* No comment provided by engineer. */ "Error creating address" = "Errore nella creazione dell'indirizzo"; @@ -2037,6 +2250,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"; @@ -2046,22 +2262,28 @@ /* 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"; -/* No comment provided by engineer. */ +/* alert title */ +"Error deleting chat" = "Errore di eliminazione della chat con il membro"; + +/* alert title */ "Error deleting chat database" = "Errore nell'eliminazione del database della chat"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Errore nell'eliminazione della chat!"; /* No comment provided by engineer. */ "Error deleting connection" = "Errore nell'eliminazione della connessione"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Errore nell'eliminazione del database"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Errore nell'eliminazione del database vecchio"; /* No comment provided by engineer. */ @@ -2082,13 +2304,13 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Errore nella crittografia del database"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Errore nell'importazione del database della chat"; /* No comment provided by engineer. */ @@ -2103,6 +2325,9 @@ /* No comment provided by engineer. */ "Error opening chat" = "Errore di apertura della chat"; +/* No comment provided by engineer. */ +"Error opening group" = "Errore di preparazione del gruppo"; + /* alert title */ "Error receiving file" = "Errore nella ricezione del file"; @@ -2112,12 +2337,24 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Errore di riconnessione ai server"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "Errore di registrazione per le notifiche"; + +/* alert title */ +"Error rejecting contact request" = "Errore nel rifiuto della richiesta di contatto"; + +/* alert title */ "Error removing member" = "Errore nella rimozione del membro"; +/* alert title */ +"Error reordering lists" = "Errore riordinando gli elenchi"; + /* No comment provided by engineer. */ "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"; @@ -2151,6 +2388,9 @@ /* No comment provided by engineer. */ "Error sending message" = "Errore nell'invio del messaggio"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Errore impostando l'accettazione automatica"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Errore nell'impostazione delle ricevute di consegna!"; @@ -2160,7 +2400,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Errore nell'interruzione della chat"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Errore nel cambio di profilo"; /* alertTitle */ @@ -2169,6 +2409,9 @@ /* 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"; @@ -2193,9 +2436,14 @@ /* No comment provided by engineer. */ "Error: " = "Errore: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Errore: %@"; +/* server test error */ +"Error: %@." = "Errore: %@."; + /* No comment provided by engineer. */ "Error: no database file" = "Errore: nessun file di database"; @@ -2211,9 +2459,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Anche quando disattivato nella conversazione."; -/* No comment provided by engineer. */ -"event happened" = "evento accaduto"; - /* No comment provided by engineer. */ "Exit without saving" = "Esci senza salvare"; @@ -2223,6 +2468,9 @@ /* No comment provided by engineer. */ "expired" = "scaduto"; +/* token status text */ +"Expired" = "Scaduto"; + /* No comment provided by engineer. */ "Export database" = "Esporta database"; @@ -2247,18 +2495,30 @@ /* 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."; @@ -2292,6 +2552,9 @@ /* chat feature */ "Files and media" = "File e multimediali"; +/* No comment provided by engineer. */ +"Files and media are prohibited in this chat." = "File e contenuti multimediali sono vietati in questa chat."; + /* No comment provided by engineer. */ "Files and media are prohibited." = "File e contenuti multimediali sono vietati in questo gruppo."; @@ -2316,6 +2579,18 @@ /* No comment provided by engineer. */ "Find chats faster" = "Trova le chat più velocemente"; +/* No comment provided by engineer. */ +"Fingerprint in destination server address does not match certificate: %@." = "L'impronta digitale nell'indirizzo del server di destinazione non corrisponde al certificato: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in forwarding server address does not match certificate: %@." = "L'impronta digitale nell'indirizzo del server di inoltro non corrisponde al certificato: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in server address does not match certificate: %@." = "L'impronta digitale nell'indirizzo del server non corrisponde al certificato: %@."; + +/* server test error */ +"Fingerprint in server address does not match certificate." = "L'impronta digitale nell'indirizzo del server non corrisponde al certificato."; + /* No comment provided by engineer. */ "Fix" = "Correggi"; @@ -2335,7 +2610,7 @@ "Fix not supported by group member" = "Correzione non supportata dal membro del gruppo"; /* No comment provided by engineer. */ -"for better metadata privacy." = "per una migliore privacy dei metadati."; +"For all moderators" = "Per tutti i moderatori"; /* servers error */ "For chat profile %@:" = "Per il profilo di chat %@:"; @@ -2346,6 +2621,9 @@ /* 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"; @@ -2382,8 +2660,8 @@ /* 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."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Il server di inoltro %1$@ non è riuscito a connettersi al server di destinazione %2$@. 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: %@."; @@ -2418,6 +2696,9 @@ /* 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"; @@ -2427,13 +2708,16 @@ /* message preview */ "Good morning!" = "Buongiorno!"; +/* shown on group welcome message */ +"group" = "gruppo"; + /* No comment provided by engineer. */ "Group" = "Gruppo"; /* No comment provided by engineer. */ "Group already exists" = "Il gruppo esiste già"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Il gruppo esiste già!"; /* No comment provided by engineer. */ @@ -2457,6 +2741,9 @@ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "L'invito al gruppo non è più valido, è stato rimosso dal mittente."; +/* No comment provided by engineer. */ +"group is deleted" = "il gruppo è eliminato"; + /* No comment provided by engineer. */ "Group link" = "Link del gruppo"; @@ -2481,6 +2768,9 @@ /* snd group event chat item */ "group profile updated" = "profilo del gruppo aggiornato"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "Il profilo del gruppo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato ai membri del gruppo."; + /* No comment provided by engineer. */ "Group welcome message" = "Messaggio di benvenuto del gruppo"; @@ -2490,9 +2780,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"; @@ -2529,6 +2825,9 @@ /* 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. */ "How SimpleX works" = "Come funziona SimpleX"; @@ -2616,6 +2915,12 @@ /* 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"; @@ -2682,6 +2987,21 @@ /* No comment provided by engineer. */ "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"; @@ -2697,7 +3017,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Nome da mostrare non valido!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Link non valido"; /* No comment provided by engineer. */ @@ -2797,24 +3117,18 @@ "Join" = "Entra"; /* No comment provided by engineer. */ -"join as %@" = "entra come %@"; +"Join as %@" = "entra come %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Entra nel gruppo"; /* No comment provided by engineer. */ "Join group conversations" = "Entra in conversazioni di gruppo"; -/* No comment provided by engineer. */ -"Join group?" = "Entrare nel gruppo?"; - /* No comment provided by engineer. */ "Join incognito" = "Entra in incognito"; -/* No comment provided by engineer. */ -"Join with current profile" = "Entra con il profilo attuale"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Entrare nel tuo gruppo?\nQuesto è il tuo link per il gruppo %@!"; /* No comment provided by engineer. */ @@ -2832,6 +3146,9 @@ /* alert title */ "Keep unused invitation?" = "Tenere l'invito inutilizzato?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "Mantieni le chat pulite"; + /* No comment provided by engineer. */ "Keep your connections" = "Mantieni le tue connessioni"; @@ -2865,6 +3182,9 @@ /* rcv group event chat item */ "left" = "è uscito/a"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Meno traffico sulle reti mobili."; + /* email subject */ "Let's talk in SimpleX Chat" = "Parliamo in SimpleX Chat"; @@ -2883,6 +3203,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"; @@ -2892,6 +3221,9 @@ /* No comment provided by engineer. */ "Live messages" = "Messaggi in diretta"; +/* in progress text */ +"Loading profile…" = "Caricamento del profilo…"; + /* No comment provided by engineer. */ "Local name" = "Nome locale"; @@ -2943,15 +3275,30 @@ /* No comment provided by engineer. */ "Member" = "Membro"; +/* past/unknown group member */ +"Member %@" = "Membro %@"; + /* profile update event chat item */ -"member %@ changed to %@" = "membro %1$@ cambiato in %2$@"; +"member %@ changed to %@" = "il membro %1$@ è diventato %2$@"; + +/* No comment provided by engineer. */ +"Member admission" = "Ammissione dei membri"; /* rcv group event chat item */ "member connected" = "si è connesso/a"; +/* No comment provided by engineer. */ +"member has old version" = "il membro ha una versione vecchia"; + /* item status text */ "Member inactive" = "Membro inattivo"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Il membro è eliminato - impossibile accettare la richiesta"; + +/* chat feature */ +"Member reports" = "Segnalazioni dei membri"; + /* 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."; @@ -2967,12 +3314,18 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!"; +/* alert message */ +"Member will join the group, accept member?" = "Il membro entrerà nel gruppo, accettarlo?"; + /* 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."; @@ -2988,6 +3341,9 @@ /* 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"; @@ -3009,6 +3365,9 @@ /* item status text */ "Message forwarded" = "Messaggio inoltrato"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Parla immediatamente appena tocchi Connetti."; + /* item status description */ "Message may be delivered later if member becomes active." = "Il messaggio può essere consegnato più tardi se il membro diventa attivo."; @@ -3057,9 +3416,15 @@ /* No comment provided by engineer. */ "Messages & files" = "Messaggi"; +/* No comment provided by engineer. */ +"Messages are protected by **end-to-end encryption**." = "I messaggi sono protetti da **crittografia end-to-end**."; + /* 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"; @@ -3132,9 +3497,15 @@ /* 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!"; @@ -3150,12 +3521,12 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Profili di chat multipli"; -/* No comment provided by engineer. */ -"mute" = "silenzia"; - -/* swipe action */ +/* notification label action */ "Mute" = "Silenzia"; +/* notification label action */ +"Mute all" = "Silenzia tutto"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Silenzioso quando inattivo!"; @@ -3183,12 +3554,15 @@ /* No comment provided by engineer. */ "Network settings" = "Impostazioni di rete"; -/* No comment provided by engineer. */ +/* alert title */ "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"; @@ -3210,6 +3584,9 @@ /* notification */ "New events" = "Nuovi eventi"; +/* No comment provided by engineer. */ +"New group role: Moderator" = "Nuovo ruolo nei gruppi: Moderatore"; + /* No comment provided by engineer. */ "New in %@" = "Novità nella %@"; @@ -3219,6 +3596,9 @@ /* No comment provided by engineer. */ "New member role" = "Nuovo ruolo del membro"; +/* rcv group event chat item */ +"New member wants to join the group." = "Un nuovo membro vuole entrare nel gruppo."; + /* notification */ "new message" = "messaggio nuovo"; @@ -3249,6 +3629,18 @@ /* 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 chats with members" = "Nessuna chat con membri"; + /* No comment provided by engineer. */ "No contacts selected" = "Nessun contatto selezionato"; @@ -3282,6 +3674,9 @@ /* 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."; @@ -3297,6 +3692,9 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Nessuna autorizzazione per registrare messaggi vocali"; +/* alert title */ +"No private routing session" = "Nessuna sessione di instradamento privato"; + /* No comment provided by engineer. */ "No push server" = "Locale"; @@ -3315,15 +3713,30 @@ /* servers error */ "No servers to send files." = "Nessun server per inviare file."; +/* No comment provided by engineer. */ +"no subscription" = "nessuna iscrizione"; + /* 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. */ +"not synchronized" = "non sincronizzato"; + +/* No comment provided by engineer. */ +"Notes" = "Note"; + /* No comment provided by engineer. */ "Nothing selected" = "Nessuna selezione"; @@ -3336,9 +3749,15 @@ /* 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\")"; @@ -3346,8 +3765,9 @@ "observer" = "osservatore"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "off"; /* blur media */ @@ -3359,7 +3779,9 @@ /* feature offered item */ "offered %@: %@" = "offerto %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3401,6 +3823,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."; @@ -3413,6 +3841,9 @@ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "Solo tu puoi inviare messaggi a tempo."; +/* No comment provided by engineer. */ +"Only you can send files and media." = "Solo tu puoi inviare file e contenuti multimediali."; + /* No comment provided by engineer. */ "Only you can send voice messages." = "Solo tu puoi inviare messaggi vocali."; @@ -3429,32 +3860,62 @@ "Only your contact can send disappearing messages." = "Solo il tuo contatto può inviare messaggi a tempo."; /* No comment provided by engineer. */ -"Only your contact can send voice messages." = "Solo il tuo contatto può inviare messaggi vocali."; +"Only your contact can send files and media." = "Solo il tuo contatto può inviare file e contenuti multimediali."; /* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Solo il tuo contatto può inviare messaggi vocali."; + +/* alert action */ "Open" = "Apri"; /* No comment provided by engineer. */ "Open changes" = "Apri le modifiche"; -/* No comment provided by engineer. */ +/* new chat action */ "Open chat" = "Apri chat"; /* authentication reason */ "Open chat console" = "Apri la console della chat"; +/* alert action */ +"Open clean link" = "Apri link pulito"; + /* No comment provided by engineer. */ "Open conditions" = "Apri le condizioni"; -/* No comment provided by engineer. */ +/* alert action */ +"Open full link" = "Apri link completo"; + +/* new chat action */ "Open group" = "Apri gruppo"; +/* alert title */ +"Open link?" = "Aprire il link?"; + /* authentication reason */ "Open migration to another device" = "Apri migrazione ad un altro dispositivo"; +/* new chat action */ +"Open new chat" = "Apri una chat nuova"; + +/* new chat action */ +"Open new group" = "Apri un gruppo nuovo"; + /* No comment provided by engineer. */ "Open Settings" = "Apri le impostazioni"; +/* No comment provided by engineer. */ +"Open to accept" = "Apri per accettare"; + +/* No comment provided by engineer. */ +"Open to connect" = "Apri per connettere"; + +/* No comment provided by engineer. */ +"Open to join" = "Apri per entrare"; + +/* No comment provided by engineer. */ +"Open to use bot" = "Apri per usare il bot"; + /* No comment provided by engineer. */ "Opening app…" = "Apertura dell'app…"; @@ -3482,6 +3943,9 @@ /* 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"; @@ -3521,9 +3985,6 @@ /* No comment provided by engineer. */ "Password to show" = "Password per mostrare"; -/* past/unknown group member */ -"Past member %@" = "Membro passato %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Incolla l'indirizzo desktop"; @@ -3539,9 +4000,18 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"pending" = "in attesa"; + /* No comment provided by engineer. */ "Pending" = "In attesa"; +/* No comment provided by engineer. */ +"pending approval" = "in attesa di approvazione"; + +/* No comment provided by engineer. */ +"pending review" = "in attesa di revisione"; + /* No comment provided by engineer. */ "Periodic" = "Periodicamente"; @@ -3572,7 +4042,7 @@ /* 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."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Controlla la tua connessione di rete con %@ e riprova."; /* No comment provided by engineer. */ @@ -3608,15 +4078,24 @@ /* 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."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo."; + +/* 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"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva la bozza dell'ultimo messaggio, con gli allegati."; @@ -3638,12 +4117,21 @@ /* 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"; @@ -3656,9 +4144,12 @@ /* No comment provided by engineer. */ "Private routing" = "Instradamento privato"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Errore di instradamento privato"; +/* alert title */ +"Private routing timeout" = "Scadenza dell'instradamento privato"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profilo e connessioni al server"; @@ -3689,6 +4180,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."; @@ -3716,6 +4210,9 @@ /* 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 background timeout" = "Scadenza del protocollo in sec. piano"; + /* No comment provided by engineer. */ "Protocol timeout" = "Scadenza del protocollo"; @@ -3788,9 +4285,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "conferma ricevuta…"; -/* notification */ -"Received file event" = "Evento file ricevuto"; - /* message info title */ "Received message" = "Messaggio ricevuto"; @@ -3851,16 +4345,32 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Consumo di batteria ridotto"; -/* reject incoming call via notification - swipe action */ +/* No comment provided by engineer. */ +"Register" = "Registra"; + +/* token info */ +"Register notification token?" = "Registrare il token di notifica?"; + +/* token status text */ +"Registered" = "Registrato"; + +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Rifiuta"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Rifiuta contatto (mittente NON avvisato)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Rifiuta la richiesta di contatto"; +/* alert title */ +"Reject member?" = "Rifiutare il membro?"; + +/* No comment provided by engineer. */ +"rejected" = "rifiutato"; + /* call status */ "rejected call" = "chiamata rifiutata"; @@ -3879,6 +4389,9 @@ /* No comment provided by engineer. */ "Remove image" = "Rimuovi immagine"; +/* No comment provided by engineer. */ +"Remove link tracking" = "Rimuovi il tracciamento del link"; + /* No comment provided by engineer. */ "Remove member" = "Rimuovi membro"; @@ -3897,12 +4410,18 @@ /* profile update event chat item */ "removed contact address" = "indirizzo di contatto rimosso"; +/* No comment provided by engineer. */ +"removed from group" = "rimosso dal gruppo"; + /* profile update event chat item */ "removed profile picture" = "immagine del profilo rimossa"; /* rcv group event chat item */ "removed you" = "ti ha rimosso/a"; +/* No comment provided by engineer. */ +"Removes messages and blocks members." = "Rimuove i messaggi e blocca i membri."; + /* No comment provided by engineer. */ "Renegotiate" = "Rinegoziare"; @@ -3912,24 +4431,66 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Rinegoziare la crittografia?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Ripetere la richiesta di connessione?"; - /* No comment provided by engineer. */ "Repeat download" = "Ripeti scaricamento"; /* No comment provided by engineer. */ "Repeat import" = "Ripeti importazione"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Ripetere la richiesta di ingresso?"; - /* No comment provided by engineer. */ "Repeat upload" = "Ripeti caricamento"; /* chat item action */ "Reply" = "Rispondi"; +/* chat item action */ +"Report" = "Segnala"; + +/* report reason */ +"Report content: only group moderators will see it." = "Segnala contenuto: solo i moderatori del gruppo lo vedranno."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Segnala profilo: solo i moderatori del gruppo lo vedranno."; + +/* report reason */ +"Report other: only group moderators will see it." = "Segnala altro: solo i moderatori del gruppo lo vedranno."; + +/* No comment provided by engineer. */ +"Report reason?" = "Motivo della segnalazione?"; + +/* alert title */ +"Report sent to moderators" = "Segnalazione inviata ai moderatori"; + +/* 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"; + +/* No comment provided by engineer. */ +"request is sent" = "richiesta inviata"; + +/* No comment provided by engineer. */ +"request to join rejected" = "richiesta di entrare rifiutata"; + +/* rcv group event chat item */ +"requested connection" = "connessione richiesta"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "connessione richiesta dal gruppo %@"; + +/* chat list item title */ +"requested to connect" = "richiesto di connettersi"; + /* No comment provided by engineer. */ "Required" = "Obbligatorio"; @@ -3975,17 +4536,29 @@ /* No comment provided by engineer. */ "Restore database error" = "Errore di ripristino del database"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Riprova"; /* chat item action */ "Reveal" = "Rivela"; +/* No comment provided by engineer. */ +"review" = "revisiona"; + /* No comment provided by engineer. */ "Review conditions" = "Leggi le condizioni"; /* No comment provided by engineer. */ -"Review later" = "Leggi più tardi"; +"Review group members" = "Revisiona i membri del gruppo"; + +/* admission stage */ +"Review members" = "Revisiona i membri"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Revisiona i membri prima di ammetterli (\"bussare\")."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "revisionato dagli amministratori"; /* No comment provided by engineer. */ "Revoke" = "Revoca"; @@ -4009,12 +4582,18 @@ "Safer groups" = "Gruppi più sicuri"; /* alert button - chat item action */ +chat item action */ "Save" = "Salva"; /* alert button */ "Save (and notify contacts)" = "Salva (e avvisa i contatti)"; +/* alert button */ +"Save (and notify members)" = "Salva (e informa i membri)"; + +/* alert title */ +"Save admission settings?" = "Salvare le impostazioni di ammissione?"; + /* alert button */ "Save and notify contact" = "Salva e avvisa il contatto"; @@ -4030,6 +4609,12 @@ /* No comment provided by engineer. */ "Save group profile" = "Salva il profilo del gruppo"; +/* alert title */ +"Save group profile?" = "Salvare 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"; @@ -4166,10 +4751,10 @@ "Send a live message - it will update for the recipient(s) as you type it" = "Invia un messaggio in diretta: si aggiornerà per i destinatari mentre lo digiti"; /* No comment provided by engineer. */ -"Send delivery receipts to" = "Invia ricevute di consegna a"; +"Send contact request?" = "Inviare una richiesta di contatto?"; /* No comment provided by engineer. */ -"send direct message" = "invia messaggio diretto"; +"Send delivery receipts to" = "Invia ricevute di consegna a"; /* No comment provided by engineer. */ "Send direct message to connect" = "Invia messaggio diretto per connetterti"; @@ -4198,18 +4783,30 @@ /* No comment provided by engineer. */ "Send notifications" = "Invia notifiche"; +/* No comment provided by engineer. */ +"Send private reports" = "Invia segnalazioni private"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Invia domande e idee"; /* No comment provided by engineer. */ "Send receipts" = "Invia ricevute"; +/* No comment provided by engineer. */ +"Send request" = "Invia richiesta"; + +/* No comment provided by engineer. */ +"Send request without message" = "Invia richiesta senza messaggio"; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Inviali dalla galleria o dalle tastiere personalizzate."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Invia fino a 100 ultimi messaggi ai nuovi membri."; +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Invia i tuoi commenti privati ai gruppi."; + /* alert message */ "Sender cancelled file transfer." = "Il mittente ha annullato il trasferimento del file."; @@ -4249,9 +4846,6 @@ /* No comment provided by engineer. */ "Sent directly" = "Inviato direttamente"; -/* notification */ -"Sent file event" = "Evento file inviato"; - /* message info title */ "Sent message" = "Messaggio inviato"; @@ -4298,10 +4892,10 @@ "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"; +"Server requires authorization to create queues, check password." = "Il server richiede l'autorizzazione di creare code, controlla la password."; /* server test error */ -"Server requires authorization to upload, check password" = "Il server richiede l'autorizzazione per il caricamento, controllare la password"; +"Server requires authorization to upload, check password." = "Il server richiede l'autorizzazione per l'invio, controlla la password."; /* No comment provided by engineer. */ "Server test failed!" = "Test del server fallito!"; @@ -4330,6 +4924,9 @@ /* 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…"; @@ -4342,6 +4939,12 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Impostalo al posto dell'autenticazione di sistema."; +/* No comment provided by engineer. */ +"Set member admission" = "Imposta l'ammissione dei membri"; + +/* 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"; @@ -4357,6 +4960,9 @@ /* No comment provided by engineer. */ "Set passphrase to export" = "Imposta la password per esportare"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Imposta la bio del profilo e il messaggio di benvenuto."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "Imposta il messaggio mostrato ai nuovi membri!"; @@ -4373,7 +4979,7 @@ "Shape profile images" = "Forma delle immagini del profilo"; /* alert action - chat item action */ +chat item action */ "Share" = "Condividi"; /* No comment provided by engineer. */ @@ -4397,6 +5003,12 @@ /* No comment provided by engineer. */ "Share link" = "Condividi link"; +/* alert button */ +"Share old address" = "Condividi l'indirizzo vecchio"; + +/* alert button */ +"Share old link" = "Condividi il link completo"; + /* No comment provided by engineer. */ "Share profile" = "Condividi il profilo"; @@ -4412,6 +5024,18 @@ /* No comment provided by engineer. */ "Share with contacts" = "Condividi con i contatti"; +/* No comment provided by engineer. */ +"Share your address" = "Condividi il tuo indirizzo"; + +/* No comment provided by engineer. */ +"Short description" = "Descrizione breve"; + +/* No comment provided by engineer. */ +"Short link" = "Link breve"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Indirizzo breve di SimpleX"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Mostra → nei messaggi inviati via instradamento privato."; @@ -4454,8 +5078,14 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "Indirizzo SimpleX o link una tantum?"; +/* alert title */ +"SimpleX address settings" = "Accetta automaticamente le impostazioni"; + +/* 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"; +"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."; @@ -4496,6 +5126,9 @@ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protocolli di SimpleX esaminati da Trail of Bits."; +/* simplex link type */ +"SimpleX relay link" = "Link del relay SimpleX"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Modalità incognito semplificata"; @@ -4538,6 +5171,10 @@ /* 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."; @@ -4595,6 +5232,9 @@ /* No comment provided by engineer. */ "Stopping chat" = "Arresto della chat"; +/* No comment provided by engineer. */ +"Storage" = "Archiviazione"; + /* No comment provided by engineer. */ "strike" = "barrato"; @@ -4637,9 +5277,21 @@ /* No comment provided by engineer. */ "Tap button " = "Tocca il pulsante "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "Tocca Connetti per chattare"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "Tocca Connetti per inviare la richiesta"; + +/* No comment provided by engineer. */ +"Tap Connect to use bot" = "Tocca Connetti per usare il bot"; + /* No comment provided by engineer. */ "Tap Create SimpleX address in the menu to create it later." = "Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi."; +/* No comment provided by engineer. */ +"Tap Join group" = "Tocca Entra nel gruppo"; + /* No comment provided by engineer. */ "Tap to activate profile." = "Tocca per attivare il profilo."; @@ -4661,9 +5313,15 @@ /* No comment provided by engineer. */ "TCP connection" = "Connessione TCP"; +/* No comment provided by engineer. */ +"TCP connection bg timeout" = "Scadenza conness. TCP in sec. piano"; + /* 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"; @@ -4673,12 +5331,15 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* 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"; @@ -4697,6 +5358,9 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Grazie agli utenti – contribuite via Weblate!"; +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "L'indirizzo sarà breve e il tuo profilo verrà condiviso attraverso l'indirizzo."; + /* 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."; @@ -4736,6 +5400,9 @@ /* 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." = "L'ID del messaggio successivo non è corretto (inferiore o uguale al precedente).\nPuò accadere a causa di qualche bug o quando la connessione è compromessa."; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "Il link sarà breve e il profilo del gruppo verrà condiviso attraverso il link."; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "Il messaggio verrà eliminato per tutti i membri."; @@ -4751,22 +5418,16 @@ /* 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."; - /* 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 same conditions will apply to operator(s): **%@**." = "Le stesse condizioni si applicheranno agli operatori **%@**."; - /* 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! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Il mittente NON verrà avvisato"; /* No comment provided by engineer. */ @@ -4799,6 +5460,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."; @@ -4824,17 +5488,23 @@ "This group no longer exists." = "Questo gruppo non esiste più."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Questo è il tuo link una tantum!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Questo è il tuo indirizzo SimpleX!"; +"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. */ +"This setting is for your current profile **%@**." = "Questa impostazione è per il tuo profilo attuale **%@**."; + +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Il tempo di scomparsa è impostato solo per i contatti nuovi."; + /* No comment provided by engineer. */ "Title" = "Titoli"; @@ -4883,9 +5553,15 @@ /* No comment provided by engineer. */ "To send" = "Per inviare"; +/* alert message */ +"To send commands you must be connected." = "Per inviare comandi devi essere connesso/a."; + /* 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."; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "Per usare un altro profilo dopo il tentativo di connessione, elimina la chat e usa di nuovo il link."; + /* No comment provided by engineer. */ "To use the servers of **%@**, accept conditions of use." = "Per usare i server di **%@**, accetta le condizioni d'uso."; @@ -4898,6 +5574,9 @@ /* 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"; @@ -4910,11 +5589,8 @@ /* 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: %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Tentativo di connessione al server usato per ricevere messaggi da questo contatto."; +/* subscription status explanation */ +"Trying to connect to the server used to receive messages from this connection." = "Tentativo di connessione al server usato per ricevere messaggi da questa connessione."; /* No comment provided by engineer. */ "Turkish interface" = "Interfaccia in turco"; @@ -5006,10 +5682,7 @@ /* authentication reason */ "Unlock app" = "Sblocca l'app"; -/* No comment provided by engineer. */ -"unmute" = "riattiva notifiche"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Riattiva notifiche"; /* No comment provided by engineer. */ @@ -5018,6 +5691,9 @@ /* 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."; @@ -5033,6 +5709,9 @@ /* No comment provided by engineer. */ "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"; @@ -5042,9 +5721,27 @@ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "L'aggiornamento delle impostazioni riconnetterà il client a tutti i server."; +/* alert button */ +"Upgrade" = "Aggiorna"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Aggiorna l'indirizzo"; + +/* alert message */ +"Upgrade address?" = "Aggiornare l'indirizzo?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "Aggiorna e apri chat"; +/* alert message */ +"Upgrade group link?" = "Aggiornare il link del gruppo?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Aggiungi link"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Aggiorna il tuo indirizzo"; + /* No comment provided by engineer. */ "Upload errors" = "Errori di invio"; @@ -5072,7 +5769,7 @@ /* No comment provided by engineer. */ "Use chat" = "Usa la chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Usa il profilo attuale"; /* No comment provided by engineer. */ @@ -5088,9 +5785,12 @@ "Use from desktop" = "Usa dal desktop"; /* No comment provided by engineer. */ -"Use iOS call interface" = "Usa interfaccia di chiamata iOS"; +"Use incognito profile" = "Usa profilo in incognito"; /* No comment provided by engineer. */ +"Use iOS call interface" = "Usa interfaccia di chiamata iOS"; + +/* new chat action */ "Use new incognito profile" = "Usa nuovo profilo in incognito"; /* No comment provided by engineer. */ @@ -5114,12 +5814,21 @@ /* 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. */ "Use the app with one hand." = "Usa l'app con una mano sola."; +/* No comment provided by engineer. */ +"Use web port" = "Usa porta web"; + /* No comment provided by engineer. */ "User selection" = "Selezione utente"; @@ -5270,6 +5979,9 @@ /* No comment provided by engineer. */ "Welcome message is too long" = "Il messaggio di benvenuto è troppo lungo"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Dai il benvenuto ai tuoi contatti 👋"; + /* No comment provided by engineer. */ "What's new" = "Novità"; @@ -5339,6 +6051,9 @@ /* No comment provided by engineer. */ "You accepted connection" = "Hai accettato la connessione"; +/* snd group event chat item */ +"you accepted this member" = "hai accettato questo membro"; + /* No comment provided by engineer. */ "You allow" = "Lo consenti"; @@ -5351,36 +6066,33 @@ /* No comment provided by engineer. */ "You are already connected with %@." = "Sei già connesso/a con %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Ti stai già connettendo a %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Ti stai già connettendo tramite questo link una tantum!"; /* No comment provided by engineer. */ "You are already in group %@." = "Sei già nel gruppo %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Stai già entrando nel gruppo %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Stai già entrando nel gruppo tramite questo link!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Stai già entrando nel gruppo tramite questo link."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Stai già entrando nel gruppo!\nRipetere la richiesta di ingresso?"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Sei connesso/a al server usato per ricevere messaggi da questo contatto."; - -/* No comment provided by engineer. */ -"you are invited to group" = "sei stato/a invitato/a al gruppo"; +/* subscription status explanation */ +"You are connected to the server used to receive messages from this connection." = "Sei connesso/a al server usato per ricevere messaggi da questa connessione."; /* No comment provided by engineer. */ "You are invited to group" = "Sei stato/a invitato/a al gruppo"; +/* subscription status explanation */ +"You are not connected to the server used to receive messages from this connection (no subscription)." = "Non sei connesso/a al server usato per ricevere messaggi da questa connessione (nessuna iscrizione)."; + /* 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."; @@ -5396,9 +6108,6 @@ /* 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 operators in Network & servers settings." = "Puoi configurare gli operatori nelle impostazioni di rete e server."; - /* No comment provided by engineer. */ "You can configure servers via settings." = "Puoi configurare i server nelle impostazioni."; @@ -5453,7 +6162,10 @@ /* 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. */ +/* alert message */ +"You can view your reports in Chat with admins." = "Puoi vedere le tue segnalazioni nella chat con gli amministratori."; + +/* alert title */ "You can't send messages!" = "Non puoi inviare messaggi!"; /* chat item text */ @@ -5474,10 +6186,7 @@ /* 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!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Hai già richiesto la connessione!\nRipetere la richiesta di connessione?"; /* No comment provided by engineer. */ @@ -5525,9 +6234,15 @@ /* chat list item description */ "you shared one-time link incognito" = "hai condiviso un link incognito una tantum"; +/* token info */ +"You should receive notifications." = "Dovresti ricevere le notifiche."; + /* snd group event chat item */ "you unblocked %@" = "hai sbloccato %@"; +/* No comment provided by engineer. */ +"You will be able to send messages **only after your request is accepted**." = "Potrai inviare messaggi **solo dopo che la tua richiesta verrà accettata**."; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi!"; @@ -5543,9 +6258,6 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Dovrai autenticarti quando avvii o riapri l'app dopo 30 secondi in secondo piano."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Ti connetterai a tutti i membri del gruppo."; - /* 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."; @@ -5567,6 +6279,9 @@ /* 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 business contact" = "Il tuo contatto lavorativo"; + /* No comment provided by engineer. */ "Your calls" = "Le tue chiamate"; @@ -5582,8 +6297,14 @@ /* No comment provided by engineer. */ "Your chat profiles" = "I tuoi profili di chat"; +/* alert message */ +"Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "La tua chat è stata spostata su %@ , ma si è verificato un errore imprevisto mentre venivi reindirizzato/a al profilo."; + /* No comment provided by engineer. */ -"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."; +"Your connection was moved to %@ but an error happened when switching 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" = "Il tuo contatto"; /* 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 (%@)."; @@ -5603,6 +6324,9 @@ /* No comment provided by engineer. */ "Your current profile" = "Il tuo profilo attuale"; +/* No comment provided by engineer. */ +"Your group" = "Il tuo gruppo"; + /* No comment provided by engineer. */ "Your ICE servers" = "I tuoi server ICE"; @@ -5618,15 +6342,15 @@ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Verrà condiviso il tuo profilo **%@**."; +/* No comment provided by engineer. */ +"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 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."; - /* No comment provided by engineer. */ "Your random profile" = "Il tuo profilo casuale"; @@ -5642,6 +6366,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"; - diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index da0ba42a86..d4510af72f 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1,45 +1,30 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* 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)"; @@ -76,6 +61,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**推奨**: デバイス トークンと通知は SimpleX Chat 通知サーバーに送信されますが、メッセージの内容、サイズ、送信者は送信されません。"; +/* 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." = "**警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。"; @@ -101,7 +89,7 @@ "%@" = "%@"; /* No comment provided by engineer. */ -"%@ (current)" = "%@ (現在)"; +"%@ (current)" = "%@ (現在)"; /* copied message info */ "%@ (current):" = "%@ (現在):"; @@ -136,12 +124,21 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ は検証されています"; +/* No comment provided by engineer. */ +"%@ server" = "%@ サーバー"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ サーバー"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ アップロード済"; /* notification title */ "%@ wants to connect!" = "%@ が接続を希望しています!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@や%@など%lld人のメンバー"; @@ -154,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 分"; @@ -166,15 +178,15 @@ /* time interval */ "%d sec" = "%d 秒"; +/* delete after time */ +"%d seconds(s)" = "%d 秒"; + /* integrity error chat item */ "%d skipped message(s)" = "%d 件のスキップされたメッセージ"; /* time interval */ "%d weeks" = "%d 週"; -/* No comment provided by engineer. */ -"%lld" = "%lld"; - /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; @@ -208,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 秒"; @@ -256,7 +265,8 @@ /* No comment provided by engineer. */ "0s" = "0秒"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1日"; /* time interval */ @@ -265,12 +275,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分"; @@ -304,6 +325,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "アドレス変更を中止しますか?"; +/* No comment provided by engineer. */ +"About operators" = "オペレーターについて"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "SimpleX Chat について"; @@ -311,23 +335,45 @@ "above, then choose:" = "上で選んでください:"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "承諾"; +/* alert action */ +"Accept as member" = "メンバーとして承認する"; + +/* alert action */ +"Accept as observer" = "オブザーバーとして承認する"; + +/* No comment provided by engineer. */ +"Accept conditions" = "条件に同意する"; + /* No comment provided by engineer. */ "Accept connection request?" = "接続要求を承認?"; +/* alert title */ +"Accept contact request" = "連絡先リクエストを受け入れる"; + /* notification body */ "Accept contact request from %@?" = "%@ からの連絡要求を受け入れますか?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "シークレットモードで承諾"; +/* alert title */ +"Accept member" = "メンバーを承認する"; + /* call status */ "accepted call" = "受けた通話"; +/* No comment provided by engineer. */ +"Accepted conditions" = "承諾された条件"; + +/* No comment provided by engineer. */ +"Acknowledged" = "了承済み"; + /* 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." = "プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。"; @@ -346,6 +392,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" = "アドレス"; @@ -361,6 +413,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "ネットワーク詳細設定"; +/* No comment provided by engineer. */ +"Advanced settings" = "詳細設定"; + /* chat item text */ "agreeing encryption for %@…" = "%@の暗号化に同意しています…"; @@ -382,6 +437,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." = "あなたの連絡先が繋がったまま継続します。"; @@ -401,7 +459,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." = "連絡先が許可している場合にのみ、メッセージへのリアクションを許可します。"; @@ -415,6 +473,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時間)"; @@ -451,15 +512,18 @@ /* No comment provided by engineer. */ "Already connected?" = "すでに接続済みですか?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "既に接続中です!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "すでにグループに参加しています!"; /* pref value */ "always" = "常に"; +/* No comment provided by engineer. */ +"Always use private routing." = "プライベートルーティングを常に使用する。"; + /* No comment provided by engineer. */ "Always use relay" = "常にリレーを経由する"; @@ -497,7 +561,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" = "添付する"; @@ -602,7 +675,8 @@ "Can't invite contacts!" = "連絡先を招待できません!"; /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "中止"; /* feature offered item */ @@ -642,7 +716,7 @@ "Change self-destruct mode" = "自己破壊モードの変更"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "自己破壊パスコードを変更する"; /* chat item text */ @@ -762,9 +836,6 @@ /* server test step */ "Connect" = "接続"; -/* No comment provided by engineer. */ -"Connect incognito" = "シークレットモードで接続"; - /* No comment provided by engineer. */ "Connect to desktop" = "デスクトップに接続"; @@ -774,10 +845,10 @@ /* No comment provided by engineer. */ "Connect to your friends faster." = "友達ともっと速くつながりましょう。"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "リンク経由で接続"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "ワンタイムリンクで接続"; /* No comment provided by engineer. */ @@ -835,9 +906,9 @@ "Connection" = "接続"; /* No comment provided by engineer. */ -"Connection and servers status." = "接続とサーバーのステータス"; +"Connection and servers status." = "接続とサーバーのステータス。"; -/* No comment provided by engineer. */ +/* alert title */ "Connection error" = "接続エラー"; /* No comment provided by engineer. */ @@ -852,7 +923,7 @@ /* No comment provided by engineer. */ "Connection terminated" = "接続停止"; -/* No comment provided by engineer. */ +/* alert title */ "Connection timeout" = "接続タイムアウト"; /* connection information */ @@ -912,12 +983,12 @@ /* 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" = "キューの作成"; -/* No comment provided by engineer. */ -"Create secret group" = "シークレットグループを作成する"; - /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleXアドレスの作成"; @@ -1020,7 +1091,8 @@ /* message decrypt error item */ "Decryption error" = "復号化エラー"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "デフォルト (%@)"; /* No comment provided by engineer. */ @@ -1030,8 +1102,7 @@ "default (yes)" = "デフォルト(はい)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "削除"; /* No comment provided by engineer. */ @@ -1097,7 +1168,7 @@ /* No comment provided by engineer. */ "Delete message?" = "メッセージを削除しますか?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "メッセージを削除"; /* No comment provided by engineer. */ @@ -1224,12 +1295,12 @@ "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" = "有効にしない"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "次から表示しない"; /* No comment provided by engineer. */ @@ -1262,7 +1333,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. */ @@ -1406,7 +1477,7 @@ /* No comment provided by engineer. */ "Error changing role" = "役割変更にエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "設定変更にエラー発生"; /* No comment provided by engineer. */ @@ -1427,19 +1498,19 @@ /* No comment provided by engineer. */ "Error decrypting file" = "ファイルの復号エラー"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "チャットデータベース削除にエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "チャット削除にエラー発生!"; /* No comment provided by engineer. */ "Error deleting connection" = "接続の削除エラー"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "データベースの削除にエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "古いデータベースを削除にエラー発生"; /* No comment provided by engineer. */ @@ -1454,10 +1525,10 @@ /* No comment provided by engineer. */ "Error encrypting database" = "データベース暗号化ににエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "チャットデータベースのエキスポートにエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "チャットデータベースのインポートにエラー発生"; /* No comment provided by engineer. */ @@ -1466,7 +1537,7 @@ /* alert title */ "Error receiving file" = "ファイル受信にエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "メンバー除名にエラー発生"; /* No comment provided by engineer. */ @@ -1520,7 +1591,9 @@ /* No comment provided by engineer. */ "Error: " = "エラー : "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "エラー : %@"; /* No comment provided by engineer. */ @@ -1532,9 +1605,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" = "保存せずに閉じる"; @@ -1592,6 +1662,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "チャットを素早く検索"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "サーバアドレスの証明証IDが正しくないかもしれません"; + /* No comment provided by engineer. */ "Fix" = "修正"; @@ -1827,7 +1900,7 @@ "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" = "すぐに"; +"Instant" = "即時"; /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "インスタントプッシュ通知は非表示になります!\n"; @@ -1920,9 +1993,9 @@ "Join" = "参加"; /* No comment provided by engineer. */ -"join as %@" = "%@ として参加"; +"Join as %@" = "%@ として参加"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "グループに参加"; /* No comment provided by engineer. */ @@ -2084,6 +2157,9 @@ /* 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…" = "データベースのアーカイブを移行しています…"; @@ -2097,7 +2173,7 @@ "Migration is completed" = "移行が完了しました"; /* No comment provided by engineer. */ -"Migrations:" = "移行"; +"Migrations:" = "移行:"; /* time unit */ "minutes" = "分"; @@ -2132,7 +2208,7 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "複数チャットのプロフィール"; -/* swipe action */ +/* notification label action */ "Mute" = "ミュート"; /* No comment provided by engineer. */ @@ -2147,10 +2223,10 @@ /* No comment provided by engineer. */ "Network settings" = "ネットワーク設定"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "ネットワーク状況"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "一度も"; /* notification */ @@ -2244,8 +2320,9 @@ "observer" = "オブザーバー"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "オフ"; /* blur media */ @@ -2257,7 +2334,9 @@ /* feature offered item */ "offered %@: %@" = "提供された %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "OK"; /* No comment provided by engineer. */ @@ -2320,10 +2399,10 @@ /* 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. */ +/* new chat action */ "Open chat" = "チャットを開く"; /* authentication reason */ @@ -2377,7 +2456,7 @@ /* 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. */ +/* alert message */ "Please check your network connection with %@ and try again." = "%@ を使用してネットワーク接続を確認し、再試行してください。"; /* No comment provided by engineer. */ @@ -2410,9 +2489,6 @@ /* No comment provided by engineer. */ "Polish interface" = "ポーランド語UI"; -/* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "サーバアドレスの証明証IDが正しくないかもしれません"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "添付を含めて、下書きを保存する。"; @@ -2431,6 +2507,9 @@ /* No comment provided by engineer. */ "Private filenames" = "プライベートなファイル名"; +/* name of notes to self */ +"Private notes" = "プライベートノート"; + /* No comment provided by engineer. */ "Profile and server connections" = "プロフィールとサーバ接続"; @@ -2515,9 +2594,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "確認を受け取りました…"; -/* notification */ -"Received file event" = "ファイル受信イベント"; - /* message info title */ "Received message" = "受信したメッセージ"; @@ -2548,14 +2624,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "電池使用量低減"; -/* reject incoming call via notification - swipe action */ +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "拒否"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "連絡を拒否(送信者には通知されません)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "連絡要求を拒否する"; /* call status */ @@ -2649,7 +2726,7 @@ "Run chat" = "チャット起動"; /* alert button - chat item action */ +chat item action */ "Save" = "保存"; /* alert button */ @@ -2787,9 +2864,6 @@ /* copied message info */ "Sent at: %@" = "送信日時: %@"; -/* notification */ -"Sent file event" = "送信済みファイルイベント"; - /* message info title */ "Sent message" = "送信"; @@ -2797,10 +2871,10 @@ "Sent messages will be deleted after set time." = "一定時間が経ったら送信されたメッセージが削除されます。"; /* server test error */ -"Server requires authorization to create queues, check password" = "キューを作成するにはサーバーの認証が必要です。パスワードを確認してください"; +"Server requires authorization to create queues, check password." = "キューを作成するにはサーバーの認証が必要です。パスワードを確認してください"; /* server test error */ -"Server requires authorization to upload, check password" = "アップロードにはサーバーの認証が必要です。パスワードを確認してください"; +"Server requires authorization to upload, check password." = "アップロードにはサーバーの認証が必要です。パスワードを確認してください"; /* No comment provided by engineer. */ "Server test failed!" = "サーバテスト失敗!"; @@ -2836,7 +2910,7 @@ "Settings" = "設定"; /* alert action - chat item action */ +chat item action */ "Share" = "共有する"; /* No comment provided by engineer. */ @@ -3058,13 +3132,10 @@ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "古いデータベースは移行時に削除されなかったので、削除することができます。"; -/* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "プロフィールは連絡先にしか共有されません。"; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "長らくお待たせしました! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "送信者には通知されません"; /* No comment provided by engineer. */ @@ -3127,12 +3198,6 @@ /* 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" = "オフにする"; @@ -3184,7 +3249,7 @@ /* authentication reason */ "Unlock app" = "アプリのロック解除"; -/* swipe action */ +/* notification label action */ "Unmute" = "ミュート解除"; /* swipe action */ @@ -3217,7 +3282,7 @@ /* No comment provided by engineer. */ "Use chat" = "チャット"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "現在のプロファイルを使用する"; /* No comment provided by engineer. */ @@ -3226,7 +3291,7 @@ /* No comment provided by engineer. */ "Use iOS call interface" = "iOS通話インターフェースを使用する"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "新しいシークレットプロファイルを使用する"; /* No comment provided by engineer. */ @@ -3361,12 +3426,6 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "すでに %@ に接続されています。"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "この連絡先から受信するメッセージのサーバに既に接続してます。"; - -/* No comment provided by engineer. */ -"you are invited to group" = "グループ招待が届きました"; - /* No comment provided by engineer. */ "You are invited to group" = "グループ招待が届きました"; @@ -3388,6 +3447,9 @@ /* 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 chat with %@" = "%@ にメッセージを送信できるようになりました"; @@ -3409,7 +3471,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "メッセージの書式にmarkdownを使用することができます:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "メッセージを送信できませんでした!"; /* chat item text */ @@ -3536,10 +3598,10 @@ "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 random profile" = "あなたのランダム・プロフィール"; @@ -3553,6 +3615,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "あなたのSimpleXアドレス"; -/* No comment provided by engineer. */ -"Your SMP servers" = "あなたのSMPサーバ"; - diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 7004d0d124..79e3da3b01 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (kan gekopieerd worden)"; @@ -11,7 +5,7 @@ "_italic_" = "\\_cursief_"; /* No comment provided by engineer. */ -"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- verbinding maken met [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! \n- ontvangst bevestiging(tot 20 leden). \n- sneller en stabieler."; +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- verbinding maken met [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- ontvangst bevestiging(tot 20 leden).\n- sneller en stabieler."; /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- stabielere berichtbezorging.\n- een beetje betere groepen.\n- en meer!"; @@ -22,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)"; @@ -199,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)"; @@ -241,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"; @@ -289,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 dag"; /* time interval */ @@ -298,12 +278,17 @@ /* 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"; @@ -356,10 +341,17 @@ "Accent" = "Accent"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Accepteer"; +/* alert action */ +"Accept as member" = "Accepteren als lid"; + +/* alert action */ +"Accept as observer" = "Accepteren als waarnemer"; + /* No comment provided by engineer. */ "Accept conditions" = "Accepteer voorwaarden"; @@ -369,10 +361,16 @@ /* notification body */ "Accept contact request from %@?" = "Accepteer contactverzoek van %@?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Accepteer incognito"; +/* alert title */ +"Accept member" = "Lid accepteren"; + +/* rcv group event chat item */ +"accepted %@" = "geaccepteerd %@"; + /* call status */ "accepted call" = "geaccepteerde oproep"; @@ -382,12 +380,18 @@ /* chat list item title */ "accepted invitation" = "geaccepteerde uitnodiging"; +/* rcv group event chat item */ +"accepted you" = "heb je geaccepteerd"; + /* 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"; @@ -397,6 +401,9 @@ /* No comment provided by engineer. */ "Add friends" = "Vrienden toevoegen"; +/* No comment provided by engineer. */ +"Add list" = "Lijst toevoegen"; + /* No comment provided by engineer. */ "Add profile" = "Profiel toevoegen"; @@ -412,6 +419,9 @@ /* No comment provided by engineer. */ "Add to another device" = "Toevoegen aan een ander apparaat"; +/* No comment provided by engineer. */ +"Add to list" = "Toevoegen aan lijst"; + /* No comment provided by engineer. */ "Add welcome message" = "Welkom bericht toevoegen"; @@ -469,12 +479,21 @@ /* chat item text */ "agreeing encryption…" = "versleuteling overeenkomen…"; +/* member criteria value */ +"all" = "alle"; + +/* 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."; @@ -502,6 +521,12 @@ /* 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 servers" = "Alle servers"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Al uw contacten blijven verbonden."; @@ -547,6 +572,9 @@ /* 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."; @@ -580,10 +608,10 @@ /* No comment provided by engineer. */ "Already connected?" = "Al verbonden?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Al bezig met verbinden!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Al lid van de groep!"; /* pref value */ @@ -601,6 +629,9 @@ /* 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"; @@ -616,6 +647,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"; @@ -643,15 +677,36 @@ /* 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"; @@ -700,9 +755,6 @@ /* 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"; @@ -730,6 +782,9 @@ /* 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."; @@ -742,6 +797,9 @@ /* 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 ✅"; @@ -773,9 +831,10 @@ "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. */ @@ -814,9 +873,15 @@ /* 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"; @@ -856,8 +921,12 @@ /* No comment provided by engineer. */ "Can't message member" = "Kan geen bericht sturen naar lid"; +/* No comment provided by engineer. */ +"can't send messages" = "kan geen berichten versturen"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Annuleren"; /* No comment provided by engineer. */ @@ -884,6 +953,9 @@ /* 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"; @@ -912,7 +984,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 */ @@ -936,7 +1008,7 @@ /* No comment provided by engineer. */ "Chat already exists" = "Chat bestaat al"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Chat bestaat al!"; /* No comment provided by engineer. */ @@ -990,9 +1062,18 @@ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt!"; +/* chat toolbar */ +"Chat with admins" = "Chat met beheerders"; + +/* No comment provided by engineer. */ +"Chat with member" = "Chat met lid"; + /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Chats with members" = "Chats met leden"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Controleer uw berichten elke 20 minuten."; @@ -1032,6 +1113,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?"; @@ -1047,6 +1134,9 @@ /* No comment provided by engineer. */ "colored" = "gekleurd"; +/* report reason */ +"Community guidelines violation" = "Schending van de communityrichtlijnen"; + /* server test step */ "Compare file" = "Bestand vergelijken"; @@ -1068,15 +1158,9 @@ /* 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. */ +/* alert button */ "Conditions of use" = "Gebruiksvoorwaarden"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for operator(s): **%@**." = "Voorwaarden worden geaccepteerd voor operator(s): **%@**."; - /* No comment provided by engineer. */ "Conditions will be accepted for the operator(s): **%@**." = "Voorwaarden worden geaccepteerd voor de operator(s): **%@**."; @@ -1089,6 +1173,9 @@ /* 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"; @@ -1119,15 +1206,15 @@ /* No comment provided by engineer. */ "Confirm upload" = "Bevestig het uploaden"; +/* token status text */ +"Confirmed" = "Bevestigd"; + /* server test step */ "Connect" = "Verbind"; /* No comment provided by engineer. */ "Connect automatically" = "Automatisch verbinden"; -/* No comment provided by engineer. */ -"Connect incognito" = "Verbind incognito"; - /* No comment provided by engineer. */ "Connect to desktop" = "Verbinden met desktop"; @@ -1137,25 +1224,22 @@ /* 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?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Verbinding maken met jezelf?\nDit is uw eigen eenmalige link!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Verbinding maken met jezelf?\nDit is uw eigen SimpleX adres!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Verbinding maken via contactadres"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Maak verbinding via link"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Verbinden via een eenmalige link?"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Verbonden met %@"; /* No comment provided by engineer. */ @@ -1167,9 +1251,6 @@ /* 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"; @@ -1219,6 +1300,9 @@ "Connection and servers status." = "Verbindings- en serverstatus."; /* No comment provided by engineer. */ +"Connection blocked" = "Verbinding geblokkeerd"; + +/* alert title */ "Connection error" = "Verbindingsfout"; /* No comment provided by engineer. */ @@ -1227,19 +1311,28 @@ /* 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. */ +/* alert title */ "Connection timeout" = "Timeout verbinding"; /* No comment provided by engineer. */ @@ -1260,9 +1353,15 @@ /* 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 deleted!" = "Contact verwijderd!"; +/* No comment provided by engineer. */ +"contact disabled" = "contact uitgeschakeld"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "contact heeft e2e-codering"; @@ -1281,6 +1380,9 @@ /* No comment provided by engineer. */ "Contact name" = "Contact naam"; +/* No comment provided by engineer. */ +"contact not ready" = "contact niet klaar"; + /* No comment provided by engineer. */ "Contact preferences" = "Contact voorkeuren"; @@ -1293,6 +1395,9 @@ /* 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"; @@ -1335,6 +1440,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/). 💻"; @@ -1344,9 +1452,6 @@ /* server test step */ "Create queue" = "Maak een wachtrij"; -/* No comment provided by engineer. */ -"Create secret group" = "Maak een geheime groep aan"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Maak een SimpleX adres aan"; @@ -1476,7 +1581,8 @@ /* No comment provided by engineer. */ "decryption errors" = "decoderingsfouten"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "standaard (%@)"; /* No comment provided by engineer. */ @@ -1486,8 +1592,7 @@ "default (yes)" = "standaard (ja)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Verwijderen"; /* No comment provided by engineer. */ @@ -1514,12 +1619,18 @@ /* No comment provided by engineer. */ "Delete chat" = "Chat verwijderen"; +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Verwijder chatberichten van uw apparaat."; + /* No comment provided by engineer. */ "Delete chat profile" = "Chatprofiel verwijderen"; /* No comment provided by engineer. */ "Delete chat profile?" = "Chatprofiel verwijderen?"; +/* alert title */ +"Delete chat with member?" = "Chat met lid verwijderen?"; + /* No comment provided by engineer. */ "Delete chat?" = "Chat verwijderen?"; @@ -1568,13 +1679,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. */ @@ -1598,6 +1712,9 @@ /* 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."; @@ -1706,6 +1823,12 @@ /* 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"; @@ -1766,6 +1889,9 @@ /* 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"; @@ -1773,13 +1899,19 @@ "Don't enable" = "Niet inschakelen"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Mis geen belangrijke berichten."; + +/* alert action */ "Don't show again" = "Niet meer weergeven"; +/* No comment provided by engineer. */ +"Done" = "Klaar"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Downgraden en chat openen"; /* alert button - chat item action */ +chat item action */ "Download" = "Downloaden"; /* No comment provided by engineer. */ @@ -1836,14 +1968,14 @@ /* 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" = "Flux inschakelen"; +"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"; @@ -1956,6 +2088,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"; @@ -2010,6 +2145,9 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Fout bij het accepteren van een contactverzoek"; +/* alert title */ +"Error accepting member" = "Fout bij het accepteren van lid"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Fout bij het toevoegen van leden"; @@ -2025,13 +2163,16 @@ /* No comment provided by engineer. */ "Error changing role" = "Fout bij wisselen van rol"; -/* No comment provided by engineer. */ +/* alert title */ "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"; + +/* alert message */ "Error connecting to forwarding server %@. Please try later." = "Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw."; /* No comment provided by engineer. */ @@ -2043,6 +2184,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"; @@ -2052,22 +2196,28 @@ /* 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"; -/* No comment provided by engineer. */ +/* alert title */ +"Error deleting chat" = "Fout bij het verwijderen van chat met lid"; + +/* alert title */ "Error deleting chat database" = "Fout bij het verwijderen van de chat database"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Fout bij verwijderen gesprek!"; /* No comment provided by engineer. */ "Error deleting connection" = "Fout bij verwijderen van verbinding"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Fout bij het verwijderen van de database"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Fout bij het verwijderen van de oude database"; /* No comment provided by engineer. */ @@ -2088,13 +2238,13 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Fout bij het versleutelen van de database"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Fout bij het importeren van de chat database"; /* No comment provided by engineer. */ @@ -2118,12 +2268,21 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Fout bij opnieuw verbinden van servers"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "Fout bij registreren voor meldingen"; + +/* alert title */ "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 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"; @@ -2166,7 +2325,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Fout bij het stoppen van de chat"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Fout bij wisselen van profiel"; /* alertTitle */ @@ -2175,6 +2334,9 @@ /* 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"; @@ -2199,7 +2361,9 @@ /* No comment provided by engineer. */ "Error: " = "Fout: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Fout: %@"; /* No comment provided by engineer. */ @@ -2217,9 +2381,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Zelfs wanneer uitgeschakeld in het gesprek."; -/* No comment provided by engineer. */ -"event happened" = "gebeurtenis gebeurd"; - /* No comment provided by engineer. */ "Exit without saving" = "Afsluiten zonder opslaan"; @@ -2229,6 +2390,9 @@ /* No comment provided by engineer. */ "expired" = "verlopen"; +/* token status text */ +"Expired" = "Verlopen"; + /* No comment provided by engineer. */ "Export database" = "Database exporteren"; @@ -2253,18 +2417,30 @@ /* 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."; @@ -2322,6 +2498,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "Vind chats sneller"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Mogelijk is de certificaat vingerafdruk in het server adres onjuist"; + /* No comment provided by engineer. */ "Fix" = "Herstel"; @@ -2341,7 +2520,7 @@ "Fix not supported by group member" = "Herstel wordt niet ondersteund door groepslid"; /* No comment provided by engineer. */ -"for better metadata privacy." = "voor betere privacy van metagegevens."; +"For all moderators" = "Voor alle moderators"; /* servers error */ "For chat profile %@:" = "Voor chatprofiel %@:"; @@ -2352,6 +2531,9 @@ /* 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"; @@ -2388,8 +2570,8 @@ /* 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."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "De doorstuurserver %1$@ kon geen verbinding maken met de bestemmingsserver %2$@. 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: %@."; @@ -2424,6 +2606,9 @@ /* 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"; @@ -2439,7 +2624,7 @@ /* No comment provided by engineer. */ "Group already exists" = "Groep bestaat al"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Groep bestaat al!"; /* No comment provided by engineer. */ @@ -2463,6 +2648,9 @@ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Groep uitnodiging is niet meer geldig, deze is verwijderd door de afzender."; +/* No comment provided by engineer. */ +"group is deleted" = "groep is verwijderd"; + /* No comment provided by engineer. */ "Group link" = "Groep link"; @@ -2496,9 +2684,15 @@ /* 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"; @@ -2535,6 +2729,9 @@ /* 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. */ "How SimpleX works" = "Hoe SimpleX werkt"; @@ -2622,6 +2819,12 @@ /* 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"; @@ -2688,6 +2891,21 @@ /* No comment provided by engineer. */ "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"; @@ -2703,7 +2921,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Ongeldige weergavenaam!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Ongeldige link"; /* No comment provided by engineer. */ @@ -2800,27 +3018,21 @@ "Japanese interface" = "Japanse interface"; /* swipe action */ -"Join" = "Word lid van"; +"Join" = "Word lid"; /* No comment provided by engineer. */ -"join as %@" = "deelnemen als %@"; +"Join as %@" = "deelnemen als %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Word lid van groep"; /* No comment provided by engineer. */ "Join group conversations" = "Neem deel aan groepsgesprekken"; -/* No comment provided by engineer. */ -"Join group?" = "Deelnemen aan groep?"; - /* No comment provided by engineer. */ "Join incognito" = "Doe incognito mee"; -/* No comment provided by engineer. */ -"Join with current profile" = "Word lid met huidig profiel"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Sluit u aan bij uw groep?\nDit is jouw link voor groep %@!"; /* No comment provided by engineer. */ @@ -2889,6 +3101,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"; @@ -2952,12 +3173,21 @@ /* profile update event chat item */ "member %@ changed to %@" = "lid %1$@ gewijzigd in %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Toelating van leden"; + /* rcv group event chat item */ "member connected" = "is toegetreden"; +/* No comment provided by engineer. */ +"member has old version" = "lid heeft oude versie"; + /* 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."; @@ -2973,12 +3203,18 @@ /* 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!"; +/* alert message */ +"Member will join the group, accept member?" = "Lid zal toetreden tot de groep, lid accepteren?"; + /* 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."; @@ -2994,6 +3230,9 @@ /* 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"; @@ -3066,6 +3305,9 @@ /* 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"; @@ -3138,9 +3380,15 @@ /* 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!"; @@ -3156,12 +3404,12 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Meerdere chatprofielen"; -/* No comment provided by engineer. */ -"mute" = "dempen"; - -/* swipe action */ +/* notification label action */ "Mute" = "Dempen"; +/* notification label action */ +"Mute all" = "Alles dempen"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Gedempt wanneer inactief!"; @@ -3189,12 +3437,15 @@ /* No comment provided by engineer. */ "Network settings" = "Netwerk instellingen"; -/* No comment provided by engineer. */ +/* alert title */ "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"; @@ -3225,6 +3476,9 @@ /* No comment provided by engineer. */ "New member role" = "Nieuwe leden rol"; +/* rcv group event chat item */ +"New member wants to join the group." = "Nieuw lid wil zich bij de groep aansluiten."; + /* notification */ "new message" = "nieuw bericht"; @@ -3255,6 +3509,18 @@ /* 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 chats with members" = "Geen chats met leden"; + /* No comment provided by engineer. */ "No contacts selected" = "Geen contacten geselecteerd"; @@ -3288,6 +3554,9 @@ /* 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."; @@ -3324,12 +3593,24 @@ /* 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. */ +"not synchronized" = "niet gesynchroniseerd"; + +/* No comment provided by engineer. */ +"Notes" = "Notities"; + /* No comment provided by engineer. */ "Nothing selected" = "Niets geselecteerd"; @@ -3342,18 +3623,25 @@ /* 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)"; +"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)"; /* member role */ "observer" = "Waarnemer"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "uit"; /* blur media */ @@ -3365,7 +3653,9 @@ /* feature offered item */ "offered %@: %@" = "voorgesteld %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "OK"; /* No comment provided by engineer. */ @@ -3407,6 +3697,12 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "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 bericht reacties toevoegen."; @@ -3437,13 +3733,13 @@ /* 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. */ +/* new chat action */ "Open chat" = "Chat openen"; /* authentication reason */ @@ -3452,9 +3748,12 @@ /* No comment provided by engineer. */ "Open conditions" = "Open voorwaarden"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Open groep"; +/* alert title */ +"Open link?" = "Link openen?"; + /* authentication reason */ "Open migration to another device" = "Open de migratie naar een ander apparaat"; @@ -3488,6 +3787,9 @@ /* 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"; @@ -3527,9 +3829,6 @@ /* No comment provided by engineer. */ "Password to show" = "Wachtwoord om weer te geven"; -/* past/unknown group member */ -"Past member %@" = "Voormalig lid %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Desktopadres plakken"; @@ -3545,9 +3844,18 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"pending" = "In behandeling"; + /* No comment provided by engineer. */ "Pending" = "in behandeling"; +/* No comment provided by engineer. */ +"pending approval" = "in afwachting van goedkeuring"; + +/* No comment provided by engineer. */ +"pending review" = "in afwachting van beoordeling"; + /* No comment provided by engineer. */ "Periodic" = "Periodiek"; @@ -3578,7 +3886,7 @@ /* 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."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Controleer uw netwerkverbinding met %@ en probeer het opnieuw."; /* No comment provided by engineer. */ @@ -3614,15 +3922,24 @@ /* 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."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Wacht totdat de moderators van de groep uw verzoek tot lidmaatschap van de groep hebben beoordeeld."; + +/* 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"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Bewaar het laatste berichtconcept, met bijlagen."; @@ -3644,12 +3961,21 @@ /* 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"; @@ -3662,7 +3988,7 @@ /* No comment provided by engineer. */ "Private routing" = "Privéroutering"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Fout in privéroutering"; /* No comment provided by engineer. */ @@ -3695,6 +4021,9 @@ /* 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."; @@ -3794,9 +4123,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "bevestiging ontvangen…"; -/* notification */ -"Received file event" = "Ontvangen bestandsgebeurtenis"; - /* message info title */ "Received message" = "Ontvangen bericht"; @@ -3857,16 +4183,32 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Verminderd batterijgebruik"; -/* reject incoming call via notification - swipe action */ +/* No comment provided by engineer. */ +"Register" = "Register"; + +/* token info */ +"Register notification token?" = "Meldingstoken registreren?"; + +/* token status text */ +"Registered" = "Geregistreerd"; + +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Afwijzen"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Contact afwijzen (afzender NIET op de hoogte)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Contactverzoek afwijzen"; +/* alert title */ +"Reject member?" = "Lid afwijzen?"; + +/* No comment provided by engineer. */ +"rejected" = "afgewezen"; + /* call status */ "rejected call" = "geweigerde oproep"; @@ -3903,6 +4245,9 @@ /* profile update event chat item */ "removed contact address" = "contactadres verwijderd"; +/* No comment provided by engineer. */ +"removed from group" = "verwijderd uit de groep"; + /* profile update event chat item */ "removed profile picture" = "profielfoto verwijderd"; @@ -3918,26 +4263,56 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Heronderhandelen over versleuteling?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Verbindingsverzoek herhalen?"; - /* No comment provided by engineer. */ "Repeat download" = "Herhaal het downloaden"; /* No comment provided by engineer. */ "Repeat import" = "Herhaal import"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Deelnameverzoek herhalen?"; - /* No comment provided by engineer. */ "Repeat upload" = "Herhaal het uploaden"; /* chat item action */ "Reply" = "Antwoord"; +/* chat item action */ +"Report" = "rapporteren"; + +/* report reason */ +"Report content: only group moderators will see it." = "Inhoud melden: alleen groepsmoderators kunnen dit zien."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Rapporteer ledenprofiel: alleen groepsmoderators kunnen dit zien."; + +/* report reason */ +"Report other: only group moderators will see it." = "Anders melden: alleen groepsmoderators kunnen het zien."; + +/* No comment provided by engineer. */ +"Report reason?" = "Reden melding?"; + +/* alert title */ +"Report sent to moderators" = "Rapport verzonden naar moderators"; + +/* 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"; + +/* No comment provided by engineer. */ +"request to join rejected" = "verzoek tot toetreding afgewezen"; + /* chat list item title */ -"requested to connect" = "gevraagd om verbinding te maken"; +"requested to connect" = "verzocht om verbinding te maken"; /* No comment provided by engineer. */ "Required" = "Vereist"; @@ -3984,17 +4359,26 @@ /* No comment provided by engineer. */ "Restore database error" = "Database fout herstellen"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Opnieuw proberen"; /* chat item action */ "Reveal" = "Onthullen"; /* No comment provided by engineer. */ -"Review conditions" = "Voorwaarden bekijken"; +"review" = "beoordeling"; /* No comment provided by engineer. */ -"Review later" = "Later beoordelen"; +"Review conditions" = "Voorwaarden bekijken"; + +/* admission stage */ +"Review members" = "Leden beoordelen"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Controleer de leden voordat u ze toelaat ('knocking')."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "beoordeeld door beheerders"; /* No comment provided by engineer. */ "Revoke" = "Intrekken"; @@ -4018,12 +4402,15 @@ "Safer groups" = "Veiligere groepen"; /* alert button - chat item action */ +chat item action */ "Save" = "Opslaan"; /* alert button */ "Save (and notify contacts)" = "Bewaar (en informeer contacten)"; +/* alert title */ +"Save admission settings?" = "Toegangsinstellingen opslaan?"; + /* alert button */ "Save and notify contact" = "Opslaan en Contact melden"; @@ -4040,7 +4427,10 @@ "Save group profile" = "Groep profiel opslaan"; /* No comment provided by engineer. */ -"Save passphrase and open chat" = "Bewaar het wachtwoord en open je chats"; +"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"; @@ -4177,9 +4567,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "Stuur ontvangstbewijzen naar"; -/* No comment provided by engineer. */ -"send direct message" = "stuur een direct bericht"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Stuur een direct bericht om verbinding te maken"; @@ -4207,6 +4594,9 @@ /* No comment provided by engineer. */ "Send notifications" = "Meldingen verzenden"; +/* No comment provided by engineer. */ +"Send private reports" = "Rapporteer privé"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Stuur vragen en ideeën"; @@ -4258,9 +4648,6 @@ /* No comment provided by engineer. */ "Sent directly" = "Direct verzonden"; -/* notification */ -"Sent file event" = "Verzonden bestandsgebeurtenis"; - /* message info title */ "Sent message" = "Verzonden bericht"; @@ -4307,10 +4694,10 @@ "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"; +"Server requires authorization to create queues, check password." = "Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord"; /* server test error */ -"Server requires authorization to upload, check password" = "Server vereist autorisatie om te uploaden, wachtwoord controleren"; +"Server requires authorization to upload, check password." = "Server vereist autorisatie om te uploaden, wachtwoord controleren"; /* No comment provided by engineer. */ "Server test failed!" = "Servertest mislukt!"; @@ -4339,6 +4726,9 @@ /* 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…"; @@ -4351,11 +4741,17 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Stel het in in plaats van systeemverificatie."; +/* No comment provided by engineer. */ +"Set member admission" = "Toegang voor leden instellen"; + +/* 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"; @@ -4382,7 +4778,7 @@ "Shape profile images" = "Vorm profiel afbeeldingen"; /* alert action - chat item action */ +chat item action */ "Share" = "Deel"; /* No comment provided by engineer. */ @@ -4421,6 +4817,9 @@ /* No comment provided by engineer. */ "Share with contacts" = "Delen met contacten"; +/* No comment provided by engineer. */ +"Short link" = "Korte link"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Toon → bij berichten verzonden via privéroutering."; @@ -4463,6 +4862,12 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX adres of eenmalige link?"; +/* alert title */ +"SimpleX address settings" = "Instellingen automatisch accepteren"; + +/* simplex link type */ +"SimpleX channel link" = "SimpleX channel 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."; @@ -4547,6 +4952,10 @@ /* 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."; @@ -4604,6 +5013,9 @@ /* No comment provided by engineer. */ "Stopping chat" = "Chat stoppen"; +/* No comment provided by engineer. */ +"Storage" = "Opslag"; + /* No comment provided by engineer. */ "strike" = "staking"; @@ -4611,7 +5023,7 @@ "Strong" = "Krachtig"; /* No comment provided by engineer. */ -"Submit" = "Indienen"; +"Submit" = "Bevestigen"; /* No comment provided by engineer. */ "Subscribed" = "Subscribed"; @@ -4673,6 +5085,9 @@ /* 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"; @@ -4682,12 +5097,15 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* 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"; @@ -4760,22 +5178,16 @@ /* 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."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Dezelfde voorwaarden gelden voor operator(s): **%@**."; - /* 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! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "De afzender wordt NIET op de hoogte gebracht"; /* No comment provided by engineer. */ @@ -4808,6 +5220,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." = "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 definitief verloren."; @@ -4821,7 +5236,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."; @@ -4833,14 +5248,14 @@ "This group no longer exists." = "Deze groep bestaat niet meer."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Dit is uw eigen eenmalige link!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Dit is uw eigen SimpleX adres!"; +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen."; /* 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 chatprofiel **%@**."; @@ -4907,6 +5322,9 @@ /* 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"; @@ -4919,12 +5337,6 @@ /* 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: %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen."; - /* No comment provided by engineer. */ "Turkish interface" = "Turkse interface"; @@ -5015,10 +5427,7 @@ /* authentication reason */ "Unlock app" = "Ontgrendel app"; -/* No comment provided by engineer. */ -"unmute" = "dempen opheffen"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Dempen opheffen"; /* No comment provided by engineer. */ @@ -5027,6 +5436,9 @@ /* swipe action */ "Unread" = "Ongelezen"; +/* No comment provided by engineer. */ +"Unsupported connection link" = "Niet-ondersteunde verbindingslink"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Er worden maximaal 100 laatste berichten naar nieuwe leden verzonden."; @@ -5042,6 +5454,9 @@ /* No comment provided by engineer. */ "Update settings?" = "Instellingen actualiseren?"; +/* No comment provided by engineer. */ +"Updated conditions" = "Bijgewerkte voorwaarden"; + /* rcv group event chat item */ "updated group profile" = "bijgewerkt groep profiel"; @@ -5081,7 +5496,7 @@ /* No comment provided by engineer. */ "Use chat" = "Gebruik chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Gebruik het huidige profiel"; /* No comment provided by engineer. */ @@ -5099,7 +5514,7 @@ /* No comment provided by engineer. */ "Use iOS call interface" = "De iOS-oproepinterface gebruiken"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Gebruik een nieuw incognitoprofiel"; /* No comment provided by engineer. */ @@ -5123,12 +5538,21 @@ /* 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 TCP port 443 for preset servers only." = "Gebruik TCP-poort 443 alleen voor vooraf ingestelde servers."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Gebruik de app tijdens het gesprek."; /* No comment provided by engineer. */ "Use the app with one hand." = "Gebruik de app met één hand."; +/* No comment provided by engineer. */ +"Use web port" = "Gebruik een webpoort"; + /* No comment provided by engineer. */ "User selection" = "Gebruikersselectie"; @@ -5136,7 +5560,7 @@ "Username" = "Gebruikersnaam"; /* No comment provided by engineer. */ -"Using SimpleX Chat servers." = "SimpleX Chat servers gebruiken."; +"Using SimpleX Chat servers." = "Gebruik SimpleX Chat servers."; /* No comment provided by engineer. */ "v%@" = "v%@"; @@ -5348,6 +5772,9 @@ /* No comment provided by engineer. */ "You accepted connection" = "Je hebt de verbinding geaccepteerd"; +/* snd group event chat item */ +"you accepted this member" = "je hebt dit lid geaccepteerd"; + /* No comment provided by engineer. */ "You allow" = "Jij staat toe"; @@ -5360,33 +5787,24 @@ /* No comment provided by engineer. */ "You are already connected with %@." = "U bent al verbonden met %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "U maakt al verbinding met %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Je maakt al verbinding via deze eenmalige link!"; /* No comment provided by engineer. */ "You are already in group %@." = "Je zit al in groep %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Je bent al lid van de groep %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Je wordt al lid van de groep via deze link!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Je wordt al lid van de groep via deze link."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Je sluit je al aan bij de groep!\nDeelnameverzoek herhalen?"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "U bent verbonden met de server die wordt gebruikt om berichten van dit contact te ontvangen."; - -/* No comment provided by engineer. */ -"you are invited to group" = "je bent uitgenodigd voor de groep"; - /* No comment provided by engineer. */ "You are invited to group" = "Je bent uitgenodigd voor de groep"; @@ -5405,9 +5823,6 @@ /* 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 operators in Network & servers settings." = "U kunt operators configureren in Netwerk- en serverinstellingen."; - /* No comment provided by engineer. */ "You can configure servers via settings." = "U kunt servers configureren via instellingen."; @@ -5462,7 +5877,10 @@ /* 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. */ +/* alert message */ +"You can view your reports in Chat with admins." = "U kunt uw rapporten bekijken in Chat met beheerders."; + +/* alert title */ "You can't send messages!" = "Je kunt geen berichten versturen!"; /* chat item text */ @@ -5483,10 +5901,7 @@ /* 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!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Je hebt al verbinding aangevraagd!\nVerbindingsverzoek herhalen?"; /* No comment provided by engineer. */ @@ -5534,6 +5949,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"; @@ -5552,9 +5970,6 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Je maakt verbinding met alle leden."; - /* 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."; @@ -5592,7 +6007,7 @@ "Your chat profiles" = "Uw chat profielen"; /* No comment provided by engineer. */ -"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."; +"Your connection was moved to %@ but an error happened when switching 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 (%@)."; @@ -5627,15 +6042,15 @@ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Uw profiel **%@** wordt gedeeld."; +/* No comment provided by engineer. */ +"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 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."; - /* No comment provided by engineer. */ "Your random profile" = "Je willekeurige profiel"; @@ -5651,6 +6066,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Uw SimpleX adres"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Uw SMP servers"; - diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index cc3bd228f9..34c79eeef4 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (można skopiować)"; @@ -22,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)"; @@ -82,6 +61,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**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."; @@ -142,6 +124,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ jest zweryfikowany"; +/* No comment provided by engineer. */ +"%@ server" = "%@ serwer"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ serwery/ów"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ wgrane"; @@ -190,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)"; @@ -232,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"; @@ -280,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 dzień"; /* time interval */ @@ -289,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"; @@ -328,6 +328,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Przerwać zmianę adresu?"; +/* No comment provided by engineer. */ +"About operators" = "O operatorach"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "O SimpleX Chat"; @@ -338,35 +341,51 @@ "Accent" = "Akcent"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +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 - swipe action */ +/* alert action +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 friends" = "Dodaj znajomych"; + +/* No comment provided by engineer. */ +"Add list" = "Dodaj listę"; + /* No comment provided by engineer. */ "Add profile" = "Dodaj profil"; @@ -376,12 +395,27 @@ /* 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"; @@ -397,6 +431,12 @@ /* 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"; @@ -421,12 +461,18 @@ /* 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."; @@ -439,6 +485,9 @@ /* 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ąć!"; @@ -451,6 +500,9 @@ /* 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."; @@ -496,6 +548,9 @@ /* 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."; @@ -529,10 +584,10 @@ /* No comment provided by engineer. */ "Already connected?" = "Już połączony?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Już połączony!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Już dołączono do grupy!"; /* pref value */ @@ -550,6 +605,9 @@ /* 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"; @@ -565,6 +623,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"; @@ -592,12 +653,30 @@ /* 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"; @@ -649,9 +728,6 @@ /* 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"; @@ -673,15 +749,30 @@ /* 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"; @@ -709,7 +800,8 @@ /* 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. */ @@ -742,6 +834,15 @@ /* 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)."; @@ -785,7 +886,8 @@ "Can't message member" = "Nie można wysłać wiadomości do członka"; /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Anuluj"; /* No comment provided by engineer. */ @@ -812,6 +914,9 @@ /* 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?"; @@ -837,7 +942,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 */ @@ -855,6 +960,15 @@ /* chat item text */ "changing address…" = "zmiana adresu…"; +/* No comment provided by engineer. */ +"Chat" = "Czat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Czat już istnieje"; + +/* new chat sheet title */ +"Chat already exists!" = "Czat już istnieje!"; + /* No comment provided by engineer. */ "Chat colors" = "Kolory czatu"; @@ -868,10 +982,10 @@ "Chat database deleted" = "Baza danych czatu usunięta"; /* No comment provided by engineer. */ -"Chat database exported" = "Wyeksportowano bazę danych czatu"; +"Chat database exported" = "Wyeksportowano bazę danych czatów"; /* No comment provided by engineer. */ -"Chat database imported" = "Zaimportowano bazę danych czatu"; +"Chat database imported" = "Zaimportowano bazę danych czatów"; /* No comment provided by engineer. */ "Chat is running" = "Czat jest uruchomiony"; @@ -900,9 +1014,21 @@ /* 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."; @@ -963,6 +1089,18 @@ /* 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): **%@**."; + +/* alert button */ +"Conditions of use" = "Warunki użytkowania"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Skonfiguruj serwery ICE"; @@ -1002,9 +1140,6 @@ /* No comment provided by engineer. */ "Connect automatically" = "Łącz automatycznie"; -/* No comment provided by engineer. */ -"Connect incognito" = "Połącz incognito"; - /* No comment provided by engineer. */ "Connect to desktop" = "Połącz do komputera"; @@ -1014,25 +1149,22 @@ /* 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ą?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Połączyć się ze sobą?\nTo jest twój jednorazowy link!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Połączyć się ze sobą?\nTo jest twój własny adres SimpleX!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Połącz przez adres kontaktowy"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Połącz się przez link"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Połącz przez jednorazowy link"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Połącz z %@"; /* No comment provided by engineer. */ @@ -1044,9 +1176,6 @@ /* 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"; @@ -1095,7 +1224,7 @@ /* No comment provided by engineer. */ "Connection and servers status." = "Stan połączenia i serwerów."; -/* No comment provided by engineer. */ +/* alert title */ "Connection error" = "Błąd połączenia"; /* No comment provided by engineer. */ @@ -1113,7 +1242,7 @@ /* No comment provided by engineer. */ "Connection terminated" = "Połączenie zakończone"; -/* No comment provided by engineer. */ +/* alert title */ "Connection timeout" = "Czas połączenia minął"; /* No comment provided by engineer. */ @@ -1215,9 +1344,6 @@ /* server test step */ "Create queue" = "Utwórz kolejkę"; -/* No comment provided by engineer. */ -"Create secret group" = "Utwórz tajną grupę"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Utwórz adres SimpleX"; @@ -1341,7 +1467,8 @@ /* No comment provided by engineer. */ "decryption errors" = "błąd odszyfrowywania"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "domyślne (%@)"; /* No comment provided by engineer. */ @@ -1351,8 +1478,7 @@ "default (yes)" = "domyślnie (tak)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Usuń"; /* No comment provided by engineer. */ @@ -1433,7 +1559,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. */ @@ -1622,14 +1748,14 @@ /* No comment provided by engineer. */ "Don't enable" = "Nie włączaj"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Nie pokazuj ponownie"; /* No comment provided by engineer. */ "Downgrade and open chat" = "Obniż wersję i otwórz czat"; /* alert button - chat item action */ +chat item action */ "Download" = "Pobierz"; /* No comment provided by engineer. */ @@ -1683,7 +1809,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. */ @@ -1863,13 +1989,13 @@ /* No comment provided by engineer. */ "Error changing role" = "Błąd zmiany roli"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert message */ "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. */ @@ -1893,19 +2019,19 @@ /* No comment provided by engineer. */ "Error decrypting file" = "Błąd odszyfrowania pliku"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Błąd usuwania bazy danych czatu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Błąd usuwania czatu!"; /* No comment provided by engineer. */ "Error deleting connection" = "Błąd usuwania połączenia"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Błąd usuwania bazy danych"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Błąd usuwania starej bazy danych"; /* No comment provided by engineer. */ @@ -1926,13 +2052,13 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Błąd szyfrowania bazy danych"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Błąd importu bazy danych czatu"; /* No comment provided by engineer. */ @@ -1953,7 +2079,7 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Błąd ponownego łączenia serwerów"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Błąd usuwania członka"; /* No comment provided by engineer. */ @@ -1998,7 +2124,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Błąd zatrzymania czatu"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Błąd zmiany profilu"; /* alertTitle */ @@ -2028,7 +2154,9 @@ /* No comment provided by engineer. */ "Error: " = "Błąd: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Błąd: %@"; /* No comment provided by engineer. */ @@ -2043,9 +2171,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Nawet po wyłączeniu w rozmowie."; -/* No comment provided by engineer. */ -"event happened" = "nowe wydarzenie"; - /* No comment provided by engineer. */ "Exit without saving" = "Wyjdź bez zapisywania"; @@ -2085,7 +2210,7 @@ /* swipe action */ "Favorite" = "Ulubione"; -/* No comment provided by engineer. */ +/* file error alert title */ "File error" = "Błąd pliku"; /* alert message */ @@ -2148,6 +2273,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "Szybciej znajduj czaty"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy"; + /* No comment provided by engineer. */ "Fix" = "Napraw"; @@ -2196,8 +2324,8 @@ /* 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."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Serwer przekazujący %1$@ nie mógł połączyć się z serwerem docelowym %2$@. 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: %@."; @@ -2247,7 +2375,7 @@ /* No comment provided by engineer. */ "Group already exists" = "Grupa już istnieje"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Grupa już istnieje!"; /* No comment provided by engineer. */ @@ -2502,7 +2630,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Nieprawidłowa nazwa wyświetlana!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Nieprawidłowy link"; /* No comment provided by engineer. */ @@ -2599,24 +2727,18 @@ "Join" = "Dołącz"; /* No comment provided by engineer. */ -"join as %@" = "dołącz jako %@"; +"Join as %@" = "dołącz jako %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Dołącz do grupy"; /* No comment provided by engineer. */ "Join group conversations" = "Dołącz do grupowej rozmowy"; -/* No comment provided by engineer. */ -"Join group?" = "Dołączyć do grupy?"; - /* No comment provided by engineer. */ "Join incognito" = "Dołącz incognito"; -/* No comment provided by engineer. */ -"Join with current profile" = "Dołącz z obecnym profilem"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Dołączyć do twojej grupy?\nTo jest twój link do grupy %@!"; /* No comment provided by engineer. */ @@ -2937,10 +3059,7 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Wiele profili czatu"; -/* No comment provided by engineer. */ -"mute" = "wycisz"; - -/* swipe action */ +/* notification label action */ "Mute" = "Wycisz"; /* No comment provided by engineer. */ @@ -2964,10 +3083,10 @@ /* No comment provided by engineer. */ "Network settings" = "Ustawienia sieci"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "Status sieci"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "nigdy"; /* No comment provided by engineer. */ @@ -3100,8 +3219,9 @@ "observer" = "obserwator"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "wyłączony"; /* blur media */ @@ -3113,7 +3233,9 @@ /* feature offered item */ "offered %@: %@" = "zaoferował %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3182,16 +3304,16 @@ /* 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. */ +/* new chat action */ "Open chat" = "Otwórz czat"; /* authentication reason */ "Open chat console" = "Otwórz konsolę czatu"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Grupa otwarta"; /* authentication reason */ @@ -3254,9 +3376,6 @@ /* No comment provided by engineer. */ "Password to show" = "Hasło do wyświetlenia"; -/* past/unknown group member */ -"Past member %@" = "Były członek %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Wklej adres komputera"; @@ -3305,7 +3424,7 @@ /* 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."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Sprawdzić połączenie sieciowe z %@ i spróbować ponownie."; /* No comment provided by engineer. */ @@ -3347,9 +3466,6 @@ /* 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."; @@ -3383,7 +3499,7 @@ /* No comment provided by engineer. */ "Private routing" = "Prywatne trasowanie"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Błąd prywatnego trasowania"; /* No comment provided by engineer. */ @@ -3515,9 +3631,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "otrzymano potwierdzenie…"; -/* notification */ -"Received file event" = "Otrzymano zdarzenie pliku"; - /* message info title */ "Received message" = "Otrzymano wiadomość"; @@ -3578,14 +3691,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Zmniejszone zużycie baterii"; -/* reject incoming call via notification - swipe action */ +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Odrzuć"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Odrzuć kontakt (nadawca NIE został powiadomiony)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Odrzuć prośbę kontaktu"; /* call status */ @@ -3639,18 +3753,12 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Renegocjować szyfrowanie?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Powtórzyć prośbę połączenia?"; - /* No comment provided by engineer. */ "Repeat download" = "Powtórz pobieranie"; /* No comment provided by engineer. */ "Repeat import" = "Powtórz importowanie"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Powtórzyć prośbę dołączenia?"; - /* No comment provided by engineer. */ "Repeat upload" = "Powtórz wgrywanie"; @@ -3702,7 +3810,7 @@ /* No comment provided by engineer. */ "Restore database error" = "Błąd przywracania bazy danych"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Ponów"; /* chat item action */ @@ -3730,7 +3838,7 @@ "Safer groups" = "Bezpieczniejsze grupy"; /* alert button - chat item action */ +chat item action */ "Save" = "Zapisz"; /* alert button */ @@ -3889,9 +3997,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "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ć"; @@ -3970,9 +4075,6 @@ /* 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ść"; @@ -4007,10 +4109,10 @@ "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"; +"Server requires authorization to create queues, check password." = "Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło"; /* server test error */ -"Server requires authorization to upload, check password" = "Serwer wymaga autoryzacji do przesłania, sprawdź hasło"; +"Server requires authorization to upload, check password." = "Serwer wymaga autoryzacji do przesłania, sprawdź hasło"; /* No comment provided by engineer. */ "Server test failed!" = "Test serwera nie powiódł się!"; @@ -4082,7 +4184,7 @@ "Shape profile images" = "Kształtuj obrazy profilowe"; /* alert action - chat item action */ +chat item action */ "Share" = "Udostępnij"; /* No comment provided by engineer. */ @@ -4148,6 +4250,9 @@ /* No comment provided by engineer. */ "SimpleX Address" = "Adres SimpleX"; +/* alert title */ +"SimpleX address settings" = "Ustawienia automatycznej akceptacji"; + /* 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."; @@ -4349,7 +4454,7 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* file error alert title */ "Temporary file error" = "Tymczasowy błąd pliku"; /* server test failure */ @@ -4421,13 +4526,10 @@ /* 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."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Drugi tik, który przegapiliśmy! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Nadawca NIE zostanie powiadomiony"; /* No comment provided by engineer. */ @@ -4478,12 +4580,6 @@ /* No comment provided by engineer. */ "This group no longer exists." = "Ta grupa już nie istnieje."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "To jest twój jednorazowy link!"; - -/* 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."; @@ -4553,12 +4649,6 @@ /* 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: %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu."; - /* No comment provided by engineer. */ "Turkish interface" = "Turecki interfejs"; @@ -4646,10 +4736,7 @@ /* authentication reason */ "Unlock app" = "Odblokuj aplikację"; -/* No comment provided by engineer. */ -"unmute" = "wyłącz wyciszenie"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Wyłącz wyciszenie"; /* No comment provided by engineer. */ @@ -4709,7 +4796,7 @@ /* No comment provided by engineer. */ "Use chat" = "Użyj czatu"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Użyj obecnego profilu"; /* No comment provided by engineer. */ @@ -4721,7 +4808,7 @@ /* No comment provided by engineer. */ "Use iOS call interface" = "Użyj interfejsu połączeń iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Użyj nowego profilu incognito"; /* No comment provided by engineer. */ @@ -4967,33 +5054,24 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Jesteś już połączony z %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Już się łączysz z %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Już jesteś połączony z tym jednorazowym linkiem!"; /* No comment provided by engineer. */ "You are already in group %@." = "Już jesteś w grupie %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Już dołączasz do grupy %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Już dołączasz do grupy przez ten link!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Już dołączasz do grupy przez ten link."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Już dołączasz do grupy!\nPowtórzyć prośbę dołączenia?"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Jesteś połączony z serwerem używanym do odbierania wiadomości od tego kontaktu."; - -/* No comment provided by engineer. */ -"you are invited to group" = "jesteś zaproszony do grupy"; - /* No comment provided by engineer. */ "You are invited to group" = "Jesteś zaproszony do grupy"; @@ -5060,7 +5138,7 @@ /* alert message */ "You can view invitation link again in connection details." = "Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Nie możesz wysyłać wiadomości!"; /* chat item text */ @@ -5081,10 +5159,7 @@ /* 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!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "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. */ @@ -5150,9 +5225,6 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Uwierzytelnienie będzie wymagane przy uruchamianiu lub wznawianiu aplikacji po 30 sekundach w tle."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Zostaniesz połączony ze wszystkimi członkami grupy."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne."; @@ -5187,7 +5259,7 @@ "Your chat profiles" = "Twoje profile czatu"; /* No comment provided by engineer. */ -"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."; +"Your connection was moved to %@ but an error happened when switching 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 (%@)."; @@ -5222,27 +5294,27 @@ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Twój profil **%@** zostanie udostępniony."; +/* No comment provided by engineer. */ +"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 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."; - /* No comment provided by engineer. */ "Your random profile" = "Twój losowy profil"; /* 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"; /* No comment provided by engineer. */ "Your SimpleX address" = "Twój adres SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Twoje serwery SMP"; - diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index dcd3de19d1..0826bca4a3 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (можно скопировать)"; @@ -22,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)"; @@ -164,7 +143,7 @@ "%@, %@ and %lld members" = "%@, %@ и %lld членов группы"; /* No comment provided by engineer. */ -"%@, %@ and %lld other members connected" = "%@, %@ и %lld других членов соединены"; +"%@, %@ and %lld other members connected" = "установлено соединение с %@, %@ и %lld другими членами группы"; /* copied message info */ "%@:" = "%@:"; @@ -185,7 +164,7 @@ "%d file(s) were not downloaded." = "%d файлов не было загружено."; /* time interval */ -"%d hours" = "%d ч."; +"%d hours" = "%d час."; /* alert title */ "%d messages not forwarded" = "%d сообщений не переслано"; @@ -199,6 +178,9 @@ /* time interval */ "%d sec" = "%d сек"; +/* delete after time */ +"%d seconds(s)" = "%d секунд"; + /* integrity error chat item */ "%d skipped message(s)" = "%d пропущенных сообщение(й)"; @@ -221,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 сообщений заблокировано"; @@ -241,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 секунд"; @@ -289,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0с"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 день"; /* time interval */ @@ -298,12 +278,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 неделю"; +/* delete after time */ +"1 year" = "1 год"; + /* No comment provided by engineer. */ "1-time link" = "Одноразовая ссылка"; @@ -332,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" = "Прекратить"; @@ -356,23 +341,39 @@ "Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Принять"; +/* alert action */ +"Accept as member" = "Принять в группу"; + +/* alert action */ +"Accept as observer" = "Принять как читателя"; + /* No comment provided by engineer. */ "Accept conditions" = "Принять условия"; /* No comment provided by engineer. */ "Accept connection request?" = "Принять запрос?"; +/* alert title */ +"Accept contact request" = "Принять запрос на соединение"; + /* notification body */ "Accept contact request from %@?" = "Принять запрос на соединение от %@?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Принять инкогнито"; +/* alert title */ +"Accept member" = "Принять члена"; + +/* rcv group event chat item */ +"accepted %@" = "принят %@"; + /* call status */ "accepted call" = "принятый звонок"; @@ -382,12 +383,18 @@ /* chat list item title */ "accepted invitation" = "принятое приглашение"; +/* rcv group event chat item */ +"accepted you" = "Вы приняты"; + /* No comment provided by engineer. */ "Acknowledged" = "Подтверждено"; /* No comment provided by engineer. */ "Acknowledgement errors" = "Ошибки подтверждения"; +/* token status text */ +"Active" = "Активный"; + /* No comment provided by engineer. */ "Active connections" = "Активные соединения"; @@ -397,6 +404,12 @@ /* No comment provided by engineer. */ "Add friends" = "Добавить друзей"; +/* No comment provided by engineer. */ +"Add list" = "Добавить список"; + +/* placeholder for sending contact request */ +"Add message" = "Добавить cообщение"; + /* No comment provided by engineer. */ "Add profile" = "Добавить профиль"; @@ -412,6 +425,9 @@ /* 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" = "Добавить приветственное сообщение"; @@ -469,12 +485,21 @@ /* chat item text */ "agreeing encryption…" = "шифрование согласовывается…"; +/* member criteria value */ +"all" = "все"; + +/* 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." = "Все данные удаляются при его вводе."; @@ -482,7 +507,7 @@ "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." = "Все члены группы останутся соединены."; /* feature role */ "all members" = "все члены"; @@ -502,6 +527,12 @@ /* 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." = "Все контакты, которые соединились через этот адрес, сохранятся."; @@ -526,6 +557,9 @@ /* No comment provided by engineer. */ "Allow downgrade" = "Разрешить прямую доставку"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Разрешить файлы и медиа, только если их разрешает Ваш контакт."; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа)"; @@ -536,7 +570,7 @@ "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." = "Разрешить посылать исчезающие сообщения."; @@ -547,6 +581,9 @@ /* 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." = "Разрешить посылать файлы и медиа."; @@ -574,16 +611,19 @@ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Разрешить Вашим контактам отправлять исчезающие сообщения."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Разрешить Вашим контактам отправлять файлы и медиа."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Разрешить Вашим контактам отправлять голосовые сообщения."; /* No comment provided by engineer. */ "Already connected?" = "Соединение уже установлено?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Уже соединяется!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Вступление в группу уже начато!"; /* pref value */ @@ -601,6 +641,9 @@ /* No comment provided by engineer. */ "and %lld other events" = "и %lld других событий"; +/* report reason */ +"Another reason" = "Другая причина"; + /* No comment provided by engineer. */ "Answer call" = "Принять звонок"; @@ -616,6 +659,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" = "Иконка"; @@ -643,15 +689,36 @@ /* 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" = "Подготовка архива"; @@ -700,9 +767,6 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Автоприем изображений"; -/* alert title */ -"Auto-accept settings" = "Настройки автоприема"; - /* No comment provided by engineer. */ "Back" = "Назад"; @@ -730,6 +794,9 @@ /* No comment provided by engineer. */ "Better groups" = "Улучшенные группы"; +/* No comment provided by engineer. */ +"Better groups performance" = "Улучшенная производительность групп"; + /* No comment provided by engineer. */ "Better message dates." = "Улучшенные даты сообщений."; @@ -742,12 +809,21 @@ /* 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. */ +"Bio" = "О себе"; + +/* alert title */ +"Bio too large" = "Описание слишком длинное"; + /* No comment provided by engineer. */ "Black" = "Черная"; @@ -758,13 +834,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?" = "Заблокировать члена группы?"; @@ -775,7 +851,8 @@ /* 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. */ @@ -790,6 +867,9 @@ /* No comment provided by engineer. */ "bold" = "жирный"; +/* No comment provided by engineer. */ +"Bot" = "Бот"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "И Вы, и Ваш контакт можете добавлять реакции на сообщения."; @@ -802,6 +882,9 @@ /* 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 files and media." = "Вы и Ваш контакт можете отправлять файлы и медиа."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Вы и Ваш контакт можете отправлять голосовые сообщения."; @@ -814,9 +897,18 @@ /* No comment provided by engineer. */ "Business chats" = "Бизнес разговоры"; +/* No comment provided by engineer. */ +"Business connection" = "Бизнес контакт"; + +/* 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" = "звонок"; @@ -845,7 +937,10 @@ "Can't call contact" = "Не удается позвонить контакту"; /* No comment provided by engineer. */ -"Can't call member" = "Не удается позвонить члену группы"; +"Can't call member" = "Не удаётся позвонить члену группы"; + +/* alert title */ +"Can't change profile" = "Нельзя поменять профиль"; /* No comment provided by engineer. */ "Can't invite contact!" = "Нельзя пригласить контакт!"; @@ -854,10 +949,14 @@ "Can't invite contacts!" = "Нельзя пригласить контакты!"; /* No comment provided by engineer. */ -"Can't message member" = "Не удается написать члену группы"; +"Can't message member" = "Не удаётся отправить сообщение члену группы"; + +/* No comment provided by engineer. */ +"can't send messages" = "нельзя отправлять"; /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Отменить"; /* No comment provided by engineer. */ @@ -884,6 +983,9 @@ /* No comment provided by engineer. */ "Change" = "Поменять"; +/* alert title */ +"Change automatic message deletion?" = "Измененить автоматическое удаление сообщений?"; + /* authentication reason */ "Change chat profiles" = "Поменять профили"; @@ -912,7 +1014,7 @@ "Change self-destruct mode" = "Изменить режим самоуничтожения"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Изменить код самоуничтожения"; /* chat item text */ @@ -936,7 +1038,7 @@ /* No comment provided by engineer. */ "Chat already exists" = "Разговор уже существует"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Разговор уже существует!"; /* No comment provided by engineer. */ @@ -990,9 +1092,21 @@ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Разговор будет удален для Вас - это действие нельзя отменить!"; +/* chat toolbar */ +"Chat with admins" = "Чат с админами"; + +/* No comment provided by engineer. */ +"Chat with member" = "Чат с членом группы"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Общайтесь с членами до того как принять их."; + /* No comment provided by engineer. */ "Chats" = "Чаты"; +/* No comment provided by engineer. */ +"Chats with members" = "Чаты с членами группы"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Проверять сообщения каждые 20 минут."; @@ -1032,6 +1146,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?" = "Очистить личные заметки?"; @@ -1047,6 +1167,9 @@ /* No comment provided by engineer. */ "colored" = "цвет"; +/* report reason */ +"Community guidelines violation" = "Нарушение правил группы"; + /* server test step */ "Compare file" = "Сравнение файла"; @@ -1068,15 +1191,9 @@ /* No comment provided by engineer. */ "Conditions are already accepted for these operator(s): **%@**." = "Условия уже приняты для следующих оператора(ов): **%@**."; -/* No comment provided by engineer. */ +/* alert button */ "Conditions of use" = "Условия использования"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Условия будут приняты для включенных операторов через 30 дней."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for operator(s): **%@**." = "Условия будут приняты для оператора(ов): **%@**."; - /* No comment provided by engineer. */ "Conditions will be accepted for the operator(s): **%@**." = "Условия будут приняты для оператора(ов): **%@**."; @@ -1089,6 +1206,9 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Настройка ICE серверов"; +/* No comment provided by engineer. */ +"Configure server operators" = "Настроить операторов серверов"; + /* No comment provided by engineer. */ "Confirm" = "Подтвердить"; @@ -1119,6 +1239,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Подтвердить загрузку"; +/* token status text */ +"Confirmed" = "Подтвержденный"; + /* server test step */ "Connect" = "Соединиться"; @@ -1126,7 +1249,7 @@ "Connect automatically" = "Соединяться автоматически"; /* No comment provided by engineer. */ -"Connect incognito" = "Соединиться Инкогнито"; +"Connect faster! 🚀" = "Соединяйтесь быстрее! 🚀"; /* No comment provided by engineer. */ "Connect to desktop" = "Подключиться к компьютеру"; @@ -1137,25 +1260,22 @@ /* No comment provided by engineer. */ "Connect to your friends faster." = "Соединяйтесь с друзьями быстрее."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Соединиться с самим собой?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Соединиться с самим собой?\nЭто ваша собственная одноразовая ссылка!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Соединиться с самим собой?\nЭто ваш собственный адрес SimpleX!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Соединиться через адрес"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Соединиться через ссылку"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Соединиться через одноразовую ссылку"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Соединиться с %@"; /* No comment provided by engineer. */ @@ -1167,9 +1287,6 @@ /* No comment provided by engineer. */ "Connected desktop" = "Подключенный компьютер"; -/* rcv group event chat item */ -"connected directly" = "соединен(а) напрямую"; - /* No comment provided by engineer. */ "Connected servers" = "Подключенные серверы"; @@ -1219,6 +1336,9 @@ "Connection and servers status." = "Состояние соединения и серверов."; /* No comment provided by engineer. */ +"Connection blocked" = "Соединение заблокировано"; + +/* alert title */ "Connection error" = "Ошибка соединения"; /* No comment provided by engineer. */ @@ -1227,19 +1347,28 @@ /* 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. */ +/* alert title */ "Connection timeout" = "Превышено время соединения"; /* No comment provided by engineer. */ @@ -1260,9 +1389,15 @@ /* No comment provided by engineer. */ "Contact already exists" = "Существующий контакт"; +/* No comment provided by engineer. */ +"contact deleted" = "контакт удален"; + /* No comment provided by engineer. */ "Contact deleted!" = "Контакт удален!"; +/* No comment provided by engineer. */ +"contact disabled" = "контакт выключен"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "у контакта есть e2e шифрование"; @@ -1281,9 +1416,18 @@ /* No comment provided by engineer. */ "Contact name" = "Имена контактов"; +/* No comment provided by engineer. */ +"contact not ready" = "контакт не готов"; + /* No comment provided by engineer. */ "Contact preferences" = "Предпочтения контакта"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Запросы на соединение из групп"; + +/* No comment provided by engineer. */ +"contact should accept…" = "контакт должен принять…"; + /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "Контакт будет удален — это нельзя отменить!"; @@ -1293,6 +1437,9 @@ /* 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" = "Продолжить"; @@ -1335,6 +1482,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/). 💻"; @@ -1345,10 +1495,10 @@ "Create queue" = "Создание очереди"; /* No comment provided by engineer. */ -"Create secret group" = "Создать скрытую группу"; +"Create SimpleX address" = "Создать адрес SimpleX"; /* No comment provided by engineer. */ -"Create SimpleX address" = "Создать адрес SimpleX"; +"Create your address" = "Создайте Ваш адрес"; /* No comment provided by engineer. */ "Create your profile" = "Создать профиль"; @@ -1476,7 +1626,8 @@ /* No comment provided by engineer. */ "decryption errors" = "ошибки расшифровки"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "по умолчанию (%@)"; /* No comment provided by engineer. */ @@ -1486,8 +1637,7 @@ "default (yes)" = "по умолчанию (да)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Удалить"; /* No comment provided by engineer. */ @@ -1514,12 +1664,18 @@ /* No comment provided by engineer. */ "Delete chat" = "Удалить разговор"; +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Удалить сообщения с вашего устройства."; + /* No comment provided by engineer. */ "Delete chat profile" = "Удалить профиль чата"; /* No comment provided by engineer. */ "Delete chat profile?" = "Удалить профиль?"; +/* alert title */ +"Delete chat with member?" = "Удалить чат с членом группы?"; + /* No comment provided by engineer. */ "Delete chat?" = "Удалить разговор?"; @@ -1568,13 +1724,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. */ @@ -1598,6 +1757,9 @@ /* 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 сообщений за раз."; @@ -1640,9 +1802,15 @@ /* No comment provided by engineer. */ "Delivery receipts!" = "Отчёты о доставке!"; +/* No comment provided by engineer. */ +"Deprecated options" = "Удалённые настройки"; + /* No comment provided by engineer. */ "Description" = "Описание"; +/* alert title */ +"Description too large" = "Описание слишком длинное"; + /* No comment provided by engineer. */ "Desktop address" = "Адрес компьютера"; @@ -1698,14 +1866,20 @@ "Direct messages" = "Прямые сообщения"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this chat." = "Прямые сообщения между членами запрещены в этом разговоре."; +"Direct messages between members are prohibited in this chat." = "Личные сообщения запрещены в этой группе."; /* No comment provided by engineer. */ -"Direct messages between members are prohibited." = "Прямые сообщения между членами группы запрещены."; +"Direct messages between members are prohibited." = "Прямые сообщения между членами запрещены."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Выключить (кроме исключений)"; +/* alert title */ +"Disable automatic message deletion?" = "Отключить автоматическое удаление сообщений?"; + +/* alert button */ +"Disable delete messages" = "Отключить удаление сообщений"; + /* No comment provided by engineer. */ "Disable for all" = "Выключить для всех"; @@ -1766,6 +1940,9 @@ /* 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" = "Не создавать адрес"; @@ -1773,13 +1950,19 @@ "Don't enable" = "Не включать"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Не пропустите важные сообщения."; + +/* alert action */ "Don't show again" = "Не показывать"; +/* No comment provided by engineer. */ +"Done" = "Готово"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Откатить версию и открыть чат"; /* alert button - chat item action */ +chat item action */ "Download" = "Загрузить"; /* No comment provided by engineer. */ @@ -1830,20 +2013,26 @@ /* No comment provided by engineer. */ "Edit group profile" = "Редактировать профиль группы"; +/* No comment provided by engineer. */ +"Empty message!" = "Пустое сообщение!"; + /* No comment provided by engineer. */ "Enable" = "Включить"; /* 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" = "Включить Flux"; +"Enable disappearing messages by default." = "Включите исчезающие сообщения по умолчанию."; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных."; /* No comment provided by engineer. */ "Enable for all" = "Включить для всех"; @@ -1956,6 +2145,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "требуется новое соглашение о шифровании для %@"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Выполняется повторное согласование шифрования."; + /* No comment provided by engineer. */ "ended" = "завершён"; @@ -2010,28 +2202,40 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Ошибка при принятии запроса на соединение"; +/* alert title */ +"Error accepting member" = "Ошибка вступления члена группы"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Ошибка при добавлении членов группы"; /* alert title */ "Error adding server" = "Ошибка добавления сервера"; +/* No comment provided by engineer. */ +"Error adding short link" = "Ошибка создания короткой ссылки"; + /* No comment provided by engineer. */ "Error changing address" = "Ошибка при изменении адреса"; +/* alert title */ +"Error changing chat profile" = "Ошибка изменения профиля"; + /* No comment provided by engineer. */ "Error changing connection profile" = "Ошибка при изменении профиля соединения"; /* No comment provided by engineer. */ "Error changing role" = "Ошибка при изменении роли"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Ошибка при изменении настройки"; /* No comment provided by engineer. */ "Error changing to incognito!" = "Ошибка при смене на Инкогнито!"; /* No comment provided by engineer. */ +"Error checking token status" = "Ошибка проверки статуса токена"; + +/* alert message */ "Error connecting to forwarding server %@. Please try later." = "Ошибка подключения к пересылающему серверу %@. Попробуйте позже."; /* No comment provided by engineer. */ @@ -2043,8 +2247,11 @@ /* No comment provided by engineer. */ "Error creating group link" = "Ошибка при создании ссылки группы"; +/* alert title */ +"Error creating list" = "Ошибка создания списка"; + /* No comment provided by engineer. */ -"Error creating member contact" = "Ошибка создания контакта с членом группы"; +"Error creating member contact" = "Ошибка при создании контакта"; /* No comment provided by engineer. */ "Error creating message" = "Ошибка создания сообщения"; @@ -2052,22 +2259,28 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Ошибка создания профиля!"; +/* No comment provided by engineer. */ +"Error creating report" = "Ошибка создания сообщения о нарушении"; + /* No comment provided by engineer. */ "Error decrypting file" = "Ошибка расшифровки файла"; -/* No comment provided by engineer. */ +/* alert title */ +"Error deleting chat" = "Ошибка при удалении чата с членом группы"; + +/* alert title */ "Error deleting chat database" = "Ошибка при удалении данных чата"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Ошибка при удалении чата!"; /* No comment provided by engineer. */ "Error deleting connection" = "Ошибка при удалении соединения"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Ошибка при удалении данных чата"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Ошибка при удалении предыдущей версии данных чата"; /* No comment provided by engineer. */ @@ -2088,13 +2301,13 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Ошибка при шифровании"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Ошибка при экспорте архива чата"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "Ошибка экспорта темы: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Ошибка при импорте архива чата"; /* No comment provided by engineer. */ @@ -2107,7 +2320,10 @@ "Error migrating settings" = "Ошибка миграции настроек"; /* No comment provided by engineer. */ -"Error opening chat" = "Ошибка доступа к чату"; +"Error opening chat" = "Ошибка при открытии чата"; + +/* No comment provided by engineer. */ +"Error opening group" = "Ошибка при открытии группы"; /* alert title */ "Error receiving file" = "Ошибка при получении файла"; @@ -2118,12 +2334,24 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Ошибка переподключения к серверам"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "Ошибка регистрации для уведомлений"; + +/* alert title */ +"Error rejecting contact request" = "Ошибка отклонения запроса"; + +/* alert title */ "Error removing member" = "Ошибка при удалении члена группы"; +/* 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" = "Ошибка при сохранении профиля группы"; @@ -2152,11 +2380,14 @@ "Error sending email" = "Ошибка отправки email"; /* No comment provided by engineer. */ -"Error sending member contact invitation" = "Ошибка отправки приглашения члену группы"; +"Error sending member contact invitation" = "Ошибка при отправке приглашения члену"; /* No comment provided by engineer. */ "Error sending message" = "Ошибка при отправке сообщения"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Ошибка при установке автоприёма запросов"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Ошибка настроек отчётов о доставке!"; @@ -2166,7 +2397,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Ошибка при остановке чата"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Ошибка переключения профиля"; /* alertTitle */ @@ -2175,6 +2406,9 @@ /* 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" = "Ошибка обновления ссылки группы"; @@ -2199,9 +2433,14 @@ /* No comment provided by engineer. */ "Error: " = "Ошибка: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Ошибка: %@"; +/* server test error */ +"Error: %@." = "Ошибка: %@."; + /* No comment provided by engineer. */ "Error: no database file" = "Ошибка: данные чата не найдены"; @@ -2217,9 +2456,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" = "Выйти без сохранения"; @@ -2229,6 +2465,9 @@ /* No comment provided by engineer. */ "expired" = "истекло"; +/* token status text */ +"Expired" = "Истекший"; + /* No comment provided by engineer. */ "Export database" = "Экспорт архива чата"; @@ -2253,18 +2492,30 @@ /* 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." = "Файл не найден - скорее всего, файл был удален или отменен."; @@ -2298,6 +2549,9 @@ /* chat feature */ "Files and media" = "Файлы и медиа"; +/* No comment provided by engineer. */ +"Files and media are prohibited in this chat." = "Файлы и медиа запрещены в этом чате."; + /* No comment provided by engineer. */ "Files and media are prohibited." = "Файлы и медиа запрещены в этой группе."; @@ -2322,6 +2576,18 @@ /* No comment provided by engineer. */ "Find chats faster" = "Быстро найти чаты"; +/* No comment provided by engineer. */ +"Fingerprint in destination server address does not match certificate: %@." = "Хэш в адресе сервера назначения не соответствует сертификату: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in forwarding server address does not match certificate: %@." = "Хэш в адресе пересылающего сервера не соответствует сертификату: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in server address does not match certificate: %@." = "Хэш в адресе сервера не соответствует сертификату: %@."; + +/* server test error */ +"Fingerprint in server address does not match certificate." = "Возможно, хэш сертификата в адресе сервера неверный."; + /* No comment provided by engineer. */ "Fix" = "Починить"; @@ -2338,10 +2604,10 @@ "Fix not supported by contact" = "Починка не поддерживается контактом"; /* No comment provided by engineer. */ -"Fix not supported by group member" = "Починка не поддерживается членом группы"; +"Fix not supported by group member" = "Починка не поддерживается членом группы."; /* No comment provided by engineer. */ -"for better metadata privacy." = "для лучшей конфиденциальности метаданных."; +"For all moderators" = "Для всех модераторов"; /* servers error */ "For chat profile %@:" = "Для профиля чата %@:"; @@ -2352,6 +2618,9 @@ /* 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" = "Для доставки сообщений"; @@ -2388,8 +2657,8 @@ /* 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." = "Пересылающий сервер %@ не смог подключиться к серверу назначения %@. Попробуйте позже."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Пересылающий сервер %1$@ не смог подключиться к серверу назначения %2$@. Попробуйте позже."; /* No comment provided by engineer. */ "Forwarding server address is incompatible with network settings: %@." = "Адрес пересылающего сервера несовместим с настройками сети: %@."; @@ -2424,6 +2693,9 @@ /* 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" = "ГИФ файлы и стикеры"; @@ -2433,13 +2705,16 @@ /* message preview */ "Good morning!" = "Доброе утро!"; +/* shown on group welcome message */ +"group" = "группа"; + /* No comment provided by engineer. */ "Group" = "Группа"; /* No comment provided by engineer. */ "Group already exists" = "Группа уже существует"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Группа уже существует!"; /* No comment provided by engineer. */ @@ -2463,6 +2738,9 @@ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Приглашение в группу больше не действительно, оно было удалено отправителем."; +/* No comment provided by engineer. */ +"group is deleted" = "группа удалена"; + /* No comment provided by engineer. */ "Group link" = "Ссылка группы"; @@ -2487,6 +2765,9 @@ /* snd group event chat item */ "group profile updated" = "профиль группы обновлен"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "Профиль группы изменен. Если Вы сохраните его, новый профиль будет отправлен членам группы."; + /* No comment provided by engineer. */ "Group welcome message" = "Приветственное сообщение группы"; @@ -2496,9 +2777,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" = "Скрытое"; @@ -2535,6 +2822,9 @@ /* No comment provided by engineer. */ "How it helps privacy" = "Как это улучшает конфиденциальность"; +/* alert button */ +"How it works" = "Как это работает"; + /* No comment provided by engineer. */ "How SimpleX works" = "Как SimpleX работает"; @@ -2622,6 +2912,12 @@ /* No comment provided by engineer. */ "inactive" = "неактивен"; +/* report reason */ +"Inappropriate content" = "Неприемлемый контент"; + +/* report reason */ +"Inappropriate profile" = "Неприемлемый профиль"; + /* No comment provided by engineer. */ "Incognito" = "Инкогнито"; @@ -2688,6 +2984,21 @@ /* No comment provided by engineer. */ "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" = "ошибка чата"; @@ -2703,7 +3014,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Ошибка имени!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Ошибка ссылки"; /* No comment provided by engineer. */ @@ -2803,24 +3114,18 @@ "Join" = "Вступить"; /* No comment provided by engineer. */ -"join as %@" = "вступить как %@"; +"Join as %@" = "вступить как %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Вступить в группу"; /* No comment provided by engineer. */ "Join group conversations" = "Присоединяйтесь к разговорам в группах"; -/* No comment provided by engineer. */ -"Join group?" = "Вступить в группу?"; - /* No comment provided by engineer. */ "Join incognito" = "Вступить инкогнито"; -/* No comment provided by engineer. */ -"Join with current profile" = "Вступить с активным профилем"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Вступить в вашу группу?\nЭто ваша ссылка на группу %@!"; /* No comment provided by engineer. */ @@ -2838,6 +3143,9 @@ /* alert title */ "Keep unused invitation?" = "Оставить неиспользованное приглашение?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "Очищайте Ваши чаты"; + /* No comment provided by engineer. */ "Keep your connections" = "Сохраните Ваши соединения"; @@ -2871,6 +3179,9 @@ /* rcv group event chat item */ "left" = "покинул(а) группу"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Меньше трафик в мобильных сетях."; + /* email subject */ "Let's talk in SimpleX Chat" = "Давайте поговорим в SimpleX Chat"; @@ -2889,6 +3200,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"; @@ -2898,6 +3218,9 @@ /* No comment provided by engineer. */ "Live messages" = "\"Живые\" сообщения"; +/* in progress text */ +"Loading profile…" = "Загрузка профиля…"; + /* No comment provided by engineer. */ "Local name" = "Локальное имя"; @@ -2949,23 +3272,38 @@ /* No comment provided by engineer. */ "Member" = "Член группы"; +/* past/unknown group member */ +"Member %@" = "Член группы %@"; + /* profile update event chat item */ "member %@ changed to %@" = "член %1$@ изменился на %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Приём членов в группу"; + /* rcv group event chat item */ "member connected" = "соединен(а)"; +/* No comment provided by engineer. */ +"member has old version" = "член имеет старую версию"; + /* item status text */ "Member inactive" = "Член неактивен"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Член группы удалён - невозможно принять запрос"; + +/* 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." = "Роль члена группы будет изменена на \"%@\". Все члены группы получат сообщение."; +"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." = "Роль члена группы будет изменена на \"%@\". Будет отправлено новое приглашение."; +"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!" = "Член будет удален из разговора - это действие нельзя отменить!"; @@ -2973,26 +3311,35 @@ /* 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." = "Члены группы могут добавлять реакции на сообщения."; +/* alert message */ +"Member will join the group, accept member?" = "Участник хочет присоединиться к группе. Принять?"; /* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Члены группы могут необратимо удалять отправленные сообщения. (24 часа)"; +"Members can add message reactions." = "Члены могут добавлять реакции на сообщения."; /* No comment provided by engineer. */ -"Members can send direct messages." = "Члены группы могут посылать прямые сообщения."; +"Members can irreversibly delete sent messages. (24 hours)" = "Члены могут необратимо удалять отправленные сообщения. (24 часа)"; /* No comment provided by engineer. */ -"Members can send disappearing messages." = "Члены группы могут посылать исчезающие сообщения."; +"Members can report messsages to moderators." = "Члены группы могут пожаловаться модераторам."; /* No comment provided by engineer. */ -"Members can send files and media." = "Члены группы могут слать файлы и медиа."; +"Members can send direct messages." = "Члены могут посылать прямые сообщения."; /* No comment provided by engineer. */ -"Members can send SimpleX links." = "Члены группы могут отправлять ссылки SimpleX."; +"Members can send disappearing messages." = "Члены могут посылать исчезающие сообщения."; /* No comment provided by engineer. */ -"Members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; +"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" = "Меню"; @@ -3015,6 +3362,9 @@ /* item status text */ "Message forwarded" = "Сообщение переслано"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Отправляйте сообщения сразу после соединения."; + /* item status description */ "Message may be delivered later if member becomes active." = "Сообщение может быть доставлено позже, если член группы станет активным."; @@ -3063,9 +3413,15 @@ /* No comment provided by engineer. */ "Messages & files" = "Сообщения"; +/* No comment provided by engineer. */ +"Messages are protected by **end-to-end encryption**." = "Сообщения защищены **end-to-end шифрованием**."; + /* 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" = "Получено сообщений"; @@ -3138,9 +3494,15 @@ /* 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!" = "Дополнительные улучшения скоро!"; @@ -3156,12 +3518,12 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Много профилей чата"; -/* No comment provided by engineer. */ -"mute" = "без звука"; - -/* swipe action */ +/* notification label action */ "Mute" = "Без звука"; +/* notification label action */ +"Mute all" = "Все без звука"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Без звука, когда не активный!"; @@ -3169,7 +3531,7 @@ "Name" = "Имя"; /* No comment provided by engineer. */ -"Network & servers" = "Сеть & серверы"; +"Network & servers" = "Сеть и серверы"; /* No comment provided by engineer. */ "Network connection" = "Интернет-соединение"; @@ -3189,12 +3551,15 @@ /* No comment provided by engineer. */ "Network settings" = "Настройки сети"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "Состояние сети"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "никогда"; +/* token status text */ +"New" = "Новый"; + /* No comment provided by engineer. */ "New chat" = "Новый чат"; @@ -3216,6 +3581,9 @@ /* notification */ "New events" = "Новые события"; +/* No comment provided by engineer. */ +"New group role: Moderator" = "Новая роль в группах: Модератор"; + /* No comment provided by engineer. */ "New in %@" = "Новое в %@"; @@ -3225,6 +3593,9 @@ /* No comment provided by engineer. */ "New member role" = "Роль члена группы"; +/* rcv group event chat item */ +"New member wants to join the group." = "Новый участник хочет присоединиться к группе."; + /* notification */ "new message" = "новое сообщение"; @@ -3255,6 +3626,18 @@ /* 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 chats with members" = "Нет чатов с членами группы"; + /* No comment provided by engineer. */ "No contacts selected" = "Контакты не выбраны"; @@ -3288,6 +3671,9 @@ /* servers error */ "No media & file servers." = "Нет серверов файлов и медиа."; +/* No comment provided by engineer. */ +"No message" = "Нет сообщения"; + /* servers error */ "No message servers." = "Нет серверов сообщений."; @@ -3303,6 +3689,9 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Нет разрешения для записи голосового сообщения"; +/* alert title */ +"No private routing session" = "Нет сессии конфиденциальной доставки"; + /* No comment provided by engineer. */ "No push server" = "Без сервера нотификаций"; @@ -3324,12 +3713,24 @@ /* 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. */ +"not synchronized" = "не синхронизирован"; + +/* No comment provided by engineer. */ +"Notes" = "Заметки"; + /* No comment provided by engineer. */ "Nothing selected" = "Ничего не выбрано"; @@ -3342,18 +3743,25 @@ /* 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- приостанавливать членов (роль \"наблюдатель\")"; +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Теперь админы могут:\n- удалять сообщения членов.\n- приостанавливать членов (роль наблюдатель)"; /* member role */ "observer" = "читатель"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "нет"; /* blur media */ @@ -3365,7 +3773,9 @@ /* feature offered item */ "offered %@: %@" = "предложил(a) %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ок"; /* No comment provided by engineer. */ @@ -3407,6 +3817,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." = "Только Вы можете добавлять реакции на сообщения."; @@ -3419,6 +3835,9 @@ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "Только Вы можете отправлять исчезающие сообщения."; +/* No comment provided by engineer. */ +"Only you can send files and media." = "Только Вы можете отправлять файлы и медиа."; + /* No comment provided by engineer. */ "Only you can send voice messages." = "Только Вы можете отправлять голосовые сообщения."; @@ -3435,32 +3854,62 @@ "Only your contact can send disappearing messages." = "Только Ваш контакт может отправлять исчезающие сообщения."; /* No comment provided by engineer. */ -"Only your contact can send voice messages." = "Только Ваш контакт может отправлять голосовые сообщения."; +"Only your contact can send files and media." = "Только Ваш контакт может отправлять файлы и медиа."; /* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Только Ваш контакт может отправлять голосовые сообщения."; + +/* alert action */ "Open" = "Открыть"; /* No comment provided by engineer. */ "Open changes" = "Открыть изменения"; -/* No comment provided by engineer. */ +/* new chat action */ "Open chat" = "Открыть чат"; /* authentication reason */ "Open chat console" = "Открыть консоль"; +/* alert action */ +"Open clean link" = "Открыть очищенную ссылку"; + /* No comment provided by engineer. */ "Open conditions" = "Открыть условия"; -/* No comment provided by engineer. */ +/* alert action */ +"Open full link" = "Открыть полную ссылку"; + +/* new chat action */ "Open group" = "Открыть группу"; +/* alert title */ +"Open link?" = "Открыть ссылку?"; + /* authentication reason */ "Open migration to another device" = "Открытие миграции на другое устройство"; +/* new chat action */ +"Open new chat" = "Открыть новый чат"; + +/* new chat action */ +"Open new group" = "Открыть новую группу"; + /* No comment provided by engineer. */ "Open Settings" = "Открыть Настройки"; +/* No comment provided by engineer. */ +"Open to accept" = "Откройте чтобы принять"; + +/* No comment provided by engineer. */ +"Open to connect" = "Откройте чтобы соединиться"; + +/* No comment provided by engineer. */ +"Open to join" = "Откройте чтобы вступить"; + +/* No comment provided by engineer. */ +"Open to use bot" = "Откройте чтобы использовать бот"; + /* No comment provided by engineer. */ "Opening app…" = "Приложение отрывается…"; @@ -3488,6 +3937,9 @@ /* No comment provided by engineer. */ "Or to share privately" = "Или поделиться конфиденциально"; +/* No comment provided by engineer. */ +"Organize chats into lists" = "Организуйте чаты в списки"; + /* No comment provided by engineer. */ "other" = "другое"; @@ -3527,9 +3979,6 @@ /* No comment provided by engineer. */ "Password to show" = "Пароль чтобы раскрыть"; -/* past/unknown group member */ -"Past member %@" = "Бывший член %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Вставить адрес компьютера"; @@ -3546,7 +3995,16 @@ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"Pending" = "В ожидании"; +"pending" = "ожидает"; + +/* No comment provided by engineer. */ +"Pending" = "Ожидает"; + +/* No comment provided by engineer. */ +"pending approval" = "ожидает утверждения"; + +/* No comment provided by engineer. */ +"pending review" = "ожидает одобрения"; /* No comment provided by engineer. */ "Periodic" = "Периодически"; @@ -3578,7 +4036,7 @@ /* 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. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Пожалуйста, проверьте Ваше соединение с %@ и попробуйте еще раз."; /* No comment provided by engineer. */ @@ -3614,15 +4072,24 @@ /* 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." = "Попробуйте выключить и снова включить уведомления."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление."; + +/* 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" = "Возможно, хэш сертификата в адресе сервера неверный"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Сохранить последний черновик, вместе с вложениями."; @@ -3644,12 +4111,21 @@ /* 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" = "Конфиденциальная доставка сообщений"; @@ -3662,9 +4138,12 @@ /* No comment provided by engineer. */ "Private routing" = "Конфиденциальная доставка"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Ошибка конфиденциальной доставки"; +/* alert title */ +"Private routing timeout" = "Таймаут конфиденциальной доставки"; + /* No comment provided by engineer. */ "Profile and server connections" = "Профиль и соединения на сервере"; @@ -3695,6 +4174,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Запретить реакции на сообщения."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Запретить жаловаться модераторам группы."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Запретить посылать прямые сообщения членам группы."; @@ -3722,6 +4204,9 @@ /* 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 background timeout" = "Фоновый таймаут протокола"; + /* No comment provided by engineer. */ "Protocol timeout" = "Таймаут протокола"; @@ -3794,9 +4279,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "получено подтверждение…"; -/* notification */ -"Received file event" = "Загрузка файла"; - /* message info title */ "Received message" = "Полученное сообщение"; @@ -3857,16 +4339,32 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Уменьшенное потребление батареи"; -/* reject incoming call via notification - swipe action */ +/* No comment provided by engineer. */ +"Register" = "Зарегистрировать"; + +/* token info */ +"Register notification token?" = "Зарегистрировать токен уведомлений?"; + +/* token status text */ +"Registered" = "Зарегистрирован"; + +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Отклонить"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Отклонить (не уведомляя отправителя)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Отклонить запрос"; +/* alert title */ +"Reject member?" = "Отклонить участника?"; + +/* No comment provided by engineer. */ +"rejected" = "отклонён"; + /* call status */ "rejected call" = "отклонённый звонок"; @@ -3885,6 +4383,9 @@ /* No comment provided by engineer. */ "Remove image" = "Удалить изображение"; +/* No comment provided by engineer. */ +"Remove link tracking" = "Удалять параметры отслеживания"; + /* No comment provided by engineer. */ "Remove member" = "Удалить члена группы"; @@ -3903,12 +4404,18 @@ /* profile update event chat item */ "removed contact address" = "удалён адрес контакта"; +/* No comment provided by engineer. */ +"removed from group" = "удален из группы"; + /* profile update event chat item */ "removed profile picture" = "удалена картинка профиля"; /* rcv group event chat item */ "removed you" = "удалил(а) Вас из группы"; +/* No comment provided by engineer. */ +"Removes messages and blocks members." = "Может удалять сообщения и блокировать членов."; + /* No comment provided by engineer. */ "Renegotiate" = "Пересогласовать"; @@ -3918,24 +4425,63 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Пересогласовать шифрование?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Повторить запрос на соединение?"; - /* No comment provided by engineer. */ "Repeat download" = "Повторить загрузку"; /* No comment provided by engineer. */ "Repeat import" = "Повторить импорт"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Повторить запрос на вступление?"; - /* No comment provided by engineer. */ "Repeat upload" = "Повторить загрузку"; /* chat item action */ "Reply" = "Ответить"; +/* chat 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?" = "Причина сообщения?"; + +/* alert title */ +"Report sent to moderators" = "Жалоба отправлена модераторам"; + +/* 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" = "Сообщения о нарушениях"; + +/* No comment provided by engineer. */ +"request is sent" = "запрос отправлен"; + +/* No comment provided by engineer. */ +"request to join rejected" = "запрос на вступление отклонён"; + +/* rcv group event chat item */ +"requested connection" = "запрос на соединение"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "запрос на соединение из группы %@"; + /* chat list item title */ "requested to connect" = "запрошено соединение"; @@ -3984,17 +4530,29 @@ /* No comment provided by engineer. */ "Restore database error" = "Ошибка при восстановлении базы данных"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Повторить"; /* chat item action */ "Reveal" = "Показать"; +/* No comment provided by engineer. */ +"review" = "рассмотрение"; + /* No comment provided by engineer. */ "Review conditions" = "Посмотреть условия"; /* No comment provided by engineer. */ -"Review later" = "Посмотреть позже"; +"Review group members" = "Одобрять членов группы"; + +/* admission stage */ +"Review members" = "Одобрять членов"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Вручную одобрять членов для вступления в группу."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "одобрен админами"; /* No comment provided by engineer. */ "Revoke" = "Отозвать"; @@ -4018,12 +4576,18 @@ "Safer groups" = "Более безопасные группы"; /* alert button - chat item action */ +chat item action */ "Save" = "Сохранить"; /* alert button */ "Save (and notify contacts)" = "Сохранить (и уведомить контакты)"; +/* alert button */ +"Save (and notify members)" = "Сохранить (и уведомить членов)"; + +/* alert title */ +"Save admission settings?" = "Сохранить настройки вступления?"; + /* alert button */ "Save and notify contact" = "Сохранить и уведомить контакт"; @@ -4039,6 +4603,12 @@ /* No comment provided by engineer. */ "Save group profile" = "Сохранить профиль группы"; +/* alert title */ +"Save group profile?" = "Сохранить профиль группы?"; + +/* No comment provided by engineer. */ +"Save list" = "Сохранить список"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Сохранить пароль и открыть чат"; @@ -4175,10 +4745,10 @@ "Send a live message - it will update for the recipient(s) as you type it" = "Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите"; /* No comment provided by engineer. */ -"Send delivery receipts to" = "Отправка отчётов о доставке"; +"Send contact request?" = "Отправить запрос на соединение?"; /* No comment provided by engineer. */ -"send direct message" = "отправьте сообщение"; +"Send delivery receipts to" = "Отправка отчётов о доставке"; /* No comment provided by engineer. */ "Send direct message to connect" = "Отправьте сообщение чтобы соединиться"; @@ -4207,11 +4777,20 @@ /* No comment provided by engineer. */ "Send notifications" = "Отправлять уведомления"; +/* No comment provided by engineer. */ +"Send private reports" = "Вы можете сообщить о нарушениях"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Отправьте вопросы и идеи"; /* No comment provided by engineer. */ -"Send receipts" = "Отправлять отчёты о доставке"; +"Send receipts" = "Отчёты о доставке"; + +/* No comment provided by engineer. */ +"Send request" = "Отправить запрос"; + +/* No comment provided by engineer. */ +"Send request without message" = "Отправить запрос без сообщения"; /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур."; @@ -4219,6 +4798,9 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Отправить до 100 последних сообщений новым членам."; +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Отправляйте Ваши конфиденциальные предложения группе."; + /* alert message */ "Sender cancelled file transfer." = "Отправитель отменил передачу файла."; @@ -4258,9 +4840,6 @@ /* No comment provided by engineer. */ "Sent directly" = "Отправлено напрямую"; -/* notification */ -"Sent file event" = "Отправка файла"; - /* message info title */ "Sent message" = "Отправленное сообщение"; @@ -4307,10 +4886,10 @@ "server queue info: %@\n\nlast received msg: %@" = "информация сервера об очереди: %1$@\n\nпоследнее полученное сообщение: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Сервер требует авторизации для создания очередей, проверьте пароль"; +"Server requires authorization to create queues, check password." = "Сервер требует авторизации для создания очередей, проверьте пароль."; /* server test error */ -"Server requires authorization to upload, check password" = "Сервер требует авторизации для загрузки, проверьте пароль"; +"Server requires authorization to upload, check password." = "Сервер требует авторизации для загрузки, проверьте пароль."; /* No comment provided by engineer. */ "Server test failed!" = "Ошибка теста сервера!"; @@ -4339,6 +4918,9 @@ /* 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…" = "Имя контакта…"; @@ -4351,6 +4933,12 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Установите код вместо системной аутентификации."; +/* No comment provided by engineer. */ +"Set member admission" = "Приём членов в группу"; + +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Установите срок хранения сообщений в чатах."; + /* profile update event chat item */ "set new contact address" = "установлен новый адрес контакта"; @@ -4366,6 +4954,9 @@ /* No comment provided by engineer. */ "Set passphrase to export" = "Установите пароль"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Добавьте описание и приветственное сообщение."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "Установить сообщение для новых членов группы!"; @@ -4382,7 +4973,7 @@ "Shape profile images" = "Форма картинок профилей"; /* alert action - chat item action */ +chat item action */ "Share" = "Поделиться"; /* No comment provided by engineer. */ @@ -4406,6 +4997,12 @@ /* No comment provided by engineer. */ "Share link" = "Поделиться ссылкой"; +/* alert button */ +"Share old address" = "Поделиться старым адресом"; + +/* alert button */ +"Share old link" = "Поделиться старой ссылкой"; + /* No comment provided by engineer. */ "Share profile" = "Поделиться профилем"; @@ -4421,6 +5018,18 @@ /* No comment provided by engineer. */ "Share with contacts" = "Поделиться с контактами"; +/* No comment provided by engineer. */ +"Share your address" = "Поделитесь Вашим адресом"; + +/* No comment provided by engineer. */ +"Short description" = "Цель"; + +/* No comment provided by engineer. */ +"Short link" = "Короткая ссылка"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Короткий адрес SimpleX"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Показать → на сообщениях доставленных конфиденциально."; @@ -4463,6 +5072,12 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "Адрес SimpleX или одноразовая ссылка?"; +/* alert title */ +"SimpleX address settings" = "Настройки автоприема"; + +/* 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 в приложение."; @@ -4505,6 +5120,9 @@ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Аудит SimpleX протоколов от Trail of Bits."; +/* simplex link type */ +"SimpleX relay link" = "Ссылка SimpleX relay"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Упрощенный режим Инкогнито"; @@ -4547,6 +5165,10 @@ /* notification title */ "Somebody" = "Контакт"; +/* blocking reason +report reason */ +"Spam" = "Спам"; + /* No comment provided by engineer. */ "Square, circle, or anything in between." = "Квадрат, круг и все, что между ними."; @@ -4604,6 +5226,9 @@ /* No comment provided by engineer. */ "Stopping chat" = "Остановка чата"; +/* No comment provided by engineer. */ +"Storage" = "Хранилище"; + /* No comment provided by engineer. */ "strike" = "зачеркнуть"; @@ -4646,9 +5271,21 @@ /* No comment provided by engineer. */ "Tap button " = "Нажмите кнопку "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "Нажмите Соединиться"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "Нажмите Соединиться, чтобы отправить запрос"; + +/* No comment provided by engineer. */ +"Tap Connect to use bot" = "Нажмите Соединиться, чтобы использовать бот"; + /* No comment provided by engineer. */ "Tap Create SimpleX address in the menu to create it later." = "Нажмите Создать адрес SimpleX в меню, чтобы создать его позже."; +/* No comment provided by engineer. */ +"Tap Join group" = "Нажмите Вступить в группу"; + /* No comment provided by engineer. */ "Tap to activate profile." = "Нажмите, чтобы сделать профиль активным."; @@ -4670,9 +5307,15 @@ /* No comment provided by engineer. */ "TCP connection" = "TCP-соединение"; +/* No comment provided by engineer. */ +"TCP connection bg timeout" = "Фоновый таймаут 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"; @@ -4682,12 +5325,15 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* 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" = "Тестировать сервер"; @@ -4706,6 +5352,9 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Благодаря пользователям – добавьте переводы через Weblate!"; +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "Адрес будет коротким, и Ваш профиль будет добавлен в адрес."; + /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; @@ -4745,6 +5394,9 @@ /* 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Это может произойти из-за ошибки программы, или когда соединение компроментировано."; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "Ссылка будет короткой, и профиль группы будет добавлен в ссылку."; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "Сообщение будет удалено для всех членов группы."; @@ -4760,22 +5412,16 @@ /* 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 same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**."; -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Те же самые условия будут приняты для оператора(ов): **%@**."; - /* No comment provided by engineer. */ "The second preset operator in the app!" = "Второй оператор серверов в приложении!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Вторая галочка - знать, что доставлено! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Отправитель не будет уведомлён"; /* No comment provided by engineer. */ @@ -4808,6 +5454,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." = "Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; @@ -4827,23 +5476,29 @@ "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 членов, отчёты о доставке выключены."; +"This group has over %lld members, delivery receipts are not sent." = "В этой группе более %lld членов, отчёты о доставке не отправляются."; /* No comment provided by engineer. */ "This group no longer exists." = "Эта группа больше не существует."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Это ваша собственная одноразовая ссылка!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Это ваш собственный адрес SimpleX!"; +"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. */ +"This setting is for your current profile **%@**." = "Эта настройка применяется к Вашему текущему профилю чата **%@**."; + +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Время удаления устанавливается только для новых контактов."; + /* No comment provided by engineer. */ "Title" = "Заголовок"; @@ -4892,9 +5547,15 @@ /* No comment provided by engineer. */ "To send" = "Для оправки"; +/* alert message */ +"To send commands you must be connected." = "Вы должны быть соединены, чтобы отправлять команды."; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены."; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "Чтобы использовать другой профиль после попытки соединения, удалите чат и используйте ссылку снова."; + /* No comment provided by engineer. */ "To use the servers of **%@**, accept conditions of use." = "Чтобы использовать серверы оператора **%@**, примите условия использования."; @@ -4907,6 +5568,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Установите режим Инкогнито при соединении."; +/* token status */ +"Token status: %@." = "Статус токена: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Прозрачность тулбара"; @@ -4919,12 +5583,6 @@ /* 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" = "Турецкий интерфейс"; @@ -5015,10 +5673,7 @@ /* authentication reason */ "Unlock app" = "Разблокировать"; -/* No comment provided by engineer. */ -"unmute" = "уведомлять"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Уведомлять"; /* No comment provided by engineer. */ @@ -5027,6 +5682,9 @@ /* swipe action */ "Unread" = "Не прочитано"; +/* No comment provided by engineer. */ +"Unsupported connection link" = "Ссылка не поддерживается"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "До 100 последних сообщений отправляются новым членам."; @@ -5042,6 +5700,9 @@ /* No comment provided by engineer. */ "Update settings?" = "Обновить настройки?"; +/* No comment provided by engineer. */ +"Updated conditions" = "Обновленные условия"; + /* rcv group event chat item */ "updated group profile" = "обновил(а) профиль группы"; @@ -5051,9 +5712,27 @@ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами."; +/* alert button */ +"Upgrade" = "Обновить"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Обновить адрес"; + +/* alert message */ +"Upgrade address?" = "Обновить адрес?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "Обновить и открыть чат"; +/* alert message */ +"Upgrade group link?" = "Обновить ссылку группы?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Обновить ссылку"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Обновите Ваш адрес"; + /* No comment provided by engineer. */ "Upload errors" = "Ошибки загрузки"; @@ -5081,7 +5760,7 @@ /* No comment provided by engineer. */ "Use chat" = "Использовать чат"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Использовать активный профиль"; /* No comment provided by engineer. */ @@ -5097,9 +5776,12 @@ "Use from desktop" = "Использовать с компьютера"; /* No comment provided by engineer. */ -"Use iOS call interface" = "Использовать интерфейс iOS для звонков"; +"Use incognito profile" = "Использовать профиль инкогнито"; /* No comment provided by engineer. */ +"Use iOS call interface" = "Использовать интерфейс iOS для звонков"; + +/* new chat action */ "Use new incognito profile" = "Использовать новый Инкогнито профиль"; /* No comment provided by engineer. */ @@ -5123,12 +5805,21 @@ /* 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 TCP port 443 for preset servers only." = "Использовать TCP-порт 443 только для серверов по умолчанию."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Используйте приложение во время звонка."; /* No comment provided by engineer. */ "Use the app with one hand." = "Используйте приложение одной рукой."; +/* No comment provided by engineer. */ +"Use web port" = "Использовать веб-порт"; + /* No comment provided by engineer. */ "User selection" = "Выбор пользователя"; @@ -5279,6 +5970,9 @@ /* No comment provided by engineer. */ "Welcome message is too long" = "Приветственное сообщение слишком длинное"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Приветствуйте Ваши контакты 👋"; + /* No comment provided by engineer. */ "What's new" = "Новые функции"; @@ -5348,6 +6042,9 @@ /* No comment provided by engineer. */ "You accepted connection" = "Вы приняли приглашение соединиться"; +/* snd group event chat item */ +"you accepted this member" = "Вы приняли этого члена"; + /* No comment provided by engineer. */ "You allow" = "Вы разрешаете"; @@ -5360,33 +6057,24 @@ /* No comment provided by engineer. */ "You are already connected with %@." = "Вы уже соединены с %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Вы уже соединяетесь с %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Вы уже соединяетесь по этой одноразовой ссылке!"; /* No comment provided by engineer. */ "You are already in group %@." = "Вы уже состоите в группе %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "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. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Вы уже вступаете в группу по этой ссылке."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "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." = "Установлено соединение с сервером, через который Вы получаете сообщения от этого контакта."; - -/* No comment provided by engineer. */ -"you are invited to group" = "Вы приглашены в группу"; - /* No comment provided by engineer. */ "You are invited to group" = "Вы приглашены в группу"; @@ -5405,9 +6093,6 @@ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "Вы можете изменить это в настройках Интерфейса."; -/* No comment provided by engineer. */ -"You can configure operators in Network & servers settings." = "Вы можете настроить операторов в настройках Сети и серверов."; - /* No comment provided by engineer. */ "You can configure servers via settings." = "Вы можете настроить серверы позже."; @@ -5462,7 +6147,10 @@ /* alert message */ "You can view invitation link again in connection details." = "Вы можете увидеть ссылку-приглашение снова открыв соединение."; -/* No comment provided by engineer. */ +/* alert message */ +"You can view your reports in Chat with admins." = "Вы можете найти Ваши жалобы в Чате с админами."; + +/* alert title */ "You can't send messages!" = "Вы не можете отправлять сообщения!"; /* chat item text */ @@ -5483,10 +6171,7 @@ /* 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. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Вы уже запросили соединение!\nПовторить запрос?"; /* No comment provided by engineer. */ @@ -5534,9 +6219,15 @@ /* chat list item description */ "you shared one-time link incognito" = "Вы создали ссылку инкогнито"; +/* token info */ +"You should receive notifications." = "Вы должны получать уведомления."; + /* snd group event chat item */ "you unblocked %@" = "Вы разблокировали %@"; +/* No comment provided by engineer. */ +"You will be able to send messages **only after your request is accepted**." = "Вы сможете отправлять сообщения **только после того как Ваш запрос будет принят**."; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!"; @@ -5552,9 +6243,6 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Вы соединитесь со всеми членами группы."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Вы все равно получите звонки и уведомления в профилях без звука, когда они активные."; @@ -5576,6 +6264,9 @@ /* 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 business contact" = "Ваш бизнес контакт"; + /* No comment provided by engineer. */ "Your calls" = "Ваши звонки"; @@ -5591,8 +6282,14 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Ваши профили чата"; +/* alert message */ +"Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Соединение было перемещено в профиль %@, но при переключении профиля произошла ошибка."; + /* No comment provided by engineer. */ -"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Соединение было перемещено на %@, но при смене профиля произошла неожиданная ошибка."; +"Your connection was moved to %@ but an error happened when switching profile." = "Соединение было перемещено на %@, но при смене профиля произошла неожиданная ошибка."; + +/* No comment provided by engineer. */ +"Your contact" = "Ваш контакт"; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт отправил файл, размер которого превышает максимальный размер (%@)."; @@ -5612,6 +6309,9 @@ /* No comment provided by engineer. */ "Your current profile" = "Ваш активный профиль"; +/* No comment provided by engineer. */ +"Your group" = "Ваша группа"; + /* No comment provided by engineer. */ "Your ICE servers" = "Ваши ICE серверы"; @@ -5627,15 +6327,15 @@ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Будет отправлен Ваш профиль **%@**."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Ваш профиль хранится на Вашем устройстве и отправляется только контактам."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю."; /* 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." = "Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве."; - /* No comment provided by engineer. */ "Your random profile" = "Случайный профиль"; @@ -5651,6 +6351,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваш адрес SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Ваши SMP серверы"; - diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 4fdc49139a..2700711773 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (สามารถคัดลอกได้)"; @@ -16,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)"; @@ -142,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 วินาที"; @@ -187,7 +163,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 วัน"; /* time interval */ @@ -196,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. */ @@ -239,15 +218,16 @@ "above, then choose:" = "ด้านบน จากนั้นเลือก:"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "รับ"; /* notification body */ "Accept contact request from %@?" = "รับการขอติดต่อจาก %@?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "ยอมรับโหมดไม่ระบุตัวตน"; /* call status */ @@ -506,7 +486,8 @@ "Can't invite contacts!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "ยกเลิก"; /* feature offered item */ @@ -546,7 +527,7 @@ "Change self-destruct mode" = "เปลี่ยนโหมดทําลายตัวเอง"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "เปลี่ยนรหัสผ่านแบบทำลายตัวเอง"; /* chat item text */ @@ -651,7 +632,7 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "เชื่อมต่อกับนักพัฒนา SimpleX Chat"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "เชื่อมต่อผ่านลิงก์"; /* No comment provided by engineer. */ @@ -687,7 +668,7 @@ /* No comment provided by engineer. */ "Connection" = "การเชื่อมต่อ"; -/* No comment provided by engineer. */ +/* alert title */ "Connection error" = "การเชื่อมต่อผิดพลาด"; /* No comment provided by engineer. */ @@ -699,7 +680,7 @@ /* No comment provided by engineer. */ "Connection request sent!" = "ส่งคําขอเชื่อมต่อแล้ว!"; -/* No comment provided by engineer. */ +/* alert title */ "Connection timeout" = "หมดเวลาการเชื่อมต่อ"; /* connection information */ @@ -759,9 +740,6 @@ /* server test step */ "Create queue" = "สร้างคิว"; -/* No comment provided by engineer. */ -"Create secret group" = "สร้างกลุ่มลับ"; - /* No comment provided by engineer. */ "Create SimpleX address" = "สร้างที่อยู่ SimpleX"; @@ -855,7 +833,8 @@ /* message decrypt error item */ "Decryption error" = "ข้อผิดพลาดในการ decrypt"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "ค่าเริ่มต้น (%@)"; /* No comment provided by engineer. */ @@ -865,8 +844,7 @@ "default (yes)" = "ค่าเริ่มต้น (ใช่)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "ลบ"; /* No comment provided by engineer. */ @@ -932,7 +910,7 @@ /* No comment provided by engineer. */ "Delete message?" = "ลบข้อความ?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "ลบข้อความ"; /* No comment provided by engineer. */ @@ -1049,7 +1027,7 @@ /* No comment provided by engineer. */ "Don't enable" = "อย่าเปิดใช้งาน"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "ไม่ต้องแสดงอีก"; /* No comment provided by engineer. */ @@ -1082,7 +1060,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. */ @@ -1220,7 +1198,7 @@ /* No comment provided by engineer. */ "Error changing role" = "เกิดข้อผิดพลาดในการเปลี่ยนบทบาท"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า"; /* No comment provided by engineer. */ @@ -1235,19 +1213,19 @@ /* No comment provided by engineer. */ "Error creating profile!" = "เกิดข้อผิดพลาดในการสร้างโปรไฟล์!"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "เกิดข้อผิดพลาดในการลบแชท!"; /* No comment provided by engineer. */ "Error deleting connection" = "เกิดข้อผิดพลาดในการลบการเชื่อมต่อ"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูล"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูลเก่า"; /* No comment provided by engineer. */ @@ -1265,10 +1243,10 @@ /* No comment provided by engineer. */ "Error encrypting database" = "เกิดข้อผิดพลาดในการ encrypt ฐานข้อมูล"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท"; /* No comment provided by engineer. */ @@ -1277,7 +1255,7 @@ /* alert title */ "Error receiving file" = "เกิดข้อผิดพลาดในการรับไฟล์"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "เกิดข้อผิดพลาดในการลบสมาชิก"; /* No comment provided by engineer. */ @@ -1331,7 +1309,9 @@ /* No comment provided by engineer. */ "Error: " = "ผิดพลาด: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "ข้อผิดพลาด: % @"; /* No comment provided by engineer. */ @@ -1400,6 +1380,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "ค้นหาแชทได้เร็วขึ้น"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; + /* No comment provided by engineer. */ "Fix" = "แก้ไข"; @@ -1722,9 +1705,9 @@ "Join" = "เข้าร่วม"; /* No comment provided by engineer. */ -"join as %@" = "เข้าร่วมเป็น %@"; +"Join as %@" = "เข้าร่วมเป็น %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "เข้าร่วมกลุ่ม"; /* No comment provided by engineer. */ @@ -1928,7 +1911,7 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "โปรไฟล์การแชทหลายรายการ"; -/* swipe action */ +/* notification label action */ "Mute" = "ปิดเสียง"; /* No comment provided by engineer. */ @@ -1943,10 +1926,10 @@ /* No comment provided by engineer. */ "Network settings" = "การตั้งค่าเครือข่าย"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "สถานะเครือข่าย"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "ไม่เคย"; /* notification */ @@ -2034,8 +2017,9 @@ "observer" = "ผู้สังเกตการณ์"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "ปิด"; /* blur media */ @@ -2047,7 +2031,9 @@ /* feature offered item */ "offered %@: %@" = "เสนอแล้ว %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "ตกลง"; /* No comment provided by engineer. */ @@ -2110,7 +2096,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้"; -/* No comment provided by engineer. */ +/* new chat action */ "Open chat" = "เปิดแชท"; /* authentication reason */ @@ -2164,7 +2150,7 @@ /* 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. */ +/* alert message */ "Please check your network connection with %@ and try again." = "โปรดตรวจสอบการเชื่อมต่อเครือข่ายของคุณกับ %@ แล้วลองอีกครั้ง"; /* No comment provided by engineer. */ @@ -2197,9 +2183,6 @@ /* No comment provided by engineer. */ "Polish interface" = "อินเตอร์เฟซภาษาโปแลนด์"; -/* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ"; @@ -2302,9 +2285,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "ได้รับการยืนยัน…"; -/* notification */ -"Received file event" = "ได้รับไฟล์"; - /* message info title */ "Received message" = "ได้รับข้อความ"; @@ -2335,11 +2315,12 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "ลดการใช้แบตเตอรี่"; -/* reject incoming call via notification - swipe action */ +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "ปฏิเสธ"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "ปฏิเสธคำขอติดต่อ"; /* call status */ @@ -2433,7 +2414,7 @@ "Run chat" = "เรียกใช้แชท"; /* alert button - chat item action */ +chat item action */ "Save" = "บันทึก"; /* alert button */ @@ -2586,9 +2567,6 @@ /* copied message info */ "Sent at: %@" = "ส่งเมื่อ: %@"; -/* notification */ -"Sent file event" = "เหตุการณ์ไฟล์ที่ส่ง"; - /* message info title */ "Sent message" = "ข้อความที่ส่งแล้ว"; @@ -2596,10 +2574,10 @@ "Sent messages will be deleted after set time." = "ข้อความที่ส่งจะถูกลบหลังเกินเวลาที่กําหนด"; /* server test error */ -"Server requires authorization to create queues, check password" = "เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน"; +"Server requires authorization to create queues, check password." = "เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน"; /* server test error */ -"Server requires authorization to upload, check password" = "เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน"; +"Server requires authorization to upload, check password." = "เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน"; /* No comment provided by engineer. */ "Server test failed!" = "การทดสอบเซิร์ฟเวอร์ล้มเหลว!"; @@ -2635,7 +2613,7 @@ "Settings" = "การตั้งค่า"; /* alert action - chat item action */ +chat item action */ "Share" = "แชร์"; /* No comment provided by engineer. */ @@ -2848,13 +2826,10 @@ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้"; -/* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น"; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "ขีดที่สองที่เราพลาด! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "ผู้ส่งจะไม่ได้รับแจ้ง"; /* No comment provided by engineer. */ @@ -2914,12 +2889,6 @@ /* 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" = "ปิด"; @@ -2971,7 +2940,7 @@ /* authentication reason */ "Unlock app" = "ปลดล็อคแอป"; -/* swipe action */ +/* notification label action */ "Unmute" = "เปิดเสียง"; /* swipe action */ @@ -3142,12 +3111,6 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "คุณได้เชื่อมต่อกับ %@ แล้ว"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้"; - -/* No comment provided by engineer. */ -"you are invited to group" = "คุณได้รับเชิญให้เข้าร่วมกลุ่ม"; - /* No comment provided by engineer. */ "You are invited to group" = "คุณได้รับเชิญให้เข้าร่วมกลุ่ม"; @@ -3190,7 +3153,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "คุณไม่สามารถส่งข้อความได้!"; /* chat item text */ @@ -3311,10 +3274,10 @@ "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." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ 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 random profile" = "โปรไฟล์แบบสุ่มของคุณ"; @@ -3328,6 +3291,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "ที่อยู่ SimpleX ของคุณ"; -/* No comment provided by engineer. */ -"Your SMP servers" = "เซิร์ฟเวอร์ SMP ของคุณ"; - diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index b3eb5d426a..9acf2cc425 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (kopyalanabilir)"; @@ -22,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)"; @@ -82,6 +61,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Önerilen**: cihaz tokeni ve bildirimler SimpleX Chat bildirim sunucularına gönderilir, ama mesajın içeriği, boyutu veya kimden geldiği gönderilmez."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "edindiğiniz bağlantı aracılığıyla bağlanmak için **Linki tarayın/yapıştırın**."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Dikkat**: Anında iletilen bildirimlere Anahtar Zinciri'nde kaydedilmiş parola gereklidir."; @@ -142,6 +124,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ onaylandı"; +/* No comment provided by engineer. */ +"%@ server" = "%@ sunucu"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ sunucular"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ yüklendi"; @@ -190,6 +178,9 @@ /* time interval */ "%d sec" = "%d saniye"; +/* delete after time */ +"%d seconds(s)" = "%d saniye(ler)"; + /* integrity error chat item */ "%d skipped message(s)" = "%d okunmamış mesaj(lar)"; @@ -232,9 +223,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"; @@ -280,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0sn"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 gün"; /* time interval */ @@ -289,12 +278,23 @@ /* 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"; +/* delete after time */ +"1 year" = "1 yıl"; + +/* 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"; @@ -328,6 +328,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Adres değişimi iptal edilsin mi?"; +/* No comment provided by engineer. */ +"About operators" = "Operatörler hakkında"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "SimpleX Chat hakkında"; @@ -338,35 +341,75 @@ "Accent" = "Ana renk"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Kabul et"; +/* alert action */ +"Accept as member" = "Üye olarak kabul et"; + +/* alert action */ +"Accept as observer" = "Gözlemci olarak 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?"; +/* alert title */ +"Accept contact request" = "Kişi isteğini kabul et"; + /* notification body */ "Accept contact request from %@?" = "%@ 'den gelen iletişim isteği kabul edilsin mi?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Takma adla kabul et"; +/* alert title */ +"Accept member" = "Üyeyi kabul et"; + +/* rcv group event chat item */ +"accepted %@" = "kabul edildi %@"; + /* call status */ "accepted call" = "kabul edilen arama"; /* No comment provided by engineer. */ -"Acknowledged" = "Onaylandı"; +"Accepted conditions" = "Kabul edilmiş koşullar"; + +/* chat list item title */ +"accepted invitation" = "davetiye kabul edildi"; + +/* rcv group event chat item */ +"accepted you" = "seni kabul etti"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Onaylı"; /* No comment provided by engineer. */ "Acknowledgement errors" = "Onay hataları"; +/* token status text */ +"Active" = "Aktif"; + /* No comment provided by engineer. */ "Active connections" = "Aktif bağlantılar"; /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek."; +/* No comment provided by engineer. */ +"Add friends" = "Arkadaş ekle"; + +/* No comment provided by engineer. */ +"Add list" = "Liste ekle"; + +/* placeholder for sending contact request */ +"Add message" = "Mesaj ekle"; + /* No comment provided by engineer. */ "Add profile" = "Profil ekle"; @@ -376,12 +419,27 @@ /* 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 to list" = "Listeye 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"; @@ -397,6 +455,12 @@ /* 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"; @@ -421,12 +485,21 @@ /* chat item text */ "agreeing encryption…" = "şifreleme kabul ediliyor…"; +/* member criteria value */ +"all" = "tümü"; + +/* No comment provided by engineer. */ +"All" = "Hepsi"; + /* No comment provided by engineer. */ "All app data is deleted." = "Tüm uygulama verileri silinir."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Tüm konuşmalar ve mesajlar silinecektir. Bu, geri alınamaz!"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Tüm sohbetler %@ listesinden kaldırılacak ve liste silinecek."; + /* No comment provided by engineer. */ "All data is erased when it is entered." = "Kullanıldığında bütün veriler silinir."; @@ -439,6 +512,9 @@ /* 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!"; @@ -451,6 +527,12 @@ /* profile dropdown */ "All profiles" = "Tüm Profiller"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Tüm raporlar sizin için arşivlenecek."; + +/* No comment provided by engineer. */ +"All servers" = "Tüm sunucular"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Konuştuğun kişilerin tümü bağlı kalacaktır."; @@ -475,6 +557,9 @@ /* No comment provided by engineer. */ "Allow downgrade" = "Sürüm düşürmeye izin ver"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Dosyalara ve medyaya yalnızca iletişiminiz izin verdiğinde izin verin."; + /* 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)"; @@ -496,6 +581,9 @@ /* 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." = "Mesajları moderatörlere bildirmeye izin ver."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Dosya ve medya göndermeye izin ver."; @@ -523,16 +611,19 @@ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Kişilerinizin kaybolan mesajlar göndermesine izin verin."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Kişilerinizin dosya ve medya göndermesine izin verin."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Kişilerinizin sesli mesajlar göndermesine izin verin."; /* No comment provided by engineer. */ "Already connected?" = "Zaten bağlandı?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Zaten bağlanılıyor!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Zaten gruba bağlanılıyor!"; /* pref value */ @@ -550,6 +641,9 @@ /* No comment provided by engineer. */ "and %lld other events" = "ve %lld diğer etkinlikler"; +/* report reason */ +"Another reason" = "Başka bir sebep"; + /* No comment provided by engineer. */ "Answer call" = "Aramayı cevapla"; @@ -565,6 +659,9 @@ /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Uygulama yerel dosyaları şifreler (videolar dışında)."; +/* No comment provided by engineer. */ +"App group:" = "Uygulama grubu:"; + /* No comment provided by engineer. */ "App icon" = "Uygulama simgesi"; @@ -592,15 +689,36 @@ /* No comment provided by engineer. */ "Apply to" = "Şuna uygula"; +/* No comment provided by engineer. */ +"Archive" = "Arşivle"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "%lld raporu arşivle?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Tüm raporlar arşivlensin mi?"; + /* 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" = "Raporu arşivle"; + +/* No comment provided by engineer. */ +"Archive report?" = "Rapor arşivlensin mi?"; + +/* swipe action */ +"Archive reports" = "Raporları arşivle"; + /* No comment provided by engineer. */ "Archived contacts" = "Arşivli kişiler"; +/* No comment provided by engineer. */ +"archived report" = "arşivlenmiş rapor"; + /* No comment provided by engineer. */ "Archiving database" = "Veritabanı arşivleniyor"; @@ -649,9 +767,6 @@ /* 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"; @@ -679,6 +794,9 @@ /* No comment provided by engineer. */ "Better groups" = "Daha iyi gruplar"; +/* No comment provided by engineer. */ +"Better groups performance" = "Daha iyi grup performansı"; + /* No comment provided by engineer. */ "Better message dates." = "Daha iyi mesaj tarihleri."; @@ -691,12 +809,21 @@ /* No comment provided by engineer. */ "Better notifications" = "Daha iyi bildirimler"; +/* No comment provided by engineer. */ +"Better privacy and security" = "Daha iyi gizlilik ve güvenlik"; + /* 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. */ +"Bio" = "Biy"; + +/* alert title */ +"Bio too large" = "Biyografi çok uzun"; + /* No comment provided by engineer. */ "Black" = "Siyah"; @@ -724,7 +851,8 @@ /* 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. */ @@ -739,6 +867,9 @@ /* No comment provided by engineer. */ "bold" = "kalın"; +/* No comment provided by engineer. */ +"Bot" = "Bot"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir."; @@ -751,15 +882,33 @@ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Sen ve konuştuğun kişi kaybolan mesajlar gönderebilir."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Sen de kişilerin de medya ve dosya gönderebilir."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Sen ve konuştuğun kişi sesli mesaj gönderebilir."; /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgarca, Fince, Tayca ve Ukraynaca - kullanıcılara ve [Weblate] e teşekkürler! (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "İş adresi"; + +/* No comment provided by engineer. */ +"Business chats" = "İş konuşmaları"; + +/* No comment provided by engineer. */ +"Business connection" = "İş bağlantısı"; + +/* No comment provided by engineer. */ +"Businesses" = "İşletmeler"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Sohbet profiline göre (varsayılan) veya [bağlantıya göre](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "SimpleX Chat'i kullanarak şunları kabul etmiş olursunuz:\n- herkese açık gruplarda yalnızca yasal içerik göndermek.\n- diğer kullanıcılara saygı göstermek – spam yapmamak."; + /* No comment provided by engineer. */ "call" = "Ara"; @@ -790,6 +939,9 @@ /* No comment provided by engineer. */ "Can't call member" = "Üye aranamaz"; +/* alert title */ +"Can't change profile" = "Profil değiştirilemiyor"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Kişi davet edilemiyor!"; @@ -799,8 +951,12 @@ /* No comment provided by engineer. */ "Can't message member" = "Üyeye mesaj gönderilemiyor"; +/* No comment provided by engineer. */ +"can't send messages" = "mesaj gönderilemiyor"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "İptal et"; /* No comment provided by engineer. */ @@ -827,6 +983,12 @@ /* No comment provided by engineer. */ "Change" = "Değiştir"; +/* alert title */ +"Change automatic message deletion?" = "Otomatik mesaj silme değiştirilsin mi?"; + +/* authentication reason */ +"Change chat profiles" = "Sohbet profillerini değiştir"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Veritabanı parolasını değiştir?"; @@ -852,7 +1014,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 */ @@ -870,6 +1032,15 @@ /* chat item text */ "changing address…" = "adres değiştiriliyor…"; +/* No comment provided by engineer. */ +"Chat" = "Sohbet"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Sohbet zaten mevcut"; + +/* new chat sheet title */ +"Chat already exists!" = "Sohbet zaten mevcut!"; + /* No comment provided by engineer. */ "Chat colors" = "Sohbet renkleri"; @@ -915,9 +1086,33 @@ /* 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!"; + +/* chat toolbar */ +"Chat with admins" = "Yöneticilerle sohbet et"; + +/* No comment provided by engineer. */ +"Chat with member" = "Üye ile sohbet et"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Üyeler katılmadan önce onlarla sohbet edin."; + /* No comment provided by engineer. */ "Chats" = "Sohbetler"; +/* No comment provided by engineer. */ +"Chats with members" = "Üyelerle 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."; @@ -951,6 +1146,12 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Sohbet temizlensin mi?"; +/* No comment provided by engineer. */ +"Clear group?" = "Grup temizlensin mi?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Grup temizlensin veya silinsin mi?"; + /* No comment provided by engineer. */ "Clear private notes?" = "Gizli notlar temizlensin mi?"; @@ -966,6 +1167,9 @@ /* No comment provided by engineer. */ "colored" = "renklendirilmiş"; +/* report reason */ +"Community guidelines violation" = "Topluluk kurallarının ihlali"; + /* server test step */ "Compare file" = "Dosya karşılaştır"; @@ -978,9 +1182,33 @@ /* 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: **%@**."; + +/* alert button */ +"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" = "Sunucu operatörlerini yapılandır"; + /* No comment provided by engineer. */ "Confirm" = "Onayla"; @@ -1011,6 +1239,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Yüklemeyi onayla"; +/* token status text */ +"Confirmed" = "Onaylandı"; + /* server test step */ "Connect" = "Bağlan"; @@ -1018,7 +1249,7 @@ "Connect automatically" = "Otomatik olarak bağlan"; /* No comment provided by engineer. */ -"Connect incognito" = "Gizli bağlan"; +"Connect faster! 🚀" = "Daha hızlı bağlanın! 🚀"; /* No comment provided by engineer. */ "Connect to desktop" = "Bilgisayara bağlan"; @@ -1029,25 +1260,22 @@ /* No comment provided by engineer. */ "Connect to your friends faster." = "Arkadaşlarınıza daha hızlı bağlanın."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Kendine mi bağlanacaksın?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Kendine mi bağlanacaksın?\nBu senin kendi tek kullanımlık bağlantın!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Kendine mi bağlanacaksın?\nBu senin kendi SimpleX adresin!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Kişi adresi aracılığıyla bağlan"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Bağlantı aracılığıyla bağlan"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Tek kullanımlık bağlantı aracılığıyla bağlan"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "%@ ile bağlan"; /* No comment provided by engineer. */ @@ -1059,9 +1287,6 @@ /* No comment provided by engineer. */ "Connected desktop" = "Bilgisayara bağlandı"; -/* rcv group event chat item */ -"connected directly" = "doğrudan bağlandı"; - /* No comment provided by engineer. */ "Connected servers" = "Bağlı sunucular"; @@ -1111,6 +1336,9 @@ "Connection and servers status." = "Bağlantı ve sunucuların durumu."; /* No comment provided by engineer. */ +"Connection blocked" = "Bağlantı engellendi"; + +/* alert title */ "Connection error" = "Bağlantı hatası"; /* No comment provided by engineer. */ @@ -1119,6 +1347,12 @@ /* chat list item title (it should not be shown */ "connection established" = "bağlantı kuruldu"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "Bağlantı sunucu operatörü tarafından engellendi:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "Bağlantı hazır değil."; + /* No comment provided by engineer. */ "Connection notifications" = "Bağlantı bildirimleri"; @@ -1126,9 +1360,15 @@ "Connection request sent!" = "Bağlantı daveti gönderildi!"; /* No comment provided by engineer. */ -"Connection terminated" = "Bağlantı sonlandırılmış"; +"Connection requires encryption renegotiation." = "Bağlantı için şifreleme yeniden görüşmesi gerekiyor."; /* No comment provided by engineer. */ +"Connection security" = "Bağlantı güvenliği"; + +/* No comment provided by engineer. */ +"Connection terminated" = "Bağlantı sonlandırılmış"; + +/* alert title */ "Connection timeout" = "Bağlantı süresi geçmiş"; /* No comment provided by engineer. */ @@ -1149,9 +1389,15 @@ /* No comment provided by engineer. */ "Contact already exists" = "Kişi zaten mevcut"; +/* No comment provided by engineer. */ +"contact deleted" = "kişi silindi"; + /* No comment provided by engineer. */ "Contact deleted!" = "Kişiler silindi!"; +/* No comment provided by engineer. */ +"contact disabled" = "kişi devre dışı"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "kişi uçtan uca şifrelemeye sahiptir"; @@ -1170,9 +1416,18 @@ /* No comment provided by engineer. */ "Contact name" = "Kişi adı"; +/* No comment provided by engineer. */ +"contact not ready" = "kişi hazır değil"; + /* No comment provided by engineer. */ "Contact preferences" = "Kişi tercihleri"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Gruplardan gelen iletişim talepleri"; + +/* No comment provided by engineer. */ +"contact should accept…" = "kişi kabul etmeli…"; + /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "Kişiler silinecek - bu geri alınamaz !"; @@ -1182,6 +1437,9 @@ /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "Kişiler silinmesi için mesajları işaretleyebilir; onları görüntüleyebilirsin."; +/* blocking reason */ +"Content violates conditions of use" = "İçerik kullanım koşullarını ihlal ediyor"; + /* No comment provided by engineer. */ "Continue" = "Devam et"; @@ -1206,6 +1464,9 @@ /* No comment provided by engineer. */ "Create" = "Oluştur"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Tek kullanımlık bağlantı oluştur"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Rasgele profil kullanarak grup oluştur."; @@ -1221,6 +1482,9 @@ /* No comment provided by engineer. */ "Create link" = "Bağlantı oluştur"; +/* No comment provided by engineer. */ +"Create list" = "Liste oluştur"; + /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "[bilgisayar uygulaması] nda yeni bir profil oluştur(https://simplex.chat/downloads/). 💻"; @@ -1231,10 +1495,10 @@ "Create queue" = "Sıra oluştur"; /* No comment provided by engineer. */ -"Create secret group" = "Gizli grup oluştur"; +"Create SimpleX address" = "SimpleX adresi oluştur"; /* No comment provided by engineer. */ -"Create SimpleX address" = "SimpleX adresi oluştur"; +"Create your address" = "Adresinizi oluşturun"; /* No comment provided by engineer. */ "Create your profile" = "Profilini oluştur"; @@ -1257,6 +1521,9 @@ /* 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"; @@ -1359,7 +1626,8 @@ /* No comment provided by engineer. */ "decryption errors" = "Şifre çözme hataları"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "varsayılan (%@)"; /* No comment provided by engineer. */ @@ -1369,8 +1637,7 @@ "default (yes)" = "varsayılan (evet)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Sil"; /* No comment provided by engineer. */ @@ -1394,12 +1661,24 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Sil ve kişiye bildir"; +/* No comment provided by engineer. */ +"Delete chat" = "Sohbeti sil"; + +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Sohbet mesajlarını cihazınızdan silin."; + /* No comment provided by engineer. */ "Delete chat profile" = "Sohbet profilini sil"; /* No comment provided by engineer. */ "Delete chat profile?" = "Sohbet profili silinsin mi?"; +/* alert title */ +"Delete chat with member?" = "Üye ile sohbet silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete chat?" = "Sohbet silinsin mi?"; + /* No comment provided by engineer. */ "Delete connection" = "Bağlantıyı sil"; @@ -1445,13 +1724,16 @@ /* No comment provided by engineer. */ "Delete link?" = "Bağlantı silinsin mi?"; +/* alert title */ +"Delete list?" = "Liste silinsin mi?"; + /* No comment provided by engineer. */ "Delete member message?" = "Kişinin mesajı silinsin mi?"; /* No comment provided by engineer. */ "Delete message?" = "Mesaj silinsin mi?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Mesajları sil"; /* No comment provided by engineer. */ @@ -1475,6 +1757,9 @@ /* server test step */ "Delete queue" = "Sırayı sil"; +/* No comment provided by engineer. */ +"Delete report" = "Raporu sil"; + /* No comment provided by engineer. */ "Delete up to 20 messages at once." = "Tek seferde en fazla 20 mesaj silin."; @@ -1505,6 +1790,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "Silme hatası"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Apple tarafından düşürülse bile teslim edilir."; + /* No comment provided by engineer. */ "Delivery" = "Teslimat"; @@ -1514,9 +1802,15 @@ /* No comment provided by engineer. */ "Delivery receipts!" = "Mesaj gönderildi bilgisi!"; +/* No comment provided by engineer. */ +"Deprecated options" = "Kullanımdan kaldırılan seçenekler"; + /* No comment provided by engineer. */ "Description" = "Açıklama"; +/* alert title */ +"Description too large" = "Açıklama çok büyük"; + /* No comment provided by engineer. */ "Desktop address" = "Bilgisayar adresi"; @@ -1571,12 +1865,21 @@ /* chat feature */ "Direct messages" = "Doğrudan mesajlar"; +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this chat." = "Üyeler arası doğrudan mesajlar bu sohbette yasaktır."; + /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "Bu grupta üyeler arasında direkt mesajlaşma yasaktır."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Devre dışı bırak (geçersiz kılmaları koru)"; +/* alert title */ +"Disable automatic message deletion?" = "Otomatik mesaj silme devre dışı bırakılsın mı?"; + +/* alert button */ +"Disable delete messages" = "Mesaj silmeyi devre dışı bırak"; + /* No comment provided by engineer. */ "Disable for all" = "Herkes için devre dışı bırak"; @@ -1637,6 +1940,9 @@ /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "Acil aramalar için SimpleX'i KULLANMAYIN."; +/* No comment provided by engineer. */ +"Documents:" = "Belgeler:"; + /* No comment provided by engineer. */ "Don't create address" = "Adres oluşturma"; @@ -1644,13 +1950,19 @@ "Don't enable" = "Etkinleştirme"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Önemli mesajları kaçırmayın."; + +/* alert action */ "Don't show again" = "Yeniden gösterme"; +/* No comment provided by engineer. */ +"Done" = "Tamam"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Sürüm düşür ve sohbeti aç"; /* alert button - chat item action */ +chat item action */ "Download" = "İndir"; /* No comment provided by engineer. */ @@ -1692,24 +2004,36 @@ /* No comment provided by engineer. */ "e2e encrypted" = "uçtan uca şifrelenmiş"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Uçtan uca şifrelenmiş bildirimler."; + /* chat item action */ "Edit" = "Düzenle"; /* No comment provided by engineer. */ "Edit group profile" = "Grup profilini düzenle"; +/* No comment provided by engineer. */ +"Empty message!" = "Boş mesaj!"; + /* No comment provided by engineer. */ "Enable" = "Etkinleştir"; /* 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. */ "Enable camera access" = "Kamera erişimini etkinleştir"; +/* No comment provided by engineer. */ +"Enable disappearing messages by default." = "Varsayılan olarak kaybolan mesajları etkinleştirin."; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Daha iyi meta veri gizliliği için Ağ & sunucu ayarlarında Flux'u etkinleştirin."; + /* No comment provided by engineer. */ "Enable for all" = "Herkes için etkinleştir"; @@ -1821,6 +2145,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "şifrelemenin yeniden anlaşması %@ için gerekiyor"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Şifreleme yeniden görüşmesi devam ediyor."; + /* No comment provided by engineer. */ "ended" = "bitti"; @@ -1869,28 +2196,46 @@ /* 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"; +/* alert title */ +"Error accepting member" = "Üyeyi kabul etme hatası"; + /* 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 adding short link" = "Kısa bağlantı ekleme hatası"; + /* No comment provided by engineer. */ "Error changing address" = "Adres değiştirilirken hata oluştu"; +/* alert title */ +"Error changing chat profile" = "Sohbet profilini değiştirme hatası"; + /* 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. */ +/* alert title */ "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 checking token status" = "Jeton durumu kontrol hatası"; + +/* alert message */ "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. */ @@ -1902,6 +2247,9 @@ /* No comment provided by engineer. */ "Error creating group link" = "Grup bağlantısı oluşturulurken hata oluştu"; +/* alert title */ +"Error creating list" = "Liste oluşturma hatası"; + /* No comment provided by engineer. */ "Error creating member contact" = "Kişi iletişimi oluşturulurken hata oluştu"; @@ -1911,22 +2259,28 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Profil oluşturulurken hata oluştu!"; +/* No comment provided by engineer. */ +"Error creating report" = "Rapor oluşturma hatası"; + /* No comment provided by engineer. */ "Error decrypting file" = "Dosya şifresi çözülürken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ +"Error deleting chat" = "Üye ile sohbet silme hatası"; + +/* alert title */ "Error deleting chat database" = "Sohbet veritabanı silinirken sorun oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Sohbet silinirken hata oluştu!"; /* No comment provided by engineer. */ "Error deleting connection" = "Bağlantı silinirken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Veritabanı silinirken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Eski veritabanı silinirken hata oluştu"; /* No comment provided by engineer. */ @@ -1947,23 +2301,29 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Veritabanı şifrelemesi çözülürken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Sohbet veritabanı içe aktarılırken hata oluştu"; /* No comment provided by engineer. */ "Error joining group" = "Gruba katılırken hata oluştu"; +/* alert title */ +"Error loading servers" = "Sunucular yüklenirken hata oluştu"; + /* No comment provided by engineer. */ "Error migrating settings" = "Ayarlar taşınırken hata oluştu"; /* No comment provided by engineer. */ -"Error opening chat" = "Sohbeti açarken sorun oluştu"; +"Error opening chat" = "Kişiyi hazırlama hatası"; + +/* No comment provided by engineer. */ +"Error opening group" = "Grubu hazırlama hatası"; /* alert title */ "Error receiving file" = "Dosya alınırken sorun oluştu"; @@ -1974,12 +2334,24 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Hata sunuculara yeniden bağlanılıyor"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "Bildirimler için kayıt hatası"; + +/* alert title */ +"Error rejecting contact request" = "Kişi isteğini reddetme hatası"; + +/* alert title */ "Error removing member" = "Kişiyi silerken sorun oluştu"; +/* alert title */ +"Error reordering lists" = "Listeleri yeniden sıralama hatası"; + /* No comment provided by engineer. */ "Error resetting statistics" = "Hata istatistikler sıfırlanıyor"; +/* alert title */ +"Error saving chat list" = "Sohbet listesini kaydetme hatası"; + /* No comment provided by engineer. */ "Error saving group profile" = "Grup profili kaydedilirken sorun oluştu"; @@ -1992,6 +2364,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"; @@ -2010,6 +2385,9 @@ /* No comment provided by engineer. */ "Error sending message" = "Mesaj gönderilirken hata oluştu"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Otomatik kabul ayarında hata"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Görüldü ayarlanırken hata oluştu!"; @@ -2019,7 +2397,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Sohbet durdurulurken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Profil değiştirme sırasında hata oluştu"; /* alertTitle */ @@ -2028,12 +2406,18 @@ /* No comment provided by engineer. */ "Error synchronizing connection" = "Bağlantı senkronizasyonunda hata oluştu"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Sunucu bağlantısını test etme hatası"; + /* No comment provided by engineer. */ "Error updating group link" = "Grup bağlantısı güncellenirken hata oluştu"; /* No comment provided by engineer. */ "Error updating message" = "Mesaj güncellenirken hata oluştu"; +/* alert title */ +"Error updating server" = "Sunucu güncellenirken hata oluştu"; + /* No comment provided by engineer. */ "Error updating settings" = "Ayarları güncellerken hata oluştu"; @@ -2049,7 +2433,9 @@ /* No comment provided by engineer. */ "Error: " = "Hata: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Hata: %@"; /* No comment provided by engineer. */ @@ -2061,11 +2447,11 @@ /* No comment provided by engineer. */ "Errors" = "Hatalar"; -/* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Konuşma sırasında devre dışı bırakılsa bile."; +/* 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"; @@ -2074,7 +2460,10 @@ "Expand" = "Genişlet"; /* No comment provided by engineer. */ -"expired" = "Süresi dolmuş"; +"expired" = "süresi dolmuş"; + +/* token status text */ +"Expired" = "Süresi dolmuş"; /* No comment provided by engineer. */ "Export database" = "Veritabanını dışarı aktar"; @@ -2100,18 +2489,30 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Hızlı ve gönderici çevrimiçi olana kadar beklemek yok!"; +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Grupların daha hızlı silinmesi."; + /* 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." = "Mesajların daha hızlı gönderilmesi."; + /* swipe action */ "Favorite" = "Favori"; /* No comment provided by engineer. */ +"Favorites" = "Favoriler"; + +/* file error alert title */ "File error" = "Dosya hatası"; /* alert message */ "File errors:\n%@" = "Dosya hataları:\n%@"; +/* file error text */ +"File is blocked by server operator:\n%@." = "Dosya sunucu operatörü tarafından engellendi:\n%@."; + /* file error text */ "File not found - most likely file was deleted or cancelled." = "Dosya bulunamadı - muhtemelen dosya silindi veya göderim iptal edildi."; @@ -2145,6 +2546,9 @@ /* chat feature */ "Files and media" = "Dosyalar ve medya"; +/* No comment provided by engineer. */ +"Files and media are prohibited in this chat." = "Bu sohbette dosyalar ve medya yasaktır."; + /* No comment provided by engineer. */ "Files and media are prohibited." = "Dosyalar ve medya bu grupta yasaklandı."; @@ -2169,6 +2573,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "Sohbetleri daha hızlı bul"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; + /* No comment provided by engineer. */ "Fix" = "Düzelt"; @@ -2187,9 +2594,27 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Düzeltme grup üyesi tarafından desteklenmiyor"; +/* No comment provided by engineer. */ +"For all moderators" = "Tüm moderatörler için"; + +/* 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 me" = "Benim için"; + +/* 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"; @@ -2220,8 +2645,8 @@ /* 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."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Yönlendirme sunucusu %1$@, hedef sunucu %2$@'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: %@."; @@ -2256,6 +2681,9 @@ /* No comment provided by engineer. */ "Further reduced battery usage" = "Daha da azaltılmış pil kullanımı"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Bahsedildiğinde bildirim alın."; + /* No comment provided by engineer. */ "GIFs and stickers" = "GİFler ve çıkartmalar"; @@ -2265,13 +2693,16 @@ /* message preview */ "Good morning!" = "Günaydın!"; +/* shown on group welcome message */ +"group" = "grup"; + /* No comment provided by engineer. */ "Group" = "Grup"; /* No comment provided by engineer. */ "Group already exists" = "Grup çoktan mevcut"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Grup çoktan mevcut!"; /* No comment provided by engineer. */ @@ -2295,6 +2726,9 @@ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Grup davet artık geçerli değil, gönderici tarafından silindi."; +/* No comment provided by engineer. */ +"group is deleted" = "grup silindi"; + /* No comment provided by engineer. */ "Group link" = "Grup bağlantısı"; @@ -2319,6 +2753,9 @@ /* snd group event chat item */ "group profile updated" = "grup profili güncellendi"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "Grup profili değiştirildi. Eğer kaydederseniz, güncellenmiş profil grup üyelerine gönderilecektir."; + /* No comment provided by engineer. */ "Group welcome message" = "Grup hoşgeldin mesajı"; @@ -2328,9 +2765,15 @@ /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "Grup senden silinecektir - bu geri alınamaz!"; +/* No comment provided by engineer. */ +"Groups" = "Gruplar"; + /* No comment provided by engineer. */ "Help" = "Yardım"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Yöneticilere gruplarını yönetmelerinde yardımcı olun."; + /* No comment provided by engineer. */ "Hidden" = "Gizlenmiş"; @@ -2361,6 +2804,15 @@ /* time unit */ "hours" = "saat"; +/* No comment provided by engineer. */ +"How it affects privacy" = "Gizliliğinizi nasıl etkiler"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Gizliliğinizi nasıl arttırır"; + +/* alert button */ +"How it works" = "Nasıl çalışır"; + /* No comment provided by engineer. */ "How SimpleX works" = "SimpleX nasıl çalışır"; @@ -2448,6 +2900,12 @@ /* No comment provided by engineer. */ "inactive" = "inaktif"; +/* report reason */ +"Inappropriate content" = "Uygunsuz içerik"; + +/* report reason */ +"Inappropriate profile" = "Uygunsuz profil"; + /* No comment provided by engineer. */ "Incognito" = "Gizli"; @@ -2514,6 +2972,21 @@ /* No comment provided by engineer. */ "Interface colors" = "Arayüz renkleri"; +/* token status text */ +"Invalid" = "Geçersiz"; + +/* token status text */ +"Invalid (bad token)" = "Geçersiz (kötü jeton)"; + +/* token status text */ +"Invalid (expired)" = "Geçersiz (süresi dolmuş)"; + +/* token status text */ +"Invalid (unregistered)" = "Geçersiz (kayıtlı değil)"; + +/* token status text */ +"Invalid (wrong topic)" = "Geçersiz (yanlış konu)"; + /* invalid chat data */ "invalid chat" = "geçersi̇z sohbet"; @@ -2529,7 +3002,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Geçersiz görünen ad!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Geçersiz bağlantı"; /* No comment provided by engineer. */ @@ -2565,6 +3038,9 @@ /* 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"; @@ -2626,24 +3102,18 @@ "Join" = "Katıl"; /* No comment provided by engineer. */ -"join as %@" = "%@ olarak katıl"; +"Join as %@" = "%@ olarak katıl"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Gruba katıl"; /* No comment provided by engineer. */ "Join group conversations" = "Grup sohbetlerine katıl"; -/* No comment provided by engineer. */ -"Join group?" = "Gruba katılınsın mı?"; - /* No comment provided by engineer. */ "Join incognito" = "Gizli katıl"; -/* No comment provided by engineer. */ -"Join with current profile" = "Şu anki profille katıl"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Bu gruba katılınsın mı?\nBu senin grup için bağlantın %@!"; /* No comment provided by engineer. */ @@ -2661,6 +3131,9 @@ /* alert title */ "Keep unused invitation?" = "Kullanılmamış davet tutulsun mu?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "Sohbetlerinizi temiz tutun"; + /* No comment provided by engineer. */ "Keep your connections" = "Bağlantılarınızı koruyun"; @@ -2679,6 +3152,12 @@ /* 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"; @@ -2688,6 +3167,9 @@ /* rcv group event chat item */ "left" = "ayrıldı"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Mobil ağlarda daha az trafik."; + /* email subject */ "Let's talk in SimpleX Chat" = "Hadi SimpleX Chat'te konuşalım"; @@ -2706,6 +3188,15 @@ /* No comment provided by engineer. */ "Linked desktops" = "Bağlanmış bilgisayarlar"; +/* swipe action */ +"List" = "Liste"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Liste adı ve emojisi tüm listeler için farklı olmalıdır."; + +/* No comment provided by engineer. */ +"List name..." = "Liste adı..."; + /* No comment provided by engineer. */ "LIVE" = "CANLI"; @@ -2715,6 +3206,9 @@ /* No comment provided by engineer. */ "Live messages" = "Canlı mesajlar"; +/* in progress text */ +"Loading profile…" = "Profil yükleniyor…"; + /* No comment provided by engineer. */ "Local name" = "Yerel isim"; @@ -2766,30 +3260,57 @@ /* No comment provided by engineer. */ "Member" = "Kişi"; +/* past/unknown group member */ +"Member %@" = "Üye%@"; + /* profile update event chat item */ "member %@ changed to %@" = "kişi %1$@ , %2$@ olarak değişti"; +/* No comment provided by engineer. */ +"Member admission" = "Üye kabulü"; + /* rcv group event chat item */ "member connected" = "bağlanıldı"; +/* No comment provided by engineer. */ +"member has old version" = "üye eski sürümde"; + /* item status text */ "Member inactive" = "Üye inaktif"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Üye silinmiş - istek kabul edilemez"; + +/* chat feature */ +"Member reports" = "Üye raporları"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Üye rolü \"%@\" olarak değiştirilecektir. Tüm sohbet üyeleri bilgilendirilecektir."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Üye sohbetten kaldırılacak - bu geri alınamaz!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Üye gruptan çıkarılacaktır - bu geri alınamaz!"; +/* alert message */ +"Member will join the group, accept member?" = "Üye gruba katılacak, kabul edilsin mi?"; + /* 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." = "Üyeler mesajları moderatörlere bildirebilir."; + /* No comment provided by engineer. */ "Members can send direct messages." = "Grup üyeleri doğrudan mesajlar gönderebilir."; @@ -2805,6 +3326,9 @@ /* No comment provided by engineer. */ "Members can send voice messages." = "Grup üyeleri sesli mesajlar gönderebilir."; +/* No comment provided by engineer. */ +"Mention members 👋" = "Üyeleri belirtin 👋"; + /* No comment provided by engineer. */ "Menus" = "Menüler"; @@ -2826,6 +3350,9 @@ /* item status text */ "Message forwarded" = "Mesaj iletildi"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Bağlan'a dokunduğunuzda hemen mesaj gönderin."; + /* item status description */ "Message may be delivered later if member becomes active." = "Kullanıcı aktif olursa mesaj iletilebilir."; @@ -2874,9 +3401,15 @@ /* No comment provided by engineer. */ "Messages & files" = "Mesajlar & dosyalar"; +/* No comment provided by engineer. */ +"Messages are protected by **end-to-end encryption**." = "Mesajlar **uçtan uca şifreleme** ile korunmaktadır."; + /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "%@ den gelen mesajlar gösterilecektir!"; +/* alert message */ +"Messages in this chat will never be deleted." = "Bu sohbetteki mesajlar asla silinmeyecek."; + /* No comment provided by engineer. */ "Messages received" = "Mesajlar alındı"; @@ -2949,27 +3482,36 @@ /* marked deleted chat item preview text */ "moderated by %@" = "%@ tarafından yönetilmekte"; +/* member role */ +"moderator" = "moderatör"; + /* time unit */ "months" = "aylar"; +/* swipe action */ +"More" = "Daha fazla"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "Daha fazla geliştirmeler yakında geliyor!"; /* No comment provided by engineer. */ "More reliable network connection." = "Daha güvenilir ağ bağlantısı."; +/* No comment provided by engineer. */ +"More reliable notifications" = "Daha güvenilir bildirimler"; + /* item status description */ "Most likely this connection is deleted." = "Büyük ihtimalle bu bağlantı silinmiş."; /* No comment provided by engineer. */ "Multiple chat profiles" = "Çoklu sohbet profili"; -/* No comment provided by engineer. */ -"mute" = "Sessiz"; - -/* swipe action */ +/* notification label action */ "Mute" = "Sustur"; +/* notification label action */ +"Mute all" = "Tümünü sessize al"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Aktif değilken susturuldu!"; @@ -2982,21 +3524,30 @@ /* No comment provided by engineer. */ "Network connection" = "Ağ bağlantısı"; +/* No comment provided by engineer. */ +"Network decentralization" = "Ağ merkeziyetsizliği"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu."; /* No comment provided by engineer. */ "Network management" = "Ağ yönetimi"; +/* No comment provided by engineer. */ +"Network operator" = "Ağ operatörü"; + /* No comment provided by engineer. */ "Network settings" = "Ağ ayarları"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "Ağ durumu"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "asla"; +/* token status text */ +"New" = "Yeni"; + /* No comment provided by engineer. */ "New chat" = "Yeni sohbet"; @@ -3015,6 +3566,12 @@ /* No comment provided by engineer. */ "New display name" = "Yeni görünen ad"; +/* notification */ +"New events" = "Yeni etkinlikler"; + +/* No comment provided by engineer. */ +"New group role: Moderator" = "Yeni grup rolü: Moderatör"; + /* No comment provided by engineer. */ "New in %@" = "%@ da yeni"; @@ -3024,6 +3581,9 @@ /* No comment provided by engineer. */ "New member role" = "Yeni üye rolü"; +/* rcv group event chat item */ +"New member wants to join the group." = "Yeni üye gruba katılmak istiyor."; + /* notification */ "new message" = "yeni mesaj"; @@ -3036,6 +3596,9 @@ /* No comment provided by engineer. */ "New passphrase…" = "Yeni parola…"; +/* No comment provided by engineer. */ +"New server" = "Yeni sunucu"; + /* 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."; @@ -3051,6 +3614,18 @@ /* Authentication unavailable */ "No app password" = "Uygulama şifresi yok"; +/* No comment provided by engineer. */ +"No chats" = "Hiç sohbet yok"; + +/* No comment provided by engineer. */ +"No chats found" = "Hiç sohbet bulunamadı"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Listede hiç sohbet yok %@"; + +/* No comment provided by engineer. */ +"No chats with members" = "Üyelerle hiç sohbet yok"; + /* No comment provided by engineer. */ "No contacts selected" = "Hiçbir kişi seçilmedi"; @@ -3081,6 +3656,15 @@ /* No comment provided by engineer. */ "No info, try to reload" = "Bilgi yok, yenilemeyi deneyin"; +/* servers error */ +"No media & file servers." = "Hiç medya & dosya sunucusu yok."; + +/* No comment provided by engineer. */ +"No message" = "Mesaj yok"; + +/* servers error */ +"No message servers." = "Hiç mesaj sunucusu yok."; + /* No comment provided by engineer. */ "No network connection" = "Ağ bağlantısı yok"; @@ -3093,21 +3677,48 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Sesli mesaj kaydetmek için izin yok"; +/* alert title */ +"No private routing session" = "Özel yönlendirme oturumu 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"; +/* servers error */ +"No servers for private message routing." = "Özel mesaj yönlendirmesi için hiç sunucu yok."; + +/* servers error */ +"No servers to receive files." = "Dosya almak için hiç sunucu yok."; + +/* servers error */ +"No servers to receive messages." = "Mesaj almak için hiç sunucu yok."; + +/* servers error */ +"No servers to send files." = "Dosya göndermek için hiç sunucu yok."; + /* copied message info in history */ "no text" = "metin yok"; +/* alert title */ +"No token!" = "Hiç jeton yok!"; + +/* No comment provided by engineer. */ +"No unread chats" = "Okunmamış sohbet yok"; + /* No comment provided by engineer. */ "No user identifiers." = "Herhangi bir kullanıcı tanımlayıcısı yok."; /* No comment provided by engineer. */ "Not compatible!" = "Uyumlu değil!"; +/* No comment provided by engineer. */ +"not synchronized" = "senkronize edilmedi"; + +/* No comment provided by engineer. */ +"Notes" = "Notlar"; + /* No comment provided by engineer. */ "Nothing selected" = "Hiçbir şey seçilmedi"; @@ -3120,6 +3731,15 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Bildirimler devre dışı!"; +/* alert title */ +"Notifications error" = "Bildirim hatası"; + +/* No comment provided by engineer. */ +"Notifications privacy" = "Bildirim gizliliği"; + +/* alert title */ +"Notifications status" = "Bildirim durumu"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Şimdi yöneticiler:\n- üyelerin mesajlarını silebilir\n- üyeleri devre dışı bırakabilir (\"gözlemci\" rolü)"; @@ -3127,8 +3747,9 @@ "observer" = "gözlemci"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "kapalı"; /* blur media */ @@ -3140,7 +3761,9 @@ /* feature offered item */ "offered %@: %@" = "%1$@: %2$@ teklif etti"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Tamam"; /* No comment provided by engineer. */ @@ -3164,6 +3787,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion ana bilgisayarları kullanılmayacaktır."; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Yalnızca sohbet sahipleri tercihleri değiştirebilir."; + /* 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."; @@ -3179,6 +3805,12 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "Yalnızca grup sahipleri sesli mesajları etkinleştirebilir."; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Sadece gönderici ve moderatörler görür"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Sadece siz ve moderatörler görür"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "Sadece siz mesaj tepkileri ekleyebilirsiniz."; @@ -3191,6 +3823,9 @@ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "Sadece sen kaybolan mesajlar gönderebilirsin."; +/* No comment provided by engineer. */ +"Only you can send files and media." = "Sadece sen dosya ve medya gönderebilirsin."; + /* No comment provided by engineer. */ "Only you can send voice messages." = "Sadece sen sesli mesajlar gönderebilirsin."; @@ -3207,29 +3842,74 @@ "Only your contact can send disappearing messages." = "Sadece karşıdaki kişi kaybolan mesajlar gönderebilir."; /* No comment provided by engineer. */ -"Only your contact can send voice messages." = "Sadece karşıdaki kişi sesli mesajlar gönderebilir."; +"Only your contact can send files and media." = "Sadece kişilerin dosya ve medya gönderebilir."; /* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Sadece karşıdaki kişi sesli mesajlar gönderebilir."; + +/* alert action */ "Open" = "Aç"; /* No comment provided by engineer. */ +"Open changes" = "Açık değişiklikler"; + +/* new chat action */ "Open chat" = "Sohbeti aç"; /* authentication reason */ "Open chat console" = "Sohbet konsolunu aç"; +/* alert action */ +"Open clean link" = "Temiz linki aç"; + /* No comment provided by engineer. */ +"Open conditions" = "Açık koşullar"; + +/* alert action */ +"Open full link" = "Tam linki aç"; + +/* new chat action */ "Open group" = "Grubu aç"; +/* alert title */ +"Open link?" = "Bağlantıyı aç?"; + /* authentication reason */ "Open migration to another device" = "Başka bir cihaza açık geçiş"; +/* new chat action */ +"Open new chat" = "Yeni sohbet aç"; + +/* new chat action */ +"Open new group" = "Yeni grup aç"; + /* No comment provided by engineer. */ "Open Settings" = "Ayarları aç"; +/* No comment provided by engineer. */ +"Open to accept" = "Kabul etmek için aç"; + +/* No comment provided by engineer. */ +"Open to connect" = "Bağlanmak için aç"; + +/* No comment provided by engineer. */ +"Open to join" = "Katılmak için aç"; + +/* No comment provided by engineer. */ +"Open to use bot" = "Botu kullanmak için aç"; + /* No comment provided by engineer. */ "Opening app…" = "Uygulama açılıyor…"; +/* No comment provided by engineer. */ +"Operator" = "Operatör"; + +/* alert title */ +"Operator server" = "Operatör sunucusu"; + +/* No comment provided by engineer. */ +"Or import archive file" = "Veya arşiv dosyasını içe aktar"; + /* No comment provided by engineer. */ "Or paste archive link" = "Veya arşiv bağlantısını yapıştırın"; @@ -3242,6 +3922,12 @@ /* No comment provided by engineer. */ "Or show this code" = "Veya bu kodu göster"; +/* No comment provided by engineer. */ +"Or to share privately" = "Veya özel olarak paylaşmak için"; + +/* No comment provided by engineer. */ +"Organize chats into lists" = "Sohbetleri listelere ayır"; + /* No comment provided by engineer. */ "other" = "diğer"; @@ -3281,9 +3967,6 @@ /* No comment provided by engineer. */ "Password to show" = "Gösterilecek şifre"; -/* past/unknown group member */ -"Past member %@" = "Geçmiş üye %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Bilgisayar adresini yapıştır"; @@ -3299,9 +3982,18 @@ /* No comment provided by engineer. */ "peer-to-peer" = "eşler arası"; +/* No comment provided by engineer. */ +"pending" = "beklemede"; + /* No comment provided by engineer. */ "Pending" = "Bekleniyor"; +/* No comment provided by engineer. */ +"pending approval" = "onay bekliyor"; + +/* No comment provided by engineer. */ +"pending review" = "inceleme bekliyor"; + /* No comment provided by engineer. */ "Periodic" = "Periyodik olarak"; @@ -3332,7 +4024,7 @@ /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Lütfen doğru bağlantıyı kullandığınızı kontrol edin veya kişiden size başka bir bağlantı göndermesini isteyin."; -/* No comment provided by engineer. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Lütfen ağ bağlantınızı %@ ile kontrol edin ve tekrar deneyin."; /* No comment provided by engineer. */ @@ -3368,21 +4060,33 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz parolayı DEĞİŞTİREMEZSİNİZ."; +/* token info */ +"Please try to disable and re-enable notfications." = "Lütfen bildirimleri devre dışı bırakmayı ve yeniden etkinleştirmeyi deneyin."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Lütfen grup moderatörlerinin gruba katılma isteğinizi incelemesini bekleyin."; + +/* token info */ +"Please wait for token activation to complete." = "Lütfen jeton aktivasyonunun tamamlanmasını bekleyin."; + +/* token info */ +"Please wait for token to be registered." = "Lütfen jeton kaydedilene kadar bekleyin."; + /* No comment provided by engineer. */ "Polish interface" = "Lehçe arayüz"; /* No comment provided by engineer. */ "Port" = "Port"; -/* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Son mesaj taslağını ekleriyle birlikte koru."; /* No comment provided by engineer. */ "Preset server address" = "Ön ayarlı sunucu adresi"; +/* No comment provided by engineer. */ +"Preset servers" = "Ön ayar sunucuları"; + /* No comment provided by engineer. */ "Preview" = "Ön izleme"; @@ -3392,12 +4096,24 @@ /* No comment provided by engineer. */ "Privacy & security" = "Gizlilik & güvenlik"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Müşterileriniz için gizlilik."; + +/* No comment provided by engineer. */ +"Privacy policy and conditions of use." = "Gizlilik politikası ve kullanım koşulları."; + /* No comment provided by engineer. */ "Privacy redefined" = "Gizlilik yeniden tanımlandı"; +/* No comment provided by engineer. */ +"Private chats, groups and your contacts are not accessible to server operators." = "Özel sohbetler, gruplar ve kişilerinize sunucu operatörleri tarafından erişilemez."; + /* No comment provided by engineer. */ "Private filenames" = "Gizli dosya adları"; +/* No comment provided by engineer. */ +"Private media file names." = "Özel medya dosyası adları."; + /* No comment provided by engineer. */ "Private message routing" = "Gizli mesaj yönlendirme"; @@ -3410,9 +4126,12 @@ /* No comment provided by engineer. */ "Private routing" = "Gizli yönlendirme"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Gizli yönlendirme hatası"; +/* alert title */ +"Private routing timeout" = "Özel yönlendirme zaman aşımı"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil ve sunucu bağlantıları"; @@ -3443,6 +4162,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Mesajlarda tepkileri yasakla."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Mesajları moderatörlere bildirmeyi yasakla."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Üyelere doğrudan mesaj göndermeyi yasakla."; @@ -3470,6 +4192,9 @@ /* 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 background timeout" = "Protokol arka plan zaman aşımı"; + /* No comment provided by engineer. */ "Protocol timeout" = "Protokol zaman aşımı"; @@ -3525,7 +4250,7 @@ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "[GitHub deposu]nda daha fazlasını okuyun(https://github.com/simplex-chat/simplex-chat#readme)."; /* No comment provided by engineer. */ -"Receipts are disabled" = "Alıcılar devre dışı bırakıldı"; +"Receipts are disabled" = "Alındı onayları devre dışı bırakıldı"; /* No comment provided by engineer. */ "Receive errors" = "Alım sırasında hata"; @@ -3542,9 +4267,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "onaylama alındı…"; -/* notification */ -"Received file event" = "Dosya etkinliği alındı"; - /* message info title */ "Received message" = "Mesaj alındı"; @@ -3605,16 +4327,32 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Azaltılmış pil kullanımı"; -/* reject incoming call via notification - swipe action */ +/* No comment provided by engineer. */ +"Register" = "Kaydol"; + +/* token info */ +"Register notification token?" = "Bildirim jetonunu kaydet?"; + +/* token status text */ +"Registered" = "Kayıtlı"; + +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Reddet"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Reddet (göndericiye bildirim GİTMEYECEKTİR)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Bağlanma isteğini reddet"; +/* alert title */ +"Reject member?" = "Üyeyi reddet?"; + +/* No comment provided by engineer. */ +"rejected" = "reddedildi"; + /* call status */ "rejected call" = "geri çevrilmiş çağrı"; @@ -3633,6 +4371,9 @@ /* No comment provided by engineer. */ "Remove image" = "Resmi kaldır"; +/* No comment provided by engineer. */ +"Remove link tracking" = "Bağlantı izlemeyi kaldır"; + /* No comment provided by engineer. */ "Remove member" = "Kişiyi sil"; @@ -3651,12 +4392,18 @@ /* profile update event chat item */ "removed contact address" = "kişi adresi silindi"; +/* No comment provided by engineer. */ +"removed from group" = "gruptan çıkarıldı"; + /* profile update event chat item */ "removed profile picture" = "profil fotoğrafı silindi"; /* rcv group event chat item */ "removed you" = "sen kaldırıldın"; +/* No comment provided by engineer. */ +"Removes messages and blocks members." = "Mesajları kaldırır ve üyeleri engeller."; + /* No comment provided by engineer. */ "Renegotiate" = "Yeniden müzakere"; @@ -3666,24 +4413,66 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Şifreleme yeniden müzakere edilsin mi?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Bağlantı isteği tekrarlansın mı?"; - /* No comment provided by engineer. */ "Repeat download" = "Tekrar indir"; /* No comment provided by engineer. */ "Repeat import" = "İthalatı tekrarla"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Katılma isteği tekrarlansın mı?"; - /* No comment provided by engineer. */ "Repeat upload" = "Yüklemeyi tekrarla"; /* chat item action */ "Reply" = "Yanıtla"; +/* chat item action */ +"Report" = "Rapor et"; + +/* report reason */ +"Report content: only group moderators will see it." = "İçeriği rapor et: sadece grup moderatörleri görecek."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Üye profilini rapor et: sadece grup moderatörleri görecek."; + +/* report reason */ +"Report other: only group moderators will see it." = "Diğerini rapor et: sadece grup moderatörleri görecek."; + +/* No comment provided by engineer. */ +"Report reason?" = "Rapor nedeni?"; + +/* alert title */ +"Report sent to moderators" = "Rapor moderatörlere gönderildi"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Spam rapor et: sadece grup moderatörleri görecek."; + +/* report reason */ +"Report violation: only group moderators will see it." = "İhlali rapor et: sadece grup moderatörleri görecek."; + +/* report in notification */ +"Report: %@" = "Rapor: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Mesajları moderatörlere bildirmek yasaktır."; + +/* No comment provided by engineer. */ +"Reports" = "Raporlar"; + +/* No comment provided by engineer. */ +"request is sent" = "istek gönderildi"; + +/* No comment provided by engineer. */ +"request to join rejected" = "katılma isteği reddedildi"; + +/* rcv group event chat item */ +"requested connection" = "istenilen bağlantı"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "%@ grubundan bağlantı isteği"; + +/* chat list item title */ +"requested to connect" = "bağlanma isteği gönderildi"; + /* No comment provided by engineer. */ "Required" = "Gerekli"; @@ -3729,12 +4518,30 @@ /* No comment provided by engineer. */ "Restore database error" = "Veritabanını geri yüklerken hata oluştu"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Yeniden dene"; /* chat item action */ "Reveal" = "Göster"; +/* No comment provided by engineer. */ +"review" = "incele"; + +/* No comment provided by engineer. */ +"Review conditions" = "Koşulları gözden geçir"; + +/* No comment provided by engineer. */ +"Review group members" = "Grup üyelerini gözden geçir"; + +/* admission stage */ +"Review members" = "Üyeleri gözden geçir"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Üyeleri kabul etmeden önce gözden geçir (\"kapı çalma\")."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "yöneticiler tarafından incelendi"; + /* No comment provided by engineer. */ "Revoke" = "İptal et"; @@ -3757,12 +4564,18 @@ "Safer groups" = "Daha güvenli gruplar"; /* alert button - chat item action */ +chat item action */ "Save" = "Kaydet"; /* alert button */ "Save (and notify contacts)" = "Kaydet (ve kişilere bildir)"; +/* alert button */ +"Save (and notify members)" = "Kaydet (ve üyelere bildir)"; + +/* alert title */ +"Save admission settings?" = "Kabul ayarlarını kaydet?"; + /* alert button */ "Save and notify contact" = "Kaydet ve kişilere bildir"; @@ -3778,6 +4591,12 @@ /* No comment provided by engineer. */ "Save group profile" = "Grup profilini kaydet"; +/* alert title */ +"Save group profile?" = "Grup profilini kaydet?"; + +/* No comment provided by engineer. */ +"Save list" = "Listeyi kaydet"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Parolayı kaydet ve sohbeti aç"; @@ -3914,10 +4733,10 @@ "Send a live message - it will update for the recipient(s) as you type it" = "Bir canlı mesaj gönder - yazışına göre kişiye(lere) kendini günceller"; /* No comment provided by engineer. */ -"Send delivery receipts to" = "Görüldü bilgilerini şuraya gönder"; +"Send contact request?" = "Kişi isteği gönderilsin mi?"; /* No comment provided by engineer. */ -"send direct message" = "doğrudan mesaj gönder"; +"Send delivery receipts to" = "Görüldü bilgilerini şuraya gönder"; /* No comment provided by engineer. */ "Send direct message to connect" = "Bağlanmak için doğrudan mesaj gönder"; @@ -3946,18 +4765,30 @@ /* No comment provided by engineer. */ "Send notifications" = "Bildirimler gönder"; +/* No comment provided by engineer. */ +"Send private reports" = "Özel raporlar gönder"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Fikirler ve sorular gönderin"; /* No comment provided by engineer. */ "Send receipts" = "Mesajlar gönder"; +/* No comment provided by engineer. */ +"Send request" = "İstek gönder"; + +/* No comment provided by engineer. */ +"Send request without message" = "Mesaj olmadan istek gönder"; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Bunları galeriden veya özel klavyelerden gönder."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Yeni üyelere 100 adete kadar son mesajları gönderin."; +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Özel geri bildiriminizi gruplara gönderin."; + /* alert message */ "Sender cancelled file transfer." = "Gönderici dosya gönderimini iptal etti."; @@ -3968,7 +4799,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." = "Tüm kişiler için iletim bilgisi gönderme özelliği etkinleştirilecek."; +"Sending delivery receipts will be enabled for all contacts." = "Tüm kişiler için alındı bilgisi gönderme özelliği etkinleştirilecek."; /* No comment provided by engineer. */ "Sending file will be stopped." = "Dosya gönderimi durdurulacaktır."; @@ -3997,9 +4828,6 @@ /* 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"; @@ -4021,6 +4849,9 @@ /* No comment provided by engineer. */ "Server" = "Sunucu"; +/* alert message */ +"Server added to operator %@." = "Sunucu operatör %@'ya eklendi."; + /* No comment provided by engineer. */ "Server address" = "Sunucu adresi"; @@ -4030,14 +4861,23 @@ /* srv error text. */ "Server address is incompatible with network settings." = "Sunucu adresi ağ ayarlarıyla uyumlu değil."; +/* alert title */ +"Server operator changed." = "Sunucu operatörü değişti."; + +/* No comment provided by engineer. */ +"Server operators" = "Sunucu operatörleri"; + +/* alert title */ +"Server protocol changed." = "Sunucu protokolü değişti."; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "sunucu kuyruk bilgisi: %1$@\n\nson alınan msj: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin"; +"Server requires authorization to create queues, check password." = "Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin"; /* server test error */ -"Server requires authorization to upload, check password" = "Sunucunun yükleme yapması için yetki gereklidir, şifreyi kontrol edin"; +"Server requires authorization to upload, check password." = "Sunucunun yükleme yapması için yetki gereklidir, şifreyi kontrol edin"; /* No comment provided by engineer. */ "Server test failed!" = "Sunucu testinde hata oluştu!"; @@ -4066,6 +4906,9 @@ /* No comment provided by engineer. */ "Set 1 day" = "1 günlüğüne ayarla"; +/* No comment provided by engineer. */ +"Set chat name…" = "Sohbet adını belirle…"; + /* No comment provided by engineer. */ "Set contact name…" = "Kişi adı gir…"; @@ -4078,6 +4921,12 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Sistem kimlik doğrulaması yerine ayarla."; +/* No comment provided by engineer. */ +"Set member admission" = "Üye kabulünü ayarla"; + +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Sohbetlerde mesajın sonlanma süresini ayarla."; + /* profile update event chat item */ "set new contact address" = "yeni kişi adresi ayarla"; @@ -4093,6 +4942,9 @@ /* No comment provided by engineer. */ "Set passphrase to export" = "Dışa aktarmak için parola ayarla"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Profil biyografisi ve hoşgeldiniz mesajı düzenle."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "Yeni üyeler için gösterilen bir mesaj ayarla!"; @@ -4109,15 +4961,21 @@ "Shape profile images" = "Profil resimlerini şekillendir"; /* alert action - chat item action */ +chat item action */ "Share" = "Paylaş"; /* No comment provided by engineer. */ "Share 1-time link" = "Tek kullanımlık bağlantıyı paylaş"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Arkadaşınızla 1 defaya mahsus bağlantı paylaşın"; + /* No comment provided by engineer. */ "Share address" = "Adresi paylaş"; +/* No comment provided by engineer. */ +"Share address publicly" = "Adresinizi herkese açık olarak paylaşın"; + /* alert title */ "Share address with contacts?" = "Kişilerle adres paylaşılsın mı?"; @@ -4127,9 +4985,18 @@ /* No comment provided by engineer. */ "Share link" = "Bağlantıyı paylaş"; +/* alert button */ +"Share old address" = "Eski adresi paylaş"; + +/* alert button */ +"Share old link" = "Eski bağlantıyı paylaş"; + /* No comment provided by engineer. */ "Share profile" = "Profil paylaş"; +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "SimpleX adresini sosyal medyada paylaşın."; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "Bu tek kullanımlık bağlantı davetini paylaş"; @@ -4139,6 +5006,18 @@ /* No comment provided by engineer. */ "Share with contacts" = "Kişilerle paylaş"; +/* No comment provided by engineer. */ +"Share your address" = "Adresini paylaş"; + +/* No comment provided by engineer. */ +"Short description" = "Kısa açıklama"; + +/* No comment provided by engineer. */ +"Short link" = "Kısa bağlantı"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Kısa SimpleX adresi"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Gizli yönlendirme yoluyla gönderilen mesajlarda → işaretini göster."; @@ -4175,6 +5054,21 @@ /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX Adresi"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "SimpleX adresi ve 1 defaya mahsus bağlantılar, herhangi bir mesajlaşma uygulaması aracılığıyla paylaşmak için güvenlidir."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX adresi mi yoksa 1 defaya mahsus bağlantı mı?"; + +/* alert title */ +"SimpleX address settings" = "Ayarları otomatik olarak kabul et"; + +/* simplex link type */ +"SimpleX channel link" = "SimpleX kanal bağlantısı"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat ve Flux, Flux tarafından işletilen sunucuları uygulamaya dahil etmek için bir anlaşma yaptı."; + /* 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."; @@ -4214,6 +5108,9 @@ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "SimpleX protokolleri Trail of Bits tarafından incelenmiştir."; +/* simplex link type */ +"SimpleX relay link" = "SimpleX aktarıcı bağlantısı"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Basitleştirilmiş gizli mod"; @@ -4250,9 +5147,16 @@ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "İçe aktarma sırasında bazı önemli olmayan hatalar oluştu:"; +/* alert message */ +"Some servers failed the test:\n%@" = "Bazı sunucular testi geçemedi:\n%@"; + /* notification title */ "Somebody" = "Biri"; +/* blocking reason +report reason */ +"Spam" = "Spam"; + /* No comment provided by engineer. */ "Square, circle, or anything in between." = "Kare,daire, veya aralarında herhangi bir şey."; @@ -4310,6 +5214,9 @@ /* No comment provided by engineer. */ "Stopping chat" = "Sohbeti durdurma"; +/* No comment provided by engineer. */ +"Storage" = "Depolama"; + /* No comment provided by engineer. */ "strike" = "çizik"; @@ -4352,6 +5259,21 @@ /* No comment provided by engineer. */ "Tap button " = "Tuşa bas "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "Sohbet etmek için Bağlan'a dokunun"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "Bağlan'a dokunarak isteği gönderin"; + +/* No comment provided by engineer. */ +"Tap Connect to use bot" = "Botu kullanmak için Bağlan tuşuna bas"; + +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Daha sonra oluşturmak için menüden BasitX adresi oluştur'a dokunun."; + +/* No comment provided by engineer. */ +"Tap Join group" = "Gruba katıl'a dokunun"; + /* No comment provided by engineer. */ "Tap to activate profile." = "Profili etkinleştirmek için tıkla."; @@ -4373,9 +5295,15 @@ /* No comment provided by engineer. */ "TCP connection" = "TCP bağlantısı"; +/* No comment provided by engineer. */ +"TCP connection bg timeout" = "TCP bağlantı arka plan zaman aşımı"; + /* No comment provided by engineer. */ "TCP connection timeout" = "TCP bağlantı zaman aşımı"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "Mesajlaşma için TCP portu"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4385,12 +5313,15 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_TVLDEKAL"; -/* No comment provided by engineer. */ +/* file error alert title */ "Temporary file error" = "Geçici dosya hatası"; /* server test failure */ "Test failed at step %@." = "Test %@ adımında başarısız oldu."; +/* No comment provided by engineer. */ +"Test notifications" = "Bildirimleri test et"; + /* No comment provided by engineer. */ "Test server" = "Sunucuyu test et"; @@ -4409,9 +5340,15 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Kullanıcılar için teşekkürler - Weblate aracılığıyla katkıda bulun!"; +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "Adres kısa olacak ve profiliniz bu adres üzerinden paylaşılacaktır."; + /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "Uygulama, her sohbette farklı operatörler kullanarak gizliliğinizi korur."; + /* 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ç)."; @@ -4421,6 +5358,9 @@ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Taradığınız kod bir SimpleX bağlantı QR kodu değildir."; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "Bağlantı, teslim edilmemiş mesajlar limitine ulaştı, kişiniz çevrimdışı olabilir."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Bağlantı kabulünüz iptal edilecektir!"; @@ -4442,6 +5382,9 @@ /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Bir sonraki mesajın kimliği yanlış (bir öncekinden az veya aynı).\nBazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir."; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "Bağlantı kısa olacak ve grup profili bağlantı üzerinden paylaşılacaktır."; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "Mesaj tüm üyeler için silinecektir."; @@ -4458,17 +5401,23 @@ "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."; +"The same conditions will apply to operator **%@**." = "Aynı koşullar operatör **%@** için de geçerli olacaktır."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Uygulamadaki ikinci ön ayar operatörü!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Özlediğimiz ikinci tik! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Gönderene BİLDİRİLMEYECEKTİR"; /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Mevcut sohbet profilinizin yeni bağlantıları için sunucular **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "Mevcut sohbet profiliniz için yeni dosyaların sunucuları **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Yapıştırdığın metin bir SimpleX bağlantısı değildir."; @@ -4478,6 +5427,9 @@ /* No comment provided by engineer. */ "Themes" = "Temalar"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Bu koşullar ayrıca şunlar için de geçerli olacaktır: **%@**."; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Bu ayarlar mevcut profiliniz **%@** içindir."; @@ -4490,6 +5442,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." = "Bu işlem geri alınamaz - seçilenden daha önce gönderilen ve alınan mesajlar silinecektir. Bu işlem birkaç dakika sürebilir."; +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Bu işlem geri alınamaz - daha önce seçilen tarihten önceki bu sohbette gönderilen ve alınan mesajlar silinecektir."; + /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Bu işlem geri alınamaz - profiliniz, kişileriniz, mesajlarınız ve dosyalarınız geri döndürülemez şekilde kaybolacaktır."; @@ -4515,17 +5470,23 @@ "This group no longer exists." = "Bu grup artık mevcut değildir."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Bu senin kendi tek kullanımlık bağlantın!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Bu senin kendi SimpleX adresin!"; +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Bu bağlantı daha yeni bir uygulama sürümü gerektiriyor. Lütfen uygulamayı güncelleyin veya kişinizden uyumlu bir bağlantı göndermesini isteyin."; /* 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." = "Bu mesaj silindi veya henüz alınmadı."; + /* 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. */ +"This setting is for your current profile **%@**." = "Bu ayar, mevcut profiliniz içindir."; + +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Kaybolma süresi yalnızca yeni kişiler için ayarlanır."; + /* No comment provided by engineer. */ "Title" = "Başlık"; @@ -4541,6 +5502,9 @@ /* 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 against your link being replaced, you can compare contact security codes." = "Bağlantınızın değiştirilmesine karşı korunmak için, kişi güvenlik kodlarını karşılaştırabilirsiniz."; + /* 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."; @@ -4553,6 +5517,9 @@ /* 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" = "Almak için"; + /* 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."; @@ -4565,9 +5532,21 @@ /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Gizli profilinizi ortaya çıkarmak için **Sohbet profilleriniz** sayfasındaki arama alanına tam bir şifre girin."; +/* No comment provided by engineer. */ +"To send" = "Göndermek için"; + +/* alert message */ +"To send commands you must be connected." = "Komut göndermek için bağlı olmanız gerekir."; + /* 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."; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "Bağlantı denemesinden sonra başka bir profili kullanmak için, sohbeti silin ve bağlantıyı tekrar kullanın."; + +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "**%@**'nın sunucularını kullanmak için, kullanım koşullarını kabul edin."; + /* 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)."; @@ -4577,6 +5556,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Bağlanırken gizli moda geçiş yap."; +/* token status */ +"Token status: %@." = "Jeton durumu: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Araç çubuğu opaklığı"; @@ -4589,12 +5571,6 @@ /* No comment provided by engineer. */ "Transport sessions" = "Taşıma oturumları"; -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %@)."; - -/* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor."; - /* No comment provided by engineer. */ "Turkish interface" = "Türkçe arayüz"; @@ -4625,6 +5601,9 @@ /* rcv group event chat item */ "unblocked %@" = "engeli kaldırıldı %@"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Teslim edilmemiş mesajlar"; + /* No comment provided by engineer. */ "Unexpected migration state" = "Beklenmeyen geçiş durumu"; @@ -4682,10 +5661,7 @@ /* authentication reason */ "Unlock app" = "Uygulama kilidini aç"; -/* No comment provided by engineer. */ -"unmute" = "susturmayı kaldır"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Susturmayı kaldır"; /* No comment provided by engineer. */ @@ -4694,6 +5670,9 @@ /* swipe action */ "Unread" = "Okunmamış"; +/* No comment provided by engineer. */ +"Unsupported connection link" = "Desteklenmeyen bağlantı bağlantısı"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Yeni üyelere 100e kadar en son mesajlar gönderildi."; @@ -4709,6 +5688,9 @@ /* No comment provided by engineer. */ "Update settings?" = "Ayarları güncelleyelim mi?"; +/* No comment provided by engineer. */ +"Updated conditions" = "Güncellenmiş koşullar"; + /* rcv group event chat item */ "updated group profile" = "grup profili güncellendi"; @@ -4718,9 +5700,27 @@ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Ayarların güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır."; +/* alert button */ +"Upgrade" = "Yükseltme"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Adres güncelleme"; + +/* alert message */ +"Upgrade address?" = "Adres güncellensin mi?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "Yükselt ve sohbeti aç"; +/* alert message */ +"Upgrade group link?" = "Grub linki güncellensin mi?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Linki güncelle"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Adresini yükselt"; + /* No comment provided by engineer. */ "Upload errors" = "Yükleme hataları"; @@ -4743,11 +5743,20 @@ "Use .onion hosts" = ".onion ana bilgisayarlarını kullan"; /* No comment provided by engineer. */ -"Use chat" = "Sohbeti kullan"; +"Use %@" = "%@ kullan"; /* No comment provided by engineer. */ +"Use chat" = "Sohbeti kullan"; + +/* new chat action */ "Use current profile" = "Şu anki profili kullan"; +/* No comment provided by engineer. */ +"Use for files" = "Dosyalar için kullan"; + +/* No comment provided by engineer. */ +"Use for messages" = "Mesajlar için kullan"; + /* No comment provided by engineer. */ "Use for new connections" = "Yeni bağlantılar için kullan"; @@ -4755,9 +5764,12 @@ "Use from desktop" = "Bilgisayardan kullan"; /* No comment provided by engineer. */ -"Use iOS call interface" = "iOS arama arayüzünden kullan"; +"Use incognito profile" = "Gizli profil kullan"; /* No comment provided by engineer. */ +"Use iOS call interface" = "iOS arama arayüzünden kullan"; + +/* new chat action */ "Use new incognito profile" = "Yeni gizli profilden kullan"; /* No comment provided by engineer. */ @@ -4772,18 +5784,30 @@ /* No comment provided by engineer. */ "Use server" = "Sunucu kullan"; +/* No comment provided by engineer. */ +"Use servers" = "Sunucuları 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 TCP port %@ when no port is specified." = "Port belirtilmediğinde TCP port %@ kullanın."; + +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "Sadece ön ayar sunucuları için TCP port 443 kullanın."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Görüşme sırasında uygulamayı kullanın."; /* No comment provided by engineer. */ "Use the app with one hand." = "Uygulamayı tek elle kullan."; +/* No comment provided by engineer. */ +"Use web port" = "Web portunu kullan"; + /* No comment provided by engineer. */ "User selection" = "Kullanıcı seçimi"; @@ -4856,9 +5880,15 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "1gb'a kadar videolar ve dosyalar"; +/* No comment provided by engineer. */ +"View conditions" = "Koşulları görüntüle"; + /* No comment provided by engineer. */ "View security code" = "Güvenlik kodunu görüntüle"; +/* No comment provided by engineer. */ +"View updated conditions" = "Güncellenmiş koşulları görüntüle"; + /* chat feature */ "Visible history" = "Görünür geçmiş"; @@ -4928,6 +5958,9 @@ /* No comment provided by engineer. */ "Welcome message is too long" = "Hoş geldiniz mesajı çok uzun"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Kişilerine hoş geldin👋"; + /* No comment provided by engineer. */ "What's new" = "Neler yeni"; @@ -4940,6 +5973,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "IP gizliyken"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Birden fazla operatör etkinleştirildiğinde, hiçbiri kimin kiminle iletişim kurduğunu öğrenmek için meta veriye sahip değildir."; + /* 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."; @@ -4994,6 +6030,9 @@ /* No comment provided by engineer. */ "You accepted connection" = "Bağlantıyı onayladın"; +/* snd group event chat item */ +"you accepted this member" = "bu üyeyi kabul ettiniz"; + /* No comment provided by engineer. */ "You allow" = "İzin veriyorsunuz"; @@ -5004,32 +6043,26 @@ "You are already connected to %@." = "Zaten %@'a bağlısınız."; /* No comment provided by engineer. */ +"You are already connected with %@." = "Zaten %@ ile bağlantıdasınız."; + +/* new chat sheet message */ "You are already connecting to %@." = "Zaten %@'a bağlanıyorsunuz."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Bu tek seferlik bağlantı üzerinden zaten bağlanıyorsunuz!"; /* No comment provided by engineer. */ "You are already in group %@." = "Zaten %@ grubundasın."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Zaten %@ grubuna katılıyorsunuz."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Bu bağlantı üzerinden gruba zaten katılıyorsunuz!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Gruba zaten bu bağlantı üzerinden katılıyorsunuz."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Gruba zaten katılıyorsunuz!\nKatılma isteği tekrarlansın mı?"; -/* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız."; - -/* No comment provided by engineer. */ -"you are invited to group" = "gruba davet edildiniz"; - /* No comment provided by engineer. */ "You are invited to group" = "Gruba davet edildiniz"; @@ -5048,6 +6081,9 @@ /* 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." = "Sunucuları ayarlar aracılığıyla yapılandırabilirsiniz."; + /* No comment provided by engineer. */ "You can create it later" = "Daha sonra oluşturabilirsiniz"; @@ -5072,6 +6108,9 @@ /* 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 connection name, to remember who the link was shared with." = "Bağlantı adını ayarlayabilirsiniz, böylece bağlantının kiminle paylaşıldığını hatırlarsınız."; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz."; @@ -5096,7 +6135,10 @@ /* 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. */ +/* alert message */ +"You can view your reports in Chat with admins." = "Raporlarınızı Yöneticilerle Sohbet bölümünde görüntüleyebilirsiniz."; + +/* alert title */ "You can't send messages!" = "Mesajlar gönderemezsiniz!"; /* chat item text */ @@ -5117,10 +6159,7 @@ /* No comment provided by engineer. */ "You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Bu adres üzerinden zaten bağlantı talebinde bulundunuz!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Zaten bağlantı isteğinde bulundunuz!\nBağlantı isteği tekrarlansın mı?"; /* No comment provided by engineer. */ @@ -5168,9 +6207,15 @@ /* chat list item description */ "you shared one-time link incognito" = "tek kullanımlık link paylaştınız gizli"; +/* token info */ +"You should receive notifications." = "Bildirim almanız gerekiyor."; + /* snd group event chat item */ "you unblocked %@" = "engelini kaldırdın %@"; +/* No comment provided by engineer. */ +"You will be able to send messages **only after your request is accepted**." = "Mesaj gönderebilmek için **isteğinizin kabul edilmesini beklemelisiniz**."; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Grup sahibinin cihazı çevrimiçi olduğunda gruba bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!"; @@ -5187,10 +6232,10 @@ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Arka planda 30 saniye kaldıktan sonra uygulamayı başlattığınızda veya devam ettirdiğinizde kimlik doğrulaması yapmanız gerekecektir."; /* No comment provided by engineer. */ -"You will connect to all group members." = "Bütün grup üyelerine bağlanacaksın."; +"You will still receive calls and notifications from muted profiles when they are active." = "Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz."; /* No comment provided by engineer. */ -"You will still receive calls and notifications from muted profiles when they are active." = "Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz."; +"You will stop receiving messages from this chat. Chat history will be preserved." = "Bu sohbetten mesaj almaya son vereceksiniz. Sohbet geçmişi korunacaktır."; /* 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."; @@ -5207,6 +6252,9 @@ /* 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 business contact" = "İş bağlantınız"; + /* No comment provided by engineer. */ "Your calls" = "Aramaların"; @@ -5222,8 +6270,14 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Sohbet profillerin"; +/* alert message */ +"Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Sohbetiniz %@'ya taşındı ancak sizi profile yönlendirirken beklenmedik bir hata oluştu."; + /* No comment provided by engineer. */ -"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Bağlantınız %@ adresine taşındı ancak sizi profile yönlendirirken beklenmedik bir hata oluştu."; +"Your connection was moved to %@ but an error happened when switching 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" = "İrtibat kişiniz"; /* 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."; @@ -5243,6 +6297,9 @@ /* No comment provided by engineer. */ "Your current profile" = "Mevcut profiliniz"; +/* No comment provided by engineer. */ +"Your group" = "Grubunuz"; + /* No comment provided by engineer. */ "Your ICE servers" = "ICE sunucularınız"; @@ -5258,27 +6315,27 @@ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Profiliniz **%@** paylaşılacaktır."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. SimpleX sunucuları profilinizi göremez."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Profiliniz değiştirildi. Kaydederseniz, güncellenmiş profil tüm kişilerinize gönderilecektir."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Profiliniz, kişileriniz ve gönderilmiş mesajlar cihazınızda saklanır."; - /* No comment provided by engineer. */ "Your random profile" = "Rasgele profiliniz"; /* No comment provided by engineer. */ "Your server address" = "Sunucu adresiniz"; +/* No comment provided by engineer. */ +"Your servers" = "Sunucularınız"; + /* No comment provided by engineer. */ "Your settings" = "Ayarlarınız"; /* No comment provided by engineer. */ "Your SimpleX address" = "SimpleX adresin"; -/* No comment provided by engineer. */ -"Your SMP servers" = "SMP sunucularınız"; - diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index ce8184272d..fe8cfe22a0 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (можна скопіювати)"; @@ -22,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)"; @@ -199,6 +178,9 @@ /* time interval */ "%d sec" = "%d сек"; +/* delete after time */ +"%d seconds(s)" = "%d секунд(и)"; + /* integrity error chat item */ "%d skipped message(s)" = "%d пропущено повідомлення(ь)"; @@ -241,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 секунд"; @@ -289,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0с"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 день"; /* time interval */ @@ -298,12 +278,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 тиждень"; +/* delete after time */ +"1 year" = "1 рік"; + /* No comment provided by engineer. */ "1-time link" = "Одноразове посилання"; @@ -343,6 +328,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Скасувати зміну адреси?"; +/* No comment provided by engineer. */ +"About operators" = "Про операторів"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "Про чат SimpleX"; @@ -353,41 +341,75 @@ "Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +swipe action */ "Accept" = "Прийняти"; +/* alert action */ +"Accept as member" = "Прийняти як учасника"; + +/* alert action */ +"Accept as observer" = "Прийняти як спостерігача"; + /* No comment provided by engineer. */ "Accept conditions" = "Прийняти умови"; /* No comment provided by engineer. */ "Accept connection request?" = "Прийняти запит на підключення?"; +/* alert title */ +"Accept contact request" = "Прийняти запит на контакт"; + /* notification body */ "Accept contact request from %@?" = "Прийняти запит на контакт від %@?"; -/* accept contact request via notification - swipe action */ +/* alert action +swipe action */ "Accept incognito" = "Прийняти інкогніто"; +/* alert title */ +"Accept member" = "Прийняти учасника"; + +/* rcv group event chat item */ +"accepted %@" = "прийнято %@"; + /* call status */ "accepted call" = "прийнято виклик"; /* No comment provided by engineer. */ "Accepted conditions" = "Прийняті умови"; +/* chat list item title */ +"accepted invitation" = "прийняте запрошення"; + +/* rcv group event chat item */ +"accepted you" = "прийняв(ла) вас"; + /* 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 friends" = "Додайте друзів"; + +/* No comment provided by engineer. */ +"Add list" = "Додати список"; + +/* placeholder for sending contact request */ +"Add message" = "Додати повідомлення"; + /* No comment provided by engineer. */ "Add profile" = "Додати профіль"; @@ -397,12 +419,21 @@ /* 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" = "Додано медіа та файлові сервери"; @@ -454,12 +485,21 @@ /* chat item text */ "agreeing encryption…" = "узгодження шифрування…"; +/* member criteria value */ +"all" = "усі"; + +/* 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." = "Всі дані стираються при введенні."; @@ -487,6 +527,12 @@ /* 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." = "Всі ваші контакти залишаться на зв'язку."; @@ -532,6 +578,9 @@ /* 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." = "Дозволяє надсилати файли та медіа."; @@ -565,10 +614,10 @@ /* No comment provided by engineer. */ "Already connected?" = "Вже підключено?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Вже підключаємось!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Вже приєднуємося до групи!"; /* pref value */ @@ -586,6 +635,9 @@ /* No comment provided by engineer. */ "and %lld other events" = "та %lld інших подій"; +/* report reason */ +"Another reason" = "Інша причина"; + /* No comment provided by engineer. */ "Answer call" = "Відповісти на дзвінок"; @@ -601,6 +653,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" = "Іконка програми"; @@ -628,15 +683,36 @@ /* 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" = "Архівування бази даних"; @@ -685,9 +761,6 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Автоматичне прийняття зображень"; -/* alert title */ -"Auto-accept settings" = "Автоприйняття налаштувань"; - /* No comment provided by engineer. */ "Back" = "Назад"; @@ -715,6 +788,9 @@ /* No comment provided by engineer. */ "Better groups" = "Кращі групи"; +/* No comment provided by engineer. */ +"Better groups performance" = "Краща продуктивність груп"; + /* No comment provided by engineer. */ "Better message dates." = "Кращі дати повідомлень."; @@ -727,12 +803,21 @@ /* 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. */ +"Bio" = "Біо"; + +/* alert title */ +"Bio too large" = "Біографія занадто велика"; + /* No comment provided by engineer. */ "Black" = "Чорний"; @@ -760,7 +845,8 @@ /* 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. */ @@ -793,9 +879,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. */ +"Business connection" = "Бізнес-зв'язок"; + +/* 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" = "дзвонити"; @@ -826,6 +927,9 @@ /* No comment provided by engineer. */ "Can't call member" = "Не вдається зателефонувати користувачеві"; +/* alert title */ +"Can't change profile" = "Не вдається змінити профіль"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Не вдається запросити контакт!"; @@ -835,8 +939,12 @@ /* No comment provided by engineer. */ "Can't message member" = "Не можу надіслати повідомлення користувачеві"; +/* No comment provided by engineer. */ +"can't send messages" = "не можна надсилати"; + /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "Скасувати"; /* No comment provided by engineer. */ @@ -863,6 +971,9 @@ /* No comment provided by engineer. */ "Change" = "Зміна"; +/* alert title */ +"Change automatic message deletion?" = "Змінити автоматичне видалення повідомлень?"; + /* authentication reason */ "Change chat profiles" = "Зміна профілів користувачів"; @@ -876,7 +987,7 @@ "Change member role?" = "Змінити роль учасника?"; /* authentication reason */ -"Change passcode" = "Змінити пароль"; +"Change passcode" = "Змінити код доступу"; /* No comment provided by engineer. */ "Change receiving address" = "Змінити адресу отримання"; @@ -891,7 +1002,7 @@ "Change self-destruct mode" = "Змінити режим самознищення"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Змінити пароль самознищення"; /* chat item text */ @@ -909,6 +1020,15 @@ /* chat item text */ "changing address…" = "змінює адресу…"; +/* No comment provided by engineer. */ +"Chat" = "Чат"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Чат вже існує"; + +/* new chat sheet title */ +"Chat already exists!" = "Чат вже існує!"; + /* No comment provided by engineer. */ "Chat colors" = "Кольори чату"; @@ -954,9 +1074,27 @@ /* 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!" = "Чат буде видалено для вас - цю дію неможливо скасувати!"; + +/* chat toolbar */ +"Chat with admins" = "Чат з адміністраторами"; + +/* No comment provided by engineer. */ +"Chat with member" = "Чат з учасником"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Спілкуйтеся з учасниками до того, як вони приєднаються."; + /* No comment provided by engineer. */ "Chats" = "Чати"; +/* No comment provided by engineer. */ +"Chats with members" = "Чати з учасниками"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Перевіряйте повідомлення кожні 20 хв."; @@ -996,6 +1134,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?" = "Чисті приватні нотатки?"; @@ -1011,6 +1155,9 @@ /* No comment provided by engineer. */ "colored" = "кольоровий"; +/* report reason */ +"Community guidelines violation" = "Порушення правил спільноти"; + /* server test step */ "Compare file" = "Порівняти файл"; @@ -1032,15 +1179,9 @@ /* No comment provided by engineer. */ "Conditions are already accepted for these operator(s): **%@**." = "Умови вже прийняті для наступних операторів: **%@**."; -/* No comment provided by engineer. */ +/* alert button */ "Conditions of use" = "Умови використання"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Умови будуть прийняті для ввімкнених операторів через 30 днів."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for operator(s): **%@**." = "Умови приймаються для оператора(ів): **%@**."; - /* No comment provided by engineer. */ "Conditions will be accepted for the operator(s): **%@**." = "Для оператора(ів) приймаються умови: **%@**."; @@ -1053,6 +1194,9 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Налаштування серверів ICE"; +/* No comment provided by engineer. */ +"Configure server operators" = "Налаштувати операторів сервера"; + /* No comment provided by engineer. */ "Confirm" = "Підтвердити"; @@ -1083,6 +1227,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Підтвердити завантаження"; +/* token status text */ +"Confirmed" = "Підтверджений"; + /* server test step */ "Connect" = "Підключіться"; @@ -1090,7 +1237,7 @@ "Connect automatically" = "Підключення автоматично"; /* No comment provided by engineer. */ -"Connect incognito" = "Підключайтеся інкогніто"; +"Connect faster! 🚀" = "Підключайтеся швидше! 🚀"; /* No comment provided by engineer. */ "Connect to desktop" = "Підключення до комп'ютера"; @@ -1101,25 +1248,22 @@ /* No comment provided by engineer. */ "Connect to your friends faster." = "Швидше спілкуйтеся з друзями."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "З'єднатися з самим собою?"; +/* new chat sheet title */ +"Connect to yourself?\nThis is your own one-time link!" = "Підключитися до себе?\nЦе ваше власне одноразове посилання!"; -/* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own one-time link!" = "Підключитися до себе? \nЦе ваше власне одноразове посилання!"; +/* new chat sheet title */ +"Connect to yourself?\nThis is your own SimpleX address!" = "З'єднатися з самим собою?\nЦе ваша власна SimpleX-адреса!"; -/* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own SimpleX address!" = "З'єднатися з самим собою? \nЦе ваша власна SimpleX-адреса!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Підключіться за контактною адресою"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Підключіться за посиланням"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Під'єднатися за одноразовим посиланням"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Підключитися до %@"; /* No comment provided by engineer. */ @@ -1131,9 +1275,6 @@ /* No comment provided by engineer. */ "Connected desktop" = "Підключений робочий стіл"; -/* rcv group event chat item */ -"connected directly" = "з'єднані безпосередньо"; - /* No comment provided by engineer. */ "Connected servers" = "Підключені сервери"; @@ -1183,6 +1324,9 @@ "Connection and servers status." = "Стан з'єднання та серверів."; /* No comment provided by engineer. */ +"Connection blocked" = "Підключення заблоковано"; + +/* alert title */ "Connection error" = "Помилка підключення"; /* No comment provided by engineer. */ @@ -1191,19 +1335,28 @@ /* 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. */ +/* alert title */ "Connection timeout" = "Тайм-аут з'єднання"; /* No comment provided by engineer. */ @@ -1224,9 +1377,15 @@ /* No comment provided by engineer. */ "Contact already exists" = "Контакт вже існує"; +/* No comment provided by engineer. */ +"contact deleted" = "контакт видалено"; + /* No comment provided by engineer. */ "Contact deleted!" = "Контакт видалено!"; +/* No comment provided by engineer. */ +"contact disabled" = "контакт вимкнено"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "контакт має шифрування e2e"; @@ -1245,9 +1404,15 @@ /* No comment provided by engineer. */ "Contact name" = "Ім'я контактної особи"; +/* No comment provided by engineer. */ +"contact not ready" = "контакт не готовий"; + /* No comment provided by engineer. */ "Contact preferences" = "Налаштування контактів"; +/* No comment provided by engineer. */ +"contact should accept…" = "контакт повинен прийняти…"; + /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "Контакт буде видалено - це неможливо скасувати!"; @@ -1257,6 +1422,9 @@ /* 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" = "Продовжуйте"; @@ -1299,6 +1467,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/). 💻" = "Створіть новий профіль у [desktop app](https://simplex.chat/downloads/). 💻"; @@ -1309,10 +1480,10 @@ "Create queue" = "Створити чергу"; /* No comment provided by engineer. */ -"Create secret group" = "Створити секретну групу"; +"Create SimpleX address" = "Створіть адресу SimpleX"; /* No comment provided by engineer. */ -"Create SimpleX address" = "Створіть адресу SimpleX"; +"Create your address" = "Створіть свою адресу"; /* No comment provided by engineer. */ "Create your profile" = "Створіть свій профіль"; @@ -1440,7 +1611,8 @@ /* No comment provided by engineer. */ "decryption errors" = "помилки розшифровки"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "за замовчуванням (%@)"; /* No comment provided by engineer. */ @@ -1450,8 +1622,7 @@ "default (yes)" = "за замовчуванням (так)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "Видалити"; /* No comment provided by engineer. */ @@ -1475,12 +1646,24 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Видалити та повідомити контакт"; +/* No comment provided by engineer. */ +"Delete chat" = "Видалити чат"; + +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Видалити повідомлення чату з вашого пристрою."; + /* No comment provided by engineer. */ "Delete chat profile" = "Видалити профіль чату"; /* No comment provided by engineer. */ "Delete chat profile?" = "Видалити профіль чату?"; +/* alert title */ +"Delete chat with member?" = "Видалити чат з учасником?"; + +/* No comment provided by engineer. */ +"Delete chat?" = "Видалити чат?"; + /* No comment provided by engineer. */ "Delete connection" = "Видалити підключення"; @@ -1526,13 +1709,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. */ @@ -1556,6 +1742,9 @@ /* 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 повідомлень одночасно."; @@ -1601,6 +1790,9 @@ /* No comment provided by engineer. */ "Description" = "Опис"; +/* alert title */ +"Description too large" = "Опис занадто великий"; + /* No comment provided by engineer. */ "Desktop address" = "Адреса робочого столу"; @@ -1655,12 +1847,21 @@ /* chat feature */ "Direct messages" = "Прямі повідомлення"; +/* No comment provided by engineer. */ +"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" = "Вимкнути для всіх"; @@ -1721,6 +1922,9 @@ /* 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" = "Не створювати адресу"; @@ -1728,13 +1932,19 @@ "Don't enable" = "Не вмикати"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Не пропускайте важливі повідомлення."; + +/* alert action */ "Don't show again" = "Більше не показувати"; +/* No comment provided by engineer. */ +"Done" = "Готово"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Пониження та відкритий чат"; /* alert button - chat item action */ +chat item action */ "Download" = "Завантажити"; /* No comment provided by engineer. */ @@ -1785,20 +1995,26 @@ /* No comment provided by engineer. */ "Edit group profile" = "Редагування профілю групи"; +/* No comment provided by engineer. */ +"Empty message!" = "Порожнє повідомлення!"; + /* No comment provided by engineer. */ "Enable" = "Увімкнути"; /* 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" = "Увімкнути Flux"; +"Enable disappearing messages by default." = "Увімкнути зникаючі повідомлення за замовчуванням."; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих."; /* No comment provided by engineer. */ "Enable for all" = "Увімкнути для всіх"; @@ -1911,6 +2127,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "для %@ потрібне повторне узгодження шифрування"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Виконується повторне узгодження шифрування."; + /* No comment provided by engineer. */ "ended" = "закінчився"; @@ -1965,28 +2184,40 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Помилка при прийнятті запиту на контакт"; +/* alert title */ +"Error accepting member" = "Помилка при прийомі учасника"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Помилка додавання користувача(ів)"; /* alert title */ "Error adding server" = "Помилка додавання сервера"; +/* No comment provided by engineer. */ +"Error adding short link" = "Помилка додавання короткого посилання"; + /* No comment provided by engineer. */ "Error changing address" = "Помилка зміни адреси"; +/* alert title */ +"Error changing chat profile" = "Помилка зміни профілю чату"; + /* No comment provided by engineer. */ "Error changing connection profile" = "Помилка при зміні профілю з'єднання"; /* No comment provided by engineer. */ "Error changing role" = "Помилка зміни ролі"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Помилка зміни налаштування"; /* No comment provided by engineer. */ "Error changing to incognito!" = "Помилка переходу на інкогніто!"; /* No comment provided by engineer. */ +"Error checking token status" = "Помилка перевірки статусу токена"; + +/* alert message */ "Error connecting to forwarding server %@. Please try later." = "Помилка підключення до сервера переадресації %@. Спробуйте пізніше."; /* No comment provided by engineer. */ @@ -1998,6 +2229,9 @@ /* No comment provided by engineer. */ "Error creating group link" = "Помилка створення посилання на групу"; +/* alert title */ +"Error creating list" = "Помилка при створенні списку"; + /* No comment provided by engineer. */ "Error creating member contact" = "Помилка при створенні контакту користувача"; @@ -2007,22 +2241,28 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Помилка створення профілю!"; +/* No comment provided by engineer. */ +"Error creating report" = "Помилка при створенні скарги"; + /* No comment provided by engineer. */ "Error decrypting file" = "Помилка розшифрування файлу"; -/* No comment provided by engineer. */ +/* alert title */ +"Error deleting chat" = "Помилка при видаленні чату з учасником"; + +/* alert title */ "Error deleting chat database" = "Помилка видалення бази даних чату"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Помилка видалення чату!"; /* No comment provided by engineer. */ "Error deleting connection" = "Помилка видалення з'єднання"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Помилка видалення бази даних"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Помилка видалення старої бази даних"; /* No comment provided by engineer. */ @@ -2043,13 +2283,13 @@ /* No comment provided by engineer. */ "Error encrypting database" = "Помилка шифрування бази даних"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Помилка експорту бази даних чату"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "Помилка експорту теми: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Помилка імпорту бази даних чату"; /* No comment provided by engineer. */ @@ -2064,6 +2304,9 @@ /* No comment provided by engineer. */ "Error opening chat" = "Помилка відкриття чату"; +/* No comment provided by engineer. */ +"Error opening group" = "Помилка відкриття групи"; + /* alert title */ "Error receiving file" = "Помилка отримання файлу"; @@ -2073,12 +2316,24 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Помилка перепідключення серверів"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "Помилка під час реєстрації для отримання сповіщень"; + +/* alert title */ +"Error rejecting contact request" = "Помилка відхилення запиту на контакт"; + +/* alert title */ "Error removing member" = "Помилка видалення учасника"; +/* 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" = "Помилка збереження профілю групи"; @@ -2121,7 +2376,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Помилка зупинки чату"; -/* No comment provided by engineer. */ +/* alert title */ "Error switching profile" = "Помилка перемикання профілю"; /* alertTitle */ @@ -2130,6 +2385,9 @@ /* 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" = "Помилка оновлення посилання на групу"; @@ -2154,7 +2412,9 @@ /* No comment provided by engineer. */ "Error: " = "Помилка: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Помилка: %@"; /* No comment provided by engineer. */ @@ -2172,9 +2432,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" = "Вихід без збереження"; @@ -2184,6 +2441,9 @@ /* No comment provided by engineer. */ "expired" = "закінчився"; +/* token status text */ +"Expired" = "Термін дії закінчився"; + /* No comment provided by engineer. */ "Export database" = "Експорт бази даних"; @@ -2208,18 +2468,30 @@ /* 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." = "Файл не знайдено - найімовірніше, файл було видалено або скасовано."; @@ -2277,6 +2549,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "Швидше знаходьте чати"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Відбиток в адресі сервера не співпадає з сертифікатом."; + /* No comment provided by engineer. */ "Fix" = "Виправити"; @@ -2296,7 +2571,7 @@ "Fix not supported by group member" = "Виправлення не підтримується учасником групи"; /* No comment provided by engineer. */ -"for better metadata privacy." = "для кращої конфіденційності метаданих."; +"For all moderators" = "Для всіх модераторів"; /* servers error */ "For chat profile %@:" = "Для профілю чату %@:"; @@ -2307,6 +2582,9 @@ /* 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" = "Для приватної маршрутизації"; @@ -2343,8 +2621,8 @@ /* 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." = "Серверу переадресації %@ не вдалося з'єднатися з сервером призначення %@. Спробуйте пізніше."; +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Серверу переадресації %1$@ не вдалося з'єднатися з сервером призначення %2$@. Спробуйте пізніше."; /* No comment provided by engineer. */ "Forwarding server address is incompatible with network settings: %@." = "Адреса сервера переадресації несумісна з налаштуваннями мережі: %@."; @@ -2379,6 +2657,9 @@ /* 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-файли та наклейки"; @@ -2388,13 +2669,16 @@ /* message preview */ "Good morning!" = "Доброго ранку!"; +/* shown on group welcome message */ +"group" = "група"; + /* No comment provided by engineer. */ "Group" = "Група"; /* No comment provided by engineer. */ "Group already exists" = "Група вже існує"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Група вже існує!"; /* No comment provided by engineer. */ @@ -2418,6 +2702,9 @@ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Групове запрошення більше не дійсне, воно було видалено відправником."; +/* No comment provided by engineer. */ +"group is deleted" = "групу видалено"; + /* No comment provided by engineer. */ "Group link" = "Посилання на групу"; @@ -2442,6 +2729,9 @@ /* snd group event chat item */ "group profile updated" = "оновлено профіль групи"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "Профіль групи було змінено. Якщо ви збережете його, оновлений профіль буде надіслано учасникам групи."; + /* No comment provided by engineer. */ "Group welcome message" = "Привітальне повідомлення групи"; @@ -2451,9 +2741,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" = "Приховано"; @@ -2490,6 +2786,9 @@ /* No comment provided by engineer. */ "How it helps privacy" = "Як це захищає приватність"; +/* alert button */ +"How it works" = "Як це працює"; + /* No comment provided by engineer. */ "How SimpleX works" = "Як працює SimpleX"; @@ -2577,6 +2876,12 @@ /* No comment provided by engineer. */ "inactive" = "неактивний"; +/* report reason */ +"Inappropriate content" = "Невідповідний вміст"; + +/* report reason */ +"Inappropriate profile" = "Невідповідний профіль"; + /* No comment provided by engineer. */ "Incognito" = "Інкогніто"; @@ -2643,6 +2948,21 @@ /* No comment provided by engineer. */ "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" = "недійсний чат"; @@ -2658,7 +2978,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "Неправильне ім'я користувача!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "Невірне посилання"; /* No comment provided by engineer. */ @@ -2694,6 +3014,9 @@ /* No comment provided by engineer. */ "Invite members" = "Запросити учасників"; +/* No comment provided by engineer. */ +"Invite to chat" = "Запросити в чат"; + /* No comment provided by engineer. */ "Invite to group" = "Запросити до групи"; @@ -2755,24 +3078,18 @@ "Join" = "Приєднуйтесь"; /* No comment provided by engineer. */ -"join as %@" = "приєднатися як %@"; +"Join as %@" = "приєднатися як %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Приєднуйтесь до групи"; /* No comment provided by engineer. */ "Join group conversations" = "Приєднуйтесь до групових розмов"; -/* No comment provided by engineer. */ -"Join group?" = "Приєднатися до групи?"; - /* No comment provided by engineer. */ "Join incognito" = "Приєднуйтесь інкогніто"; -/* No comment provided by engineer. */ -"Join with current profile" = "Приєднатися з поточним профілем"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Приєднатися до групи?\nЦе ваше посилання на групу %@!"; /* No comment provided by engineer. */ @@ -2790,6 +3107,9 @@ /* alert title */ "Keep unused invitation?" = "Зберігати невикористані запрошення?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "Підтримуйте чистоту в чатах"; + /* No comment provided by engineer. */ "Keep your connections" = "Зберігайте свої зв'язки"; @@ -2808,6 +3128,12 @@ /* swipe action */ "Leave" = "Залишити"; +/* No comment provided by engineer. */ +"Leave chat" = "Вийти з чату"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Залишити чат?"; + /* No comment provided by engineer. */ "Leave group" = "Покинути групу"; @@ -2817,6 +3143,9 @@ /* rcv group event chat item */ "left" = "ліворуч"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Менше трафіку в мобільних мережах."; + /* email subject */ "Let's talk in SimpleX Chat" = "Поговоримо в чаті SimpleX"; @@ -2835,6 +3164,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" = "НАЖИВО"; @@ -2844,6 +3182,9 @@ /* No comment provided by engineer. */ "Live messages" = "Живі повідомлення"; +/* in progress text */ +"Loading profile…" = "Завантаження профілю…"; + /* No comment provided by engineer. */ "Local name" = "Місцева назва"; @@ -2898,27 +3239,48 @@ /* profile update event chat item */ "member %@ changed to %@" = "учасника %1$@ змінено на %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Прийом членів"; + /* rcv group event chat item */ "member connected" = "з'єднаний"; +/* No comment provided by engineer. */ +"member has old version" = "учасник використовує застарілу версію"; + /* 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!" = "Учасник буде видалений з групи - це неможливо скасувати!"; +/* alert message */ +"Member will join the group, accept member?" = "Учасник приєднається до групи, прийняти учасника?"; + /* 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 direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; @@ -2934,6 +3296,9 @@ /* No comment provided by engineer. */ "Members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; +/* No comment provided by engineer. */ +"Mention members 👋" = "Згадуйте учасників 👋"; + /* No comment provided by engineer. */ "Menus" = "Меню"; @@ -2955,6 +3320,9 @@ /* item status text */ "Message forwarded" = "Повідомлення переслано"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Миттєве повідомлення, щойно ви натиснете \"Підключитися\"."; + /* item status description */ "Message may be delivered later if member becomes active." = "Повідомлення може бути доставлене пізніше, якщо користувач стане активним."; @@ -3003,9 +3371,15 @@ /* No comment provided by engineer. */ "Messages & files" = "Повідомлення та файли"; +/* No comment provided by engineer. */ +"Messages are protected by **end-to-end encryption**." = "Повідомлення захищені **наскрізним шифруванням**."; + /* 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" = "Отримані повідомлення"; @@ -3078,9 +3452,15 @@ /* 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!" = "Незабаром буде ще більше покращень!"; @@ -3096,12 +3476,12 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "Кілька профілів чату"; -/* No comment provided by engineer. */ -"mute" = "приглушити"; - -/* swipe action */ +/* notification label action */ "Mute" = "Вимкнути звук"; +/* notification label action */ +"Mute all" = "Вимкнути звук для всіх"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Вимкнено, коли неактивний!"; @@ -3129,12 +3509,15 @@ /* No comment provided by engineer. */ "Network settings" = "Налаштування мережі"; -/* No comment provided by engineer. */ +/* alert title */ "Network status" = "Стан мережі"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "ніколи"; +/* token status text */ +"New" = "Новий"; + /* No comment provided by engineer. */ "New chat" = "Новий чат"; @@ -3156,6 +3539,9 @@ /* notification */ "New events" = "Нові події"; +/* No comment provided by engineer. */ +"New group role: Moderator" = "Нова роль у групі: Модератор"; + /* No comment provided by engineer. */ "New in %@" = "Нове в %@"; @@ -3165,6 +3551,9 @@ /* No comment provided by engineer. */ "New member role" = "Нова роль учасника"; +/* rcv group event chat item */ +"New member wants to join the group." = "Новий учасник хоче приєднатися до групи."; + /* notification */ "new message" = "нове повідомлення"; @@ -3195,6 +3584,18 @@ /* 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 chats with members" = "Ніяких чатів з учасниками"; + /* No comment provided by engineer. */ "No contacts selected" = "Не вибрано жодного контакту"; @@ -3228,6 +3629,9 @@ /* servers error */ "No media & file servers." = "Ніяких медіа та файлових серверів."; +/* No comment provided by engineer. */ +"No message" = "Немає повідомлення"; + /* servers error */ "No message servers." = "Ніяких серверів повідомлень."; @@ -3243,6 +3647,9 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Немає дозволу на запис голосового повідомлення"; +/* alert title */ +"No private routing session" = "Немає приватного сеансу маршрутизації"; + /* No comment provided by engineer. */ "No push server" = "Локально"; @@ -3264,12 +3671,24 @@ /* 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. */ +"not synchronized" = "не синхронізовано"; + +/* No comment provided by engineer. */ +"Notes" = "Нотатки"; + /* No comment provided by engineer. */ "Nothing selected" = "Нічого не вибрано"; @@ -3282,9 +3701,15 @@ /* 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- відключати користувачів (роль \"спостерігач\")"; @@ -3292,8 +3717,9 @@ "observer" = "спостерігач"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "вимкнено"; /* blur media */ @@ -3305,7 +3731,9 @@ /* feature offered item */ "offered %@: %@" = "запропонував %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Гаразд"; /* No comment provided by engineer. */ @@ -3329,6 +3757,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion хости не будуть використовуватися."; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Лише власники чату можуть змінювати налаштування."; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**."; @@ -3344,6 +3775,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." = "Тільки ви можете додавати реакції на повідомлення."; @@ -3374,13 +3811,13 @@ /* 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. */ +/* new chat action */ "Open chat" = "Відкритий чат"; /* authentication reason */ @@ -3389,15 +3826,33 @@ /* No comment provided by engineer. */ "Open conditions" = "Відкриті умови"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Відкрита група"; +/* alert title */ +"Open link?" = "Відкрите посилання?"; + /* authentication reason */ "Open migration to another device" = "Відкрита міграція на інший пристрій"; +/* new chat action */ +"Open new chat" = "Відкрити новий чат"; + +/* new chat action */ +"Open new group" = "Відкрити нову групу"; + /* No comment provided by engineer. */ "Open Settings" = "Відкрийте Налаштування"; +/* No comment provided by engineer. */ +"Open to accept" = "Відкрити для прийняття"; + +/* No comment provided by engineer. */ +"Open to connect" = "Відкрито для підключення"; + +/* No comment provided by engineer. */ +"Open to join" = "Відкрито для приєднання"; + /* No comment provided by engineer. */ "Opening app…" = "Відкриваємо програму…"; @@ -3407,6 +3862,9 @@ /* alert title */ "Operator server" = "Сервер оператора"; +/* No comment provided by engineer. */ +"Or import archive file" = "Або імпортуйте архівний файл"; + /* No comment provided by engineer. */ "Or paste archive link" = "Або вставте посилання на архів"; @@ -3422,6 +3880,9 @@ /* No comment provided by engineer. */ "Or to share privately" = "Або поділитися приватно"; +/* No comment provided by engineer. */ +"Organize chats into lists" = "Організовуйте чати в списки"; + /* No comment provided by engineer. */ "other" = "інший"; @@ -3461,9 +3922,6 @@ /* No comment provided by engineer. */ "Password to show" = "Показати пароль"; -/* past/unknown group member */ -"Past member %@" = "Колишній учасник %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Вставте адресу робочого столу"; @@ -3479,9 +3937,18 @@ /* No comment provided by engineer. */ "peer-to-peer" = "одноранговий"; +/* No comment provided by engineer. */ +"pending" = "очікує"; + /* No comment provided by engineer. */ "Pending" = "В очікуванні"; +/* No comment provided by engineer. */ +"pending approval" = "очікує на схвалення"; + +/* No comment provided by engineer. */ +"pending review" = "очікує на схвалення"; + /* No comment provided by engineer. */ "Periodic" = "Періодично"; @@ -3512,7 +3979,7 @@ /* 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. */ +/* alert message */ "Please check your network connection with %@ and try again." = "Будь ласка, перевірте підключення до мережі за допомогою %@ і спробуйте ще раз."; /* No comment provided by engineer. */ @@ -3548,15 +4015,24 @@ /* 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." = "Будь ласка, спробуйте вимкнути та знову увімкнути сповіщення."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Будь ласка, зачекайте, поки модератори групи розглянуть ваш запит на приєднання до групи."; + +/* 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" = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Зберегти чернетку останнього повідомлення з вкладеннями."; @@ -3575,12 +4051,24 @@ /* 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" = "Маршрутизація приватних повідомлень"; @@ -3593,9 +4081,12 @@ /* No comment provided by engineer. */ "Private routing" = "Приватна маршрутизація"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "Помилка приватної маршрутизації"; +/* alert title */ +"Private routing timeout" = "Тайм-аут приватної маршрутизації"; + /* No comment provided by engineer. */ "Profile and server connections" = "З'єднання профілю та сервера"; @@ -3626,6 +4117,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Заборонити реакції на повідомлення."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Заборонити повідомлення модераторам."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Заборонити надсилати прямі повідомлення учасникам."; @@ -3653,6 +4147,9 @@ /* 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 background timeout" = "Фоновий тайм-аут протоколу"; + /* No comment provided by engineer. */ "Protocol timeout" = "Тайм-аут протоколу"; @@ -3725,9 +4222,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "отримали підтвердження…"; -/* notification */ -"Received file event" = "Подія отримання файлу"; - /* message info title */ "Received message" = "Отримано повідомлення"; @@ -3788,16 +4282,32 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Зменшення використання акумулятора"; -/* reject incoming call via notification - swipe action */ +/* No comment provided by engineer. */ +"Register" = "Зареєструватися"; + +/* token info */ +"Register notification token?" = "Зареєструвати токен сповіщення?"; + +/* token status text */ +"Registered" = "Зареєстровано"; + +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "Відхилити"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Відхилити (відправника НЕ повідомлено)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "Відхилити запит на контакт"; +/* alert title */ +"Reject member?" = "Відхилити учасника?"; + +/* No comment provided by engineer. */ +"rejected" = "відхилено"; + /* call status */ "rejected call" = "відхилений виклик"; @@ -3834,12 +4344,18 @@ /* profile update event chat item */ "removed contact address" = "видалено контактну адресу"; +/* No comment provided by engineer. */ +"removed from group" = "видалено з групи"; + /* profile update event chat item */ "removed profile picture" = "видалено зображення профілю"; /* rcv group event chat item */ "removed you" = "прибрали вас"; +/* No comment provided by engineer. */ +"Removes messages and blocks members." = "Видаляє повідомлення та блокує користувачів."; + /* No comment provided by engineer. */ "Renegotiate" = "Переузгодьте"; @@ -3849,24 +4365,60 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Переузгодьте шифрування?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Повторити запит на підключення?"; - /* No comment provided by engineer. */ "Repeat download" = "Повторити завантаження"; /* No comment provided by engineer. */ "Repeat import" = "Повторний імпорт"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Повторити запит на приєднання?"; - /* No comment provided by engineer. */ "Repeat upload" = "Повторне завантаження"; /* chat item action */ "Reply" = "Відповісти"; +/* chat 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?" = "Причина повідомлення?"; + +/* alert title */ +"Report sent to moderators" = "Повідомлення надіслано модераторам"; + +/* 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" = "Звіти"; + +/* No comment provided by engineer. */ +"request is sent" = "запит відправлено"; + +/* No comment provided by engineer. */ +"request to join rejected" = "запит на приєднання відхилено"; + +/* chat list item title */ +"requested to connect" = "запит на підключення"; + /* No comment provided by engineer. */ "Required" = "Потрібно"; @@ -3912,17 +4464,29 @@ /* No comment provided by engineer. */ "Restore database error" = "Відновлення помилки бази даних"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "Спробуйте ще раз"; /* chat item action */ "Reveal" = "Показувати"; +/* No comment provided by engineer. */ +"review" = "перегляд"; + /* No comment provided by engineer. */ "Review conditions" = "Умови перегляду"; /* No comment provided by engineer. */ -"Review later" = "Перегляньте пізніше"; +"Review group members" = "Учасники групи оглядів"; + +/* admission stage */ +"Review members" = "Схвалювати учасників"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Перевірка учасників перед тим, як їх прийняти («стукіт»)."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "схвалено адміністраторами"; /* No comment provided by engineer. */ "Revoke" = "Відкликати"; @@ -3946,12 +4510,18 @@ "Safer groups" = "Безпечніші групи"; /* alert button - chat item action */ +chat item action */ "Save" = "Зберегти"; /* alert button */ "Save (and notify contacts)" = "Зберегти (і повідомити контактам)"; +/* alert button */ +"Save (and notify members)" = "Зберегти (і повідомити учасникам)"; + +/* alert title */ +"Save admission settings?" = "Зберегти налаштування входу?"; + /* alert button */ "Save and notify contact" = "Зберегти та повідомити контакт"; @@ -3967,6 +4537,12 @@ /* No comment provided by engineer. */ "Save group profile" = "Зберегти профіль групи"; +/* alert title */ +"Save group profile?" = "Зберегти профіль групи?"; + +/* No comment provided by engineer. */ +"Save list" = "Зберегти список"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Збережіть пароль і відкрийте чат"; @@ -4103,10 +4679,10 @@ "Send a live message - it will update for the recipient(s) as you type it" = "Надішліть повідомлення в реальному часі - воно буде оновлюватися для одержувача (одержувачів), поки ви його вводите"; /* No comment provided by engineer. */ -"Send delivery receipts to" = "Надсилання звітів про доставку"; +"Send contact request?" = "Надіслати запит на контакт?"; /* No comment provided by engineer. */ -"send direct message" = "надіслати пряме повідомлення"; +"Send delivery receipts to" = "Надсилання звітів про доставку"; /* No comment provided by engineer. */ "Send direct message to connect" = "Надішліть пряме повідомлення, щоб підключитися"; @@ -4135,18 +4711,30 @@ /* No comment provided by engineer. */ "Send notifications" = "Надсилати сповіщення"; +/* No comment provided by engineer. */ +"Send private reports" = "Надсилайте приватні звіти"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Надсилайте запитання та ідеї"; /* No comment provided by engineer. */ "Send receipts" = "Надіслати підтвердження"; +/* No comment provided by engineer. */ +"Send request" = "Надіслати запит"; + +/* No comment provided by engineer. */ +"Send request without message" = "Надіслати запит без повідомлення"; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Надсилайте їх із галереї чи власних клавіатур."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Надішліть до 100 останніх повідомлень новим користувачам."; +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Надсилайте свої приватні відгуки до груп."; + /* alert message */ "Sender cancelled file transfer." = "Відправник скасував передачу файлу."; @@ -4186,9 +4774,6 @@ /* No comment provided by engineer. */ "Sent directly" = "Відправлено напряму"; -/* notification */ -"Sent file event" = "Подія надісланого файлу"; - /* message info title */ "Sent message" = "Надіслано повідомлення"; @@ -4235,10 +4820,10 @@ "server queue info: %@\n\nlast received msg: %@" = "інформація про чергу на сервері: %1$@\n\nостаннє отримане повідомлення: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Сервер вимагає авторизації для створення черг, перевірте пароль"; +"Server requires authorization to create queues, check password." = "Сервер вимагає авторизації для створення черг, перевірте пароль."; /* server test error */ -"Server requires authorization to upload, check password" = "Сервер вимагає авторизації для завантаження, перевірте пароль"; +"Server requires authorization to upload, check password." = "Сервер вимагає авторизації для завантаження, перевірте пароль."; /* No comment provided by engineer. */ "Server test failed!" = "Тест сервера завершився невдало!"; @@ -4267,6 +4852,9 @@ /* 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…" = "Встановити ім'я контакту…"; @@ -4279,6 +4867,12 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Встановіть його замість аутентифікації системи."; +/* No comment provided by engineer. */ +"Set member admission" = "Встановити прийом учасників"; + +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Встановлюйте термін придатності повідомлень у чатах."; + /* profile update event chat item */ "set new contact address" = "встановити нову контактну адресу"; @@ -4294,6 +4888,9 @@ /* No comment provided by engineer. */ "Set passphrase to export" = "Встановити ключову фразу для експорту"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Налаштуйте біографію профілю та вітальне повідомлення."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "Налаштуйте повідомлення, яке показуватиметься новим користувачам!"; @@ -4310,7 +4907,7 @@ "Shape profile images" = "Сформуйте зображення профілю"; /* alert action - chat item action */ +chat item action */ "Share" = "Поділіться"; /* No comment provided by engineer. */ @@ -4334,6 +4931,12 @@ /* No comment provided by engineer. */ "Share link" = "Поділіться посиланням"; +/* alert button */ +"Share old address" = "Поділіться старою адресою"; + +/* alert button */ +"Share old link" = "Поділіться старим посиланням"; + /* No comment provided by engineer. */ "Share profile" = "Поділіться профілем"; @@ -4349,6 +4952,18 @@ /* No comment provided by engineer. */ "Share with contacts" = "Поділіться з контактами"; +/* No comment provided by engineer. */ +"Share your address" = "Поділіться своєю адресою"; + +/* No comment provided by engineer. */ +"Short description" = "Короткий опис"; + +/* No comment provided by engineer. */ +"Short link" = "Коротке посилання"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Коротка адреса SimpleX"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Показувати → у повідомленнях, надісланих через приватну маршрутизацію."; @@ -4391,6 +5006,15 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX адреса або одноразове посилання?"; +/* alert title */ +"SimpleX address settings" = "Автоприйняття налаштувань"; + +/* 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, у додаток."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Безпека SimpleX Chat була перевірена компанією Trail of Bits."; @@ -4472,6 +5096,10 @@ /* notification title */ "Somebody" = "Хтось"; +/* blocking reason +report reason */ +"Spam" = "Спам"; + /* No comment provided by engineer. */ "Square, circle, or anything in between." = "Квадрат, коло або щось середнє між ними."; @@ -4529,6 +5157,9 @@ /* No comment provided by engineer. */ "Stopping chat" = "Зупинка чату"; +/* No comment provided by engineer. */ +"Storage" = "Зберігання"; + /* No comment provided by engineer. */ "strike" = "закреслено"; @@ -4571,6 +5202,18 @@ /* No comment provided by engineer. */ "Tap button " = "Натисніть кнопку "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "Натисніть Підключитися до чату"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "Натисніть Підключитися, щоб відправити запит"; + +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше."; + +/* No comment provided by engineer. */ +"Tap Join group" = "Натисніть Приєднатися до групи"; + /* No comment provided by engineer. */ "Tap to activate profile." = "Натисніть, щоб активувати профіль."; @@ -4592,9 +5235,15 @@ /* No comment provided by engineer. */ "TCP connection" = "TCP-з'єднання"; +/* No comment provided by engineer. */ +"TCP connection bg timeout" = "Таймаут TCP-з'єднання bg"; + /* 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"; @@ -4604,12 +5253,15 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* 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" = "Тестовий сервер"; @@ -4628,6 +5280,9 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Дякуємо користувачам - зробіть свій внесок через Weblate!"; +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "Адреса буде короткою, і ваш профіль буде доступний за цією адресою."; + /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; @@ -4667,6 +5322,9 @@ /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Ідентифікатор наступного повідомлення неправильний (менше або дорівнює попередньому).\nЦе може статися через помилку або коли з'єднання скомпрометовано."; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "Посилання буде коротким, а профіль групи буде поширюватися за посиланням."; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "Повідомлення буде видалено для всіх учасників."; @@ -4682,22 +5340,16 @@ /* 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 same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Такі ж умови будуть застосовуватися до оператора(ів): **%@**."; - /* No comment provided by engineer. */ "The second preset operator in the app!" = "Другий попередньо встановлений оператор у застосунку!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Другу галочку ми пропустили! ✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "Відправник НЕ буде повідомлений"; /* No comment provided by engineer. */ @@ -4730,6 +5382,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." = "Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені."; @@ -4755,17 +5410,20 @@ "This group no longer exists." = "Цієї групи більше не існує."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Це ваше власне одноразове посилання!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Це ваша власна SimpleX-адреса!"; +"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. */ +"Time to disappear is set only for new contacts." = "Час зникнення встановлюється тільки для нових контактів."; + /* No comment provided by engineer. */ "Title" = "Заголовок"; @@ -4817,6 +5475,9 @@ /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату."; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "Щоб використовувати інший профіль після спроби з'єднання, видаліть чат і скористайтеся посиланням знову."; + /* No comment provided by engineer. */ "To use the servers of **%@**, accept conditions of use." = "Щоб користуватися серверами **%@**, прийміть умови використання."; @@ -4829,6 +5490,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Увімкніть інкогніто при підключенні."; +/* token status */ +"Token status: %@." = "Статус токена: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Непрозорість панелі інструментів"; @@ -4841,12 +5505,6 @@ /* 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" = "Турецький інтерфейс"; @@ -4937,10 +5595,7 @@ /* authentication reason */ "Unlock app" = "Розблокувати додаток"; -/* No comment provided by engineer. */ -"unmute" = "увімкнути звук"; - -/* swipe action */ +/* notification label action */ "Unmute" = "Увімкнути звук"; /* No comment provided by engineer. */ @@ -4949,6 +5604,9 @@ /* swipe action */ "Unread" = "Непрочитане"; +/* No comment provided by engineer. */ +"Unsupported connection link" = "Несумісне посилання для підключення"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Новим користувачам надсилається до 100 останніх повідомлень."; @@ -4964,6 +5622,9 @@ /* No comment provided by engineer. */ "Update settings?" = "Оновити налаштування?"; +/* No comment provided by engineer. */ +"Updated conditions" = "Оновлені умови"; + /* rcv group event chat item */ "updated group profile" = "оновлений профіль групи"; @@ -4973,9 +5634,27 @@ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів."; +/* alert button */ +"Upgrade" = "Оновлення"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Адреса оновлення"; + +/* alert message */ +"Upgrade address?" = "Змінити адресу?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "Оновлення та відкритий чат"; +/* alert message */ +"Upgrade group link?" = "Оновити посилання на групу?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Посилання для оновлення"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Поновіть свою адресу"; + /* No comment provided by engineer. */ "Upload errors" = "Помилки завантаження"; @@ -5003,7 +5682,7 @@ /* No comment provided by engineer. */ "Use chat" = "Використовуйте чат"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Використовувати поточний профіль"; /* No comment provided by engineer. */ @@ -5019,9 +5698,12 @@ "Use from desktop" = "Використання з робочого столу"; /* No comment provided by engineer. */ -"Use iOS call interface" = "Використовуйте інтерфейс виклику iOS"; +"Use incognito profile" = "Використовуйте профіль інкогніто"; /* No comment provided by engineer. */ +"Use iOS call interface" = "Використовуйте інтерфейс виклику iOS"; + +/* new chat action */ "Use new incognito profile" = "Використовуйте новий профіль інкогніто"; /* No comment provided by engineer. */ @@ -5045,12 +5727,21 @@ /* 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 TCP port 443 for preset servers only." = "Використовуйте TCP порт 443 лише для попередньо налаштованих серверів."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Використовуйте додаток під час розмови."; /* No comment provided by engineer. */ "Use the app with one hand." = "Використовуйте додаток однією рукою."; +/* No comment provided by engineer. */ +"Use web port" = "Використовувати веб-порт"; + /* No comment provided by engineer. */ "User selection" = "Вибір користувача"; @@ -5201,6 +5892,9 @@ /* No comment provided by engineer. */ "Welcome message is too long" = "Привітальне повідомлення занадто довге"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Вітаємо ваші контакти 👋"; + /* No comment provided by engineer. */ "What's new" = "Що нового"; @@ -5270,6 +5964,9 @@ /* No comment provided by engineer. */ "You accepted connection" = "Ви прийняли підключення"; +/* snd group event chat item */ +"you accepted this member" = "ви прийняли цього учасника"; + /* No comment provided by engineer. */ "You allow" = "Ви дозволяєте"; @@ -5280,32 +5977,26 @@ "You are already connected to %@." = "Ви вже підключені до %@."; /* No comment provided by engineer. */ +"You are already connected with %@." = "Ви вже підключені до %@."; + +/* new chat sheet message */ "You are already connecting to %@." = "Ви вже з'єднані з %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Ви вже підключаєтеся до %@.Ви вже підключаєтеся за цим одноразовим посиланням!"; /* No comment provided by engineer. */ "You are already in group %@." = "Ви вже перебуваєте в групі %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "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. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Ви вже приєдналися до групи за цим посиланням."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "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." = "Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту."; - -/* No comment provided by engineer. */ -"you are invited to group" = "вас запрошують до групи"; - /* No comment provided by engineer. */ "You are invited to group" = "Запрошуємо вас до групи"; @@ -5324,9 +6015,6 @@ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "Ви можете змінити його в налаштуваннях зовнішнього вигляду."; -/* No comment provided by engineer. */ -"You can configure operators in Network & servers settings." = "Ви можете налаштувати операторів у налаштуваннях Мережі та серверів."; - /* No comment provided by engineer. */ "You can configure servers via settings." = "Ви можете налаштувати сервери за допомогою налаштувань."; @@ -5381,7 +6069,10 @@ /* alert message */ "You can view invitation link again in connection details." = "Ви можете переглянути посилання на запрошення ще раз у деталях підключення."; -/* No comment provided by engineer. */ +/* alert message */ +"You can view your reports in Chat with admins." = "Ви можете переглянути свої звіти у чаті з адміністраторами."; + +/* alert title */ "You can't send messages!" = "Ви не можете надсилати повідомлення!"; /* chat item text */ @@ -5402,10 +6093,7 @@ /* 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. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Ви вже надіслали запит на підключення!\nПовторити запит на підключення?"; /* No comment provided by engineer. */ @@ -5453,9 +6141,15 @@ /* chat list item description */ "you shared one-time link incognito" = "ви поділилися одноразовим посиланням інкогніто"; +/* token info */ +"You should receive notifications." = "Ви повинні отримувати сповіщення."; + /* snd group event chat item */ "you unblocked %@" = "ви розблокували %@"; +/* No comment provided by engineer. */ +"You will be able to send messages **only after your request is accepted**." = "Ви зможете надсилати повідомлення **тільки після того, як ваш запит буде прийнято**."; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!"; @@ -5472,10 +6166,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." = "Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена."; @@ -5492,6 +6186,9 @@ /* 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 business contact" = "Ваш діловий контакт"; + /* No comment provided by engineer. */ "Your calls" = "Твої дзвінки"; @@ -5507,8 +6204,14 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Ваші профілі чату"; +/* alert message */ +"Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Ваш чат було переміщено на %@, але при перенаправленні на профіль сталася несподівана помилка."; + /* No comment provided by engineer. */ -"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка."; +"Your connection was moved to %@ but an error happened when switching profile." = "Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка."; + +/* No comment provided by engineer. */ +"Your contact" = "Ваш контакт"; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; @@ -5528,6 +6231,9 @@ /* No comment provided by engineer. */ "Your current profile" = "Ваш поточний профіль"; +/* No comment provided by engineer. */ +"Your group" = "Ваша група"; + /* No comment provided by engineer. */ "Your ICE servers" = "Ваші сервери ICE"; @@ -5543,15 +6249,15 @@ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Ваш профіль **%@** буде опублікований."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Профіль доступний лише вашим контактам."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль."; /* 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." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої."; - /* No comment provided by engineer. */ "Your random profile" = "Ваш випадковий профіль"; @@ -5567,6 +6273,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваша адреса SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Ваші SMP-сервери"; - diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 62ff2088c2..24d153afd5 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1,9 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (可复制)"; @@ -22,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)"; @@ -82,6 +61,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**推荐**:设备令牌和通知会发送至 SimpleX Chat 通知服务器,但是消息内容、大小或者发送人不会。"; +/* 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." = "**警告**:及时推送通知需要保存在钥匙串的密码。"; @@ -142,12 +124,21 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ 已认证"; +/* No comment provided by engineer. */ +"%@ server" = "服务器"; + +/* No comment provided by engineer. */ +"%@ servers" = "服务器"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ 已上传"; /* notification title */ "%@ wants to connect!" = "%@ 要连接!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ 和 %lld 成员"; @@ -160,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 分钟"; @@ -172,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 星期"; @@ -214,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 秒"; @@ -262,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0秒"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1天"; /* time interval */ @@ -271,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分钟"; @@ -310,6 +328,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "中止地址更改?"; +/* No comment provided by engineer. */ +"About operators" = "关于运营方"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "关于SimpleX Chat"; @@ -320,35 +341,51 @@ "Accent" = "强调"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +alert action +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 - swipe action */ +/* alert action +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 friends" = "添加好友"; + +/* No comment provided by engineer. */ +"Add list" = "添加列表"; + /* No comment provided by engineer. */ "Add profile" = "添加个人资料"; @@ -358,12 +395,27 @@ /* 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" = "附加重音"; @@ -379,6 +431,12 @@ /* 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" = "管理员"; @@ -403,12 +461,18 @@ /* 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." = "所有数据在输入后将被删除。"; @@ -421,6 +485,9 @@ /* 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!" = "所有消息都将被删除 - 这无法被撤销!"; @@ -433,6 +500,12 @@ /* 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." = "所有联系人会保持连接。"; @@ -478,6 +551,9 @@ /* 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." = "允许发送文件和媒体。"; @@ -511,10 +587,10 @@ /* No comment provided by engineer. */ "Already connected?" = "已连接?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "已经在连接了!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "已经加入了该群组!"; /* pref value */ @@ -532,6 +608,9 @@ /* No comment provided by engineer. */ "and %lld other events" = "和 %lld 其他事件"; +/* report reason */ +"Another reason" = "另一个理由"; + /* No comment provided by engineer. */ "Answer call" = "接听来电"; @@ -547,6 +626,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" = "应用程序图标"; @@ -556,6 +638,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" = "应用程序版本"; @@ -571,12 +656,30 @@ /* 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" = "已存档的联系人"; @@ -649,15 +752,36 @@ /* 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" = "黑色"; @@ -685,7 +809,8 @@ /* 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. */ @@ -718,9 +843,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)!" = "保加利亚语、芬兰语、泰语和乌克兰语——感谢用户和[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" = "呼叫"; @@ -761,7 +898,8 @@ "Can't message member" = "无法向成员发送消息"; /* alert action - alert button */ +alert button +new chat action */ "Cancel" = "取消"; /* No comment provided by engineer. */ @@ -788,6 +926,12 @@ /* 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?" = "更改数据库密码?"; @@ -813,7 +957,7 @@ "Change self-destruct mode" = "更改自毁模式"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "更改自毁密码"; /* chat item text */ @@ -831,6 +975,15 @@ /* chat item text */ "changing address…" = "更改地址…"; +/* No comment provided by engineer. */ +"Chat" = "聊天"; + +/* No comment provided by engineer. */ +"Chat already exists" = "聊天已存在"; + +/* new chat sheet title */ +"Chat already exists!" = "聊天已存在!"; + /* No comment provided by engineer. */ "Chat colors" = "聊天颜色"; @@ -867,15 +1020,30 @@ /* 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." = "检查服务器地址并再试一次。"; @@ -909,6 +1077,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?" = "清除私密笔记?"; @@ -924,6 +1098,9 @@ /* No comment provided by engineer. */ "colored" = "彩色"; +/* report reason */ +"Community guidelines violation" = "违反社区指导方针"; + /* server test step */ "Compare file" = "对比文件"; @@ -936,9 +1113,33 @@ /* 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): **%@**." = "已经接受下列运营方的条款:**%@**。"; + +/* alert button */ +"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" = "确认"; @@ -969,15 +1170,15 @@ /* No comment provided by engineer. */ "Confirm upload" = "确认上传"; +/* token status text */ +"Confirmed" = "已确定"; + /* server test step */ "Connect" = "连接"; /* No comment provided by engineer. */ "Connect automatically" = "自动连接"; -/* No comment provided by engineer. */ -"Connect incognito" = "在隐身状态下连接"; - /* No comment provided by engineer. */ "Connect to desktop" = "连接到桌面"; @@ -987,25 +1188,22 @@ /* No comment provided by engineer. */ "Connect to your friends faster." = "更快地与您的朋友联系。"; -/* No comment provided by engineer. */ -"Connect to yourself?" = "连接到你自己?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "与自己建立联系?\n这是您自己的一次性链接!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "与自己建立联系?\n这是您自己的 SimpleX 地址!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "通过联系地址连接"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "通过链接连接"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "通过一次性链接连接"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "与 %@连接"; /* No comment provided by engineer. */ @@ -1017,9 +1215,6 @@ /* No comment provided by engineer. */ "Connected desktop" = "已连接的桌面"; -/* rcv group event chat item */ -"connected directly" = "已直连"; - /* No comment provided by engineer. */ "Connected servers" = "已连接的服务器"; @@ -1069,6 +1264,9 @@ "Connection and servers status." = "连接和服务器状态。"; /* No comment provided by engineer. */ +"Connection blocked" = "连接被阻止"; + +/* alert title */ "Connection error" = "连接错误"; /* No comment provided by engineer. */ @@ -1077,6 +1275,12 @@ /* 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" = "连接通知"; @@ -1084,9 +1288,15 @@ "Connection request sent!" = "已发送连接请求!"; /* No comment provided by engineer. */ -"Connection terminated" = "连接被终止"; +"Connection requires encryption renegotiation." = "连接需要加密重协商。"; /* No comment provided by engineer. */ +"Connection security" = "连接安全性"; + +/* No comment provided by engineer. */ +"Connection terminated" = "连接被终止"; + +/* alert title */ "Connection timeout" = "连接超时"; /* No comment provided by engineer. */ @@ -1140,6 +1350,9 @@ /* 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" = "继续"; @@ -1155,12 +1368,18 @@ /* 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 1-time link" = "创建一次性链接"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "使用随机身份创建群组."; @@ -1176,6 +1395,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/)中创建新的个人资料。 💻"; @@ -1185,9 +1407,6 @@ /* server test step */ "Create queue" = "创建队列"; -/* No comment provided by engineer. */ -"Create secret group" = "创建私密群组"; - /* No comment provided by engineer. */ "Create SimpleX address" = "创建 SimpleX 地址"; @@ -1212,6 +1431,9 @@ /* 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" = "当前密码"; @@ -1230,6 +1452,9 @@ /* No comment provided by engineer. */ "Custom time" = "自定义时间"; +/* No comment provided by engineer. */ +"Customizable message shape." = "可自定义消息形状。"; + /* No comment provided by engineer. */ "Customize theme" = "自定义主题"; @@ -1311,7 +1536,8 @@ /* No comment provided by engineer. */ "decryption errors" = "解密错误"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "默认 (%@)"; /* No comment provided by engineer. */ @@ -1321,8 +1547,7 @@ "default (yes)" = "默认 (是)"; /* alert action - chat item action - swipe action */ +swipe action */ "Delete" = "删除"; /* No comment provided by engineer. */ @@ -1346,12 +1571,21 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "删除并通知联系人"; +/* No comment provided by engineer. */ +"Delete chat" = "删除聊天"; + +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "从你的设备删除聊天消息。"; + /* No comment provided by engineer. */ "Delete chat profile" = "删除聊天资料"; /* No comment provided by engineer. */ "Delete chat profile?" = "删除聊天资料?"; +/* No comment provided by engineer. */ +"Delete chat?" = "删除聊天?"; + /* No comment provided by engineer. */ "Delete connection" = "删除连接"; @@ -1397,13 +1631,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. */ @@ -1415,6 +1652,9 @@ /* No comment provided by engineer. */ "Delete old database?" = "删除旧数据库吗?"; +/* No comment provided by engineer. */ +"Delete or moderate up to 200 messages." = "允许自行删除或管理员移除最多200条消息。"; + /* No comment provided by engineer. */ "Delete pending connection?" = "删除待定连接?"; @@ -1424,6 +1664,9 @@ /* 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 条信息。"; @@ -1454,6 +1697,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "删除错误"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "已送达,即使苹果已将其删除。"; + /* No comment provided by engineer. */ "Delivery" = "传送"; @@ -1503,7 +1749,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 锁定。"; @@ -1521,11 +1767,20 @@ "Direct messages" = "私信"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited." = "此群中禁止成员之间私信。"; +"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" = "全部禁用"; @@ -1577,12 +1832,18 @@ /* 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" = "不创建地址"; @@ -1590,13 +1851,19 @@ "Don't enable" = "不要启用"; /* No comment provided by engineer. */ +"Don't miss important messages." = "不错过重要消息。"; + +/* alert action */ "Don't show again" = "不再显示"; +/* No comment provided by engineer. */ +"Done" = "完成"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "降级并打开聊天"; /* alert button - chat item action */ +chat item action */ "Download" = "下载"; /* No comment provided by engineer. */ @@ -1608,6 +1875,9 @@ /* server test step */ "Download file" = "下载文件"; +/* alert action */ +"Download files" = "下载文件"; + /* No comment provided by engineer. */ "Downloaded" = "已下载"; @@ -1627,7 +1897,7 @@ "duplicate message" = "重复的消息"; /* No comment provided by engineer. */ -"duplicates" = "复本"; +"duplicates" = "副本"; /* No comment provided by engineer. */ "Duration" = "时长"; @@ -1635,6 +1905,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "端到端加密"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "端到端加密的通知。"; + /* chat item action */ "Edit" = "编辑"; @@ -1647,12 +1920,15 @@ /* 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" = "全部启用"; @@ -1764,6 +2040,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "需要为 %@ 重新进行加密协商"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "正进行加密重协商。"; + /* No comment provided by engineer. */ "ended" = "已结束"; @@ -1812,22 +2091,34 @@ /* 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 adding member(s)" = "添加成员错误"; +/* alert title */ +"Error adding server" = "添加服务器出错"; + /* No comment provided by engineer. */ "Error changing address" = "更改地址错误"; /* No comment provided by engineer. */ -"Error changing role" = "更改角色错误"; +"Error changing connection profile" = "更改连接资料出错"; /* No comment provided by engineer. */ +"Error changing role" = "更改角色错误"; + +/* alert title */ "Error changing setting" = "更改设置错误"; /* No comment provided by engineer. */ +"Error changing to incognito!" = "切换至隐身聊天出错!"; + +/* alert message */ "Error connecting to forwarding server %@. Please try later." = "连接到转发服务器 %@ 时出错。请稍后尝试。"; /* No comment provided by engineer. */ @@ -1839,6 +2130,9 @@ /* No comment provided by engineer. */ "Error creating group link" = "创建群组链接错误"; +/* alert title */ +"Error creating list" = "创建列表出错"; + /* No comment provided by engineer. */ "Error creating member contact" = "创建成员联系人时出错"; @@ -1848,22 +2142,25 @@ /* No comment provided by engineer. */ "Error creating profile!" = "创建资料错误!"; +/* No comment provided by engineer. */ +"Error creating report" = "创建举报出错"; + /* No comment provided by engineer. */ "Error decrypting file" = "解密文件时出错"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "删除聊天数据库错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "删除聊天错误!"; /* No comment provided by engineer. */ "Error deleting connection" = "删除连接错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "删除数据库错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "删除旧数据库错误"; /* No comment provided by engineer. */ @@ -1884,18 +2181,24 @@ /* No comment provided by engineer. */ "Error encrypting database" = "加密数据库错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "导出聊天数据库错误"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "导出主题时出错: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "导入聊天数据库错误"; /* No comment provided by engineer. */ "Error joining group" = "加入群组错误"; +/* alert title */ +"Error loading servers" = "加载服务器出错"; + +/* No comment provided by engineer. */ +"Error migrating settings" = "迁移设置出错"; + /* No comment provided by engineer. */ "Error opening chat" = "打开聊天时出错"; @@ -1908,12 +2211,21 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "重新连接服务器时出错"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "注册消息推送出错"; + +/* alert title */ "Error removing member" = "删除成员错误"; +/* 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" = "保存群组资料错误"; @@ -1926,6 +2238,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "保存密码到钥匙串错误"; +/* alert title */ +"Error saving servers" = "保存服务器出错"; + /* when migrating */ "Error saving settings" = "保存设置出错"; @@ -1953,18 +2268,27 @@ /* No comment provided by engineer. */ "Error stopping chat" = "停止聊天错误"; +/* alert title */ +"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" = "更新设置错误"; @@ -1980,7 +2304,9 @@ /* No comment provided by engineer. */ "Error: " = "错误: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "错误: %@"; /* No comment provided by engineer. */ @@ -1992,11 +2318,11 @@ /* No comment provided by engineer. */ "Errors" = "错误"; -/* No comment provided by engineer. */ -"Even when disabled in the conversation." = "即使在对话中被禁用。"; +/* 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" = "退出而不保存"; @@ -2007,6 +2333,9 @@ /* No comment provided by engineer. */ "expired" = "过期"; +/* token status text */ +"Expired" = "已过期"; + /* No comment provided by engineer. */ "Export database" = "导出数据库"; @@ -2031,15 +2360,30 @@ /* 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." = "找不到文件 - 很可能文件已被删除或取消。"; @@ -2097,6 +2441,9 @@ /* No comment provided by engineer. */ "Find chats faster" = "更快地查找聊天记录"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "服务器地址中的证书指纹可能不正确"; + /* No comment provided by engineer. */ "Fix" = "修复"; @@ -2115,15 +2462,45 @@ /* 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" = "已转发"; @@ -2134,7 +2511,10 @@ "Forwarded from" = "转发自"; /* No comment provided by engineer. */ -"Forwarding server %@ failed to connect to destination server %@. Please try later." = "转发服务器 %@ 无法连接到目标服务器 %@。请稍后尝试。"; +"Forwarding %lld messages" = "正在转发 %lld 条消息"; + +/* alert message */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "转发服务器 %1$@ 无法连接到目标服务器 %2$@。请稍后尝试。"; /* No comment provided by engineer. */ "Forwarding server address is incompatible with network settings: %@." = "转发服务器地址与网络设置不兼容:%@。"; @@ -2169,6 +2549,9 @@ /* 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 和贴纸"; @@ -2184,7 +2567,7 @@ /* No comment provided by engineer. */ "Group already exists" = "群组已存在"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "群已存在!"; /* No comment provided by engineer. */ @@ -2241,9 +2624,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" = "隐藏"; @@ -2274,6 +2663,15 @@ /* 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. */ "How SimpleX works" = "SimpleX的工作原理"; @@ -2337,6 +2735,9 @@ /* 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" = "改进了消息传递"; @@ -2358,6 +2759,12 @@ /* No comment provided by engineer. */ "inactive" = "无效"; +/* report reason */ +"Inappropriate content" = "不当内容"; + +/* report reason */ +"Inappropriate profile" = "不当个人资料"; + /* No comment provided by engineer. */ "Incognito" = "隐身聊天"; @@ -2424,6 +2831,21 @@ /* No comment provided by engineer. */ "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" = "无效聊天"; @@ -2439,7 +2861,7 @@ /* No comment provided by engineer. */ "Invalid display name!" = "无效的显示名!"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid link" = "无效链接"; /* No comment provided by engineer. */ @@ -2475,6 +2897,9 @@ /* No comment provided by engineer. */ "Invite members" = "邀请成员"; +/* No comment provided by engineer. */ +"Invite to chat" = "邀请加入聊天"; + /* No comment provided by engineer. */ "Invite to group" = "邀请加入群组"; @@ -2496,6 +2921,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" = "不可撤回消息移除"; @@ -2533,24 +2961,18 @@ "Join" = "加入"; /* No comment provided by engineer. */ -"join as %@" = "以 %@ 身份加入"; +"Join as %@" = "以 %@ 身份加入"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "加入群组"; /* No comment provided by engineer. */ "Join group conversations" = "加入群对话"; -/* No comment provided by engineer. */ -"Join group?" = "加入群组?"; - /* No comment provided by engineer. */ "Join incognito" = "加入隐身聊天"; -/* No comment provided by engineer. */ -"Join with current profile" = "使用当前档案加入"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "加入您的群组?\n这是您组 %@ 的链接!"; /* No comment provided by engineer. */ @@ -2586,6 +3008,12 @@ /* swipe action */ "Leave" = "离开"; +/* No comment provided by engineer. */ +"Leave chat" = "离开聊天"; + +/* No comment provided by engineer. */ +"Leave chat?" = "离开聊天?"; + /* No comment provided by engineer. */ "Leave group" = "离开群组"; @@ -2613,6 +3041,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" = "实时"; @@ -2682,12 +3119,21 @@ /* 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!" = "成员将被移出群组——此操作无法撤消!"; @@ -2697,6 +3143,9 @@ /* 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." = "群组成员可以私信。"; @@ -2712,6 +3161,9 @@ /* No comment provided by engineer. */ "Members can send voice messages." = "群组成员可以发送语音消息。"; +/* No comment provided by engineer. */ +"Mention members 👋" = "提及成员👋"; + /* No comment provided by engineer. */ "Menus" = "菜单"; @@ -2757,6 +3209,9 @@ /* No comment provided by engineer. */ "Message servers" = "消息服务器"; +/* No comment provided by engineer. */ +"Message shape" = "消息形状"; + /* No comment provided by engineer. */ "Message source remains private." = "消息来源保持私密。"; @@ -2781,12 +3236,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." = "消息、文件和通话受到 **端到端加密** 的保护,具有完全正向保密、否认和闯入恢复。"; @@ -2853,24 +3314,30 @@ /* 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. */ "Multiple chat profiles" = "多个聊天资料"; -/* No comment provided by engineer. */ -"mute" = "静音"; - -/* swipe action */ +/* notification label action */ "Mute" = "静音"; +/* notification label action */ +"Mute all" = "全部静音"; + /* No comment provided by engineer. */ "Muted when inactive!" = "不活动时静音!"; @@ -2883,21 +3350,30 @@ /* 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. */ +/* alert title */ "Network status" = "网络状态"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "从不"; +/* token status text */ +"New" = "新"; + /* No comment provided by engineer. */ "New chat" = "新聊天"; @@ -2916,6 +3392,9 @@ /* No comment provided by engineer. */ "New display name" = "新显示名"; +/* notification */ +"New events" = "新事件"; + /* No comment provided by engineer. */ "New in %@" = "%@ 的新内容"; @@ -2937,6 +3416,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" = "否"; @@ -2946,6 +3434,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" = "未选择联系人"; @@ -2976,9 +3473,24 @@ /* 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" = "没有录制语音消息的权限"; @@ -2988,24 +3500,57 @@ /* 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- 禁用成员(“观察员”角色)"; @@ -3013,8 +3558,9 @@ "observer" = "观察者"; /* enabled status - group pref value - time to disappear */ +group pref value +member criteria value +time to disappear */ "off" = "关闭"; /* blur media */ @@ -3026,7 +3572,9 @@ /* feature offered item */ "offered %@: %@" = "已提供 %1$@:%2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "好的"; /* No comment provided by engineer. */ @@ -3050,6 +3598,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "将不会使用 Onion 主机。"; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "仅聊天所有人可更改首选项。"; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。"; @@ -3065,6 +3616,12 @@ /* 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." = "只有您可以添加消息回应。"; @@ -3095,16 +3652,22 @@ /* 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" = "打开更改"; + +/* new chat action */ "Open chat" = "打开聊天"; /* authentication reason */ "Open chat console" = "打开聊天控制台"; /* No comment provided by engineer. */ +"Open conditions" = "打开条款"; + +/* new chat action */ "Open group" = "打开群"; /* authentication reason */ @@ -3116,6 +3679,15 @@ /* 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" = "或粘贴存档链接"; @@ -3128,6 +3700,12 @@ /* 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" = "其他"; @@ -3159,10 +3737,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" = "粘贴桌面地址"; @@ -3212,7 +3790,7 @@ /* 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. */ +/* alert message */ "Please check your network connection with %@ and try again." = "请检查您与%@的网络连接,然后重试。"; /* No comment provided by engineer. */ @@ -3251,9 +3829,6 @@ /* No comment provided by engineer. */ "Polish interface" = "波兰语界面"; -/* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "服务器地址中的证书指纹可能不正确"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "保留最后的消息草稿及其附件。"; @@ -3269,9 +3844,15 @@ /* 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" = "私密文件名"; @@ -3287,7 +3868,7 @@ /* No comment provided by engineer. */ "Private routing" = "专用路由"; -/* No comment provided by engineer. */ +/* alert title */ "Private routing error" = "专用路由错误"; /* No comment provided by engineer. */ @@ -3416,9 +3997,6 @@ /* No comment provided by engineer. */ "received confirmation…" = "已受到确认……"; -/* notification */ -"Received file event" = "收到文件项目"; - /* message info title */ "Received message" = "收到的信息"; @@ -3479,14 +4057,15 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "减少电池使用量"; -/* reject incoming call via notification - swipe action */ +/* alert action +reject incoming call via notification +swipe action */ "Reject" = "拒绝"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "拒绝联系人(发送者不会被通知)"; -/* No comment provided by engineer. */ +/* alert title */ "Reject contact request" = "拒绝联系人请求"; /* call status */ @@ -3537,18 +4116,12 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "重新协商加密?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "重复连接请求吗?"; - /* No comment provided by engineer. */ "Repeat download" = "重复下载"; /* No comment provided by engineer. */ "Repeat import" = "重复导入"; -/* No comment provided by engineer. */ -"Repeat join request?" = "重复加入请求吗?"; - /* No comment provided by engineer. */ "Repeat upload" = "重复上传"; @@ -3600,26 +4173,29 @@ /* No comment provided by engineer. */ "Restore database error" = "恢复数据库错误"; -/* No comment provided by engineer. */ +/* alert action */ "Retry" = "重试"; /* chat item action */ "Reveal" = "揭示"; /* No comment provided by engineer. */ -"Revoke" = "撤销"; - -/* cancel file action */ -"Revoke file" = "撤销文件"; +"Review conditions" = "审阅条款"; /* No comment provided by engineer. */ -"Revoke file?" = "撤销文件?"; +"Revoke" = "吊销"; + +/* cancel file action */ +"Revoke file" = "吊销文件"; + +/* No comment provided by engineer. */ +"Revoke file?" = "吊销文件?"; /* No comment provided by engineer. */ "Role" = "角色"; /* No comment provided by engineer. */ -"Run chat" = "运行聊天程序"; +"Run chat" = "运行聊天"; /* No comment provided by engineer. */ "Safely receive files" = "安全接收文件"; @@ -3628,7 +4204,7 @@ "Safer groups" = "更安全的群组"; /* alert button - chat item action */ +chat item action */ "Save" = "保存"; /* alert button */ @@ -3649,6 +4225,9 @@ /* 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" = "保存密码并打开聊天"; @@ -3670,6 +4249,9 @@ /* No comment provided by engineer. */ "Save welcome message?" = "保存欢迎信息?"; +/* alert title */ +"Save your profile?" = "保存您的个人资料?"; + /* No comment provided by engineer. */ "saved" = "已保存"; @@ -3688,6 +4270,9 @@ /* 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" = "规模"; @@ -3778,9 +4363,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "将送达回执发送给"; -/* No comment provided by engineer. */ -"send direct message" = "发送私信"; - /* No comment provided by engineer. */ "Send direct message to connect" = "发送私信来连接"; @@ -3859,9 +4441,6 @@ /* No comment provided by engineer. */ "Sent directly" = "直接发送"; -/* notification */ -"Sent file event" = "已发送文件项目"; - /* message info title */ "Sent message" = "已发信息"; @@ -3893,10 +4472,10 @@ "server queue info: %@\n\nlast received msg: %@" = "服务器队列信息: %1$@\n\n上次收到的消息: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "服务器需要授权才能创建队列,检查密码"; +"Server requires authorization to create queues, check password." = "服务器需要授权才能创建队列,检查密码"; /* server test error */ -"Server requires authorization to upload, check password" = "服务器需要授权来上传,检查密码"; +"Server requires authorization to upload, check password." = "服务器需要授权来上传,检查密码"; /* No comment provided by engineer. */ "Server test failed!" = "服务器测试失败!"; @@ -3965,7 +4544,7 @@ "Shape profile images" = "改变个人资料图形状"; /* alert action - chat item action */ +chat item action */ "Share" = "分享"; /* No comment provided by engineer. */ @@ -4028,6 +4607,21 @@ /* 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 地址或一次性链接?"; + +/* alert title */ +"SimpleX address settings" = "自动接受设置"; + +/* 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 审核。"; @@ -4035,10 +4629,10 @@ "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 链接"; @@ -4064,6 +4658,9 @@ /* 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" = "简化的隐身模式"; @@ -4085,6 +4682,9 @@ /* 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:" = "某些文件未导出:"; @@ -4220,7 +4820,7 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* No comment provided by engineer. */ +/* file error alert title */ "Temporary file error" = "临时文件错误"; /* server test failure */ @@ -4292,13 +4892,10 @@ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "旧数据库在迁移过程中没有被移除,可以删除。"; -/* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "该资料仅与您的联系人共享。"; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "我们错过的第二个\"√\"!✅"; -/* No comment provided by engineer. */ +/* alert message */ "The sender will NOT be notified" = "发送者将不会收到通知"; /* No comment provided by engineer. */ @@ -4346,12 +4943,6 @@ /* No comment provided by engineer. */ "This group no longer exists." = "该群组已不存在。"; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "这是你自己的一次性链接!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "这是你自己的 SimpleX 地址!"; - /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "此链接已在其他移动设备上使用,请在桌面上创建新链接。"; @@ -4415,12 +5006,6 @@ /* 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" = "土耳其语界面"; @@ -4508,10 +5093,7 @@ /* authentication reason */ "Unlock app" = "解锁应用程序"; -/* No comment provided by engineer. */ -"unmute" = "取消静音"; - -/* swipe action */ +/* notification label action */ "Unmute" = "取消静音"; /* No comment provided by engineer. */ @@ -4571,7 +5153,7 @@ /* No comment provided by engineer. */ "Use chat" = "使用聊天"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "使用当前配置文件"; /* No comment provided by engineer. */ @@ -4583,7 +5165,7 @@ /* No comment provided by engineer. */ "Use iOS call interface" = "使用 iOS 通话界面"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "使用新的隐身配置文件"; /* No comment provided by engineer. */ @@ -4823,33 +5405,24 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "您已经连接到 %@。"; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "您已连接到 %@。"; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "你已经在通过这个一次性链接进行连接!"; /* No comment provided by engineer. */ "You are already in group %@." = "您已在组 %@ 中。"; -/* No comment provided by engineer. */ +/* new chat sheet message */ "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. */ +/* new chat sheet message */ "You are already joining the group via this link." = "你已经在通过此链接加入该群。"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "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." = "您已连接到用于接收该联系人消息的服务器。"; - -/* No comment provided by engineer. */ -"you are invited to group" = "您被邀请加入群组"; - /* No comment provided by engineer. */ "You are invited to group" = "您被邀请加入群组"; @@ -4916,7 +5489,7 @@ /* alert message */ "You can view invitation link again in connection details." = "您可以在连接详情中再次查看邀请链接。"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "您无法发送消息!"; /* chat item text */ @@ -4937,10 +5510,7 @@ /* 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. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "您已经请求连接了!\n重复连接请求?"; /* No comment provided by engineer. */ @@ -5006,9 +5576,6 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。"; -/* No comment provided by engineer. */ -"You will connect to all group members." = "你将连接到所有群成员。"; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。"; @@ -5070,10 +5637,10 @@ "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 random profile" = "您的随机资料"; @@ -5087,6 +5654,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "您的 SimpleX 地址"; -/* No comment provided by engineer. */ -"Your SMP servers" = "您的 SMP 服务器"; - diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index 250616ea5c..5255319194 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -5,17 +5,17 @@ plugins { id("org.jetbrains.compose") kotlin("android") id("org.jetbrains.kotlin.plugin.serialization") + id("org.jetbrains.kotlin.plugin.compose") } android { - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "chat.simplex.app" namespace = "chat.simplex.app" minSdk = 26 - //noinspection OldTargetApi - targetSdk = 34 + targetSdk = 35 // !!! // skip version code after release to F-Droid, as it uses two version codes versionCode = (extra["android.version_code"] as String).toInt() @@ -85,6 +85,7 @@ android { "en", "ar", "bg", + "ca", "cs", "de", "es", @@ -92,6 +93,7 @@ android { "fi", "fr", "hu", + "in", "it", "iw", "ja", @@ -99,10 +101,12 @@ android { "nl", "pl", "pt-rBR", + "ro", "ru", "th", "tr", "uk", + "vi", "zh-rCN" ) ndkVersion = "23.1.7779620" @@ -187,8 +191,11 @@ tasks { outputDir = outputs.files.files.last() } exec { - workingDir("../../../scripts/android") - environment = mapOf("JAVA_HOME" to "$javaHome") + workingDir("../../scripts/android") + environment = mapOf( + "JAVA_HOME" to "$javaHome", + "PATH" to "${System.getenv("PATH")}:$javaHome/bin" + ) commandLine = listOf( "./compress-and-sign-apk.sh", "${rootProject.extra["compression.level"]}", diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index bb6a6f8f8a..d6059896a5 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -41,6 +41,7 @@ android:fullBackupOnly="false" android:icon="@mipmap/icon" android:label="${app_name}" + android:largeHeap="true" android:extractNativeLibs="${extract_native_libs}" android:supportsRtl="true" android:theme="@style/Theme.SimpleX"> @@ -77,8 +78,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -179,5 +205,16 @@ android:name=".SimplexService$AutoRestartReceiver" android:enabled="true" android:exported="false" /> + + + + + + + + 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 2d2829f1f2..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 @@ -229,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/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index ee259a98d0..83767f90d7 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 @@ -24,7 +24,6 @@ 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.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* @@ -33,7 +32,6 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import com.jakewharton.processphoenix.ProcessPhoenix import kotlinx.coroutines.* -import kotlinx.coroutines.flow.map import java.io.* import java.util.* import java.util.concurrent.TimeUnit @@ -48,6 +46,7 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun onCreate() { super.onCreate() + AppContextProvider.initialize(this) if (ProcessPhoenix.isPhoenixProcess(this)) { return } else { @@ -71,6 +70,7 @@ class SimplexApp: Application(), LifecycleEventObserver { context = this initHaskell(packageName) initMultiplatform() + reconfigureBroadcastReceivers() runMigrations() tmpDir.deleteRecursively() tmpDir.mkdir() @@ -93,7 +93,7 @@ class SimplexApp: Application(), LifecycleEventObserver { Lifecycle.Event.ON_START -> { isAppOnForeground = true if (chatModel.chatRunning.value == true) { - withChats { + withContext(Dispatchers.Main) { kotlin.runCatching { val currentUserId = chatModel.currentUser.value?.userId val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId())) @@ -106,7 +106,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) } - updateChats(chats) + chatModel.chatsContext.updateChats(chats) } }.onFailure { Log.e(TAG, it.stackTraceToString()) } } @@ -216,6 +216,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() @@ -371,4 +372,10 @@ class SimplexApp: Application(), LifecycleEventObserver { 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 6ca8dd43a0..289ecc0a31 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 @@ -112,7 +112,7 @@ class SimplexService: Service() { 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) + val newNtf = createServiceNotification(title, text) serviceNotification = newNtf return newNtf } @@ -144,11 +144,18 @@ class SimplexService: Service() { return@withLongRunningApi } saveServiceState(self, ServiceState.STARTED) - wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply { - acquire() - } - } +// Permanent wakelock prevents deep sleep and results in high battery usage. +// Instead, the app relies on being whitelisted for unrestricted battery usage, +// and also takes wakelock on network events and on network information changes, which allows it to reconnect. +// Network events and information changes are delivered even when device is in deep sleep. +// Possibly, we may need to additionally use "alarms" to wake the app periodically, +// but in all pre-release tests the app was reliably delivering messages in deep sleep, even restored dropped connections. +// +// wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { +// newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply { +// acquire() +// } +// } } finally { isCheckingNewMessages = false } @@ -168,7 +175,7 @@ class SimplexService: Service() { return null } - private fun createNotification(title: String, text: String): Notification { + private fun createServiceNotification(title: String, text: String): Notification { val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent -> PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) } @@ -181,6 +188,7 @@ class SimplexService: Service() { .setContentIntent(pendingIntent) .setSilent(true) .setShowWhen(false) // no date/time + .setOngoing(true) // Starting SDK 33 / Android 13, foreground notifications can be swiped away // Shows a button which opens notification channel settings if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -249,6 +257,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 @@ -694,6 +725,7 @@ 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 cf19589d4a..5d8371708c 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 @@ -136,7 +136,6 @@ object NtfManager { val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags) val actionButton = when (action) { NotificationAction.ACCEPT_CONTACT_REQUEST -> generalGetString(MR.strings.accept) - NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO -> generalGetString(MR.strings.accept_contact_incognito_button) } builder.addAction(0, actionButton, actionPendingIntent) } @@ -316,7 +315,6 @@ object NtfManager { val m = SimplexApp.context.chatModel when (intent.action) { NotificationAction.ACCEPT_CONTACT_REQUEST.name -> ntfManager.acceptContactRequestAction(userId, incognito = false, chatId) - NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO.name -> ntfManager.acceptContactRequestAction(userId, incognito = true, chatId) RejectCallAction -> { val invitation = m.callInvitations[chatId] if (invitation != null) { diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 5b7f89f2df..602b8f2b90 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("org.jetbrains.compose") id("com.android.library") id("org.jetbrains.kotlin.plugin.serialization") + id("org.jetbrains.kotlin.plugin.compose") id("dev.icerock.mobile.multiplatform-resources") id("com.github.gmazzo.buildconfig") version "5.3.5" } @@ -39,6 +40,8 @@ kotlin { api("com.russhwolf:multiplatform-settings:1.1.1") api("com.charleskorn.kaml:kaml:0.59.0") api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}") + implementation("org.jetbrains.compose.material:material-icons-core:1.7.3") + implementation("org.jetbrains.compose.material:material-icons-extended:1.7.3") implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}") //Barcode api("org.boofcv:boofcv-core:1.1.3") @@ -92,7 +95,8 @@ kotlin { implementation("com.jakewharton:process-phoenix:3.0.0") - val cameraXVersion = "1.3.4" + // https://issuetracker.google.com/issues/351313880 + val cameraXVersion = "1.5.1" implementation("androidx.camera:camera-core:${cameraXVersion}") implementation("androidx.camera:camera-camera2:${cameraXVersion}") implementation("androidx.camera:camera-lifecycle:${cameraXVersion}") @@ -110,7 +114,7 @@ kotlin { } // For jSystemThemeDetector only implementation("net.java.dev.jna:jna-platform:5.14.0") - implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT") + implementation("com.sshtools:two-slices:0.9.1") implementation("org.slf4j:slf4j-simple:2.0.12") implementation("uk.co.caprica:vlcj:4.8.3") implementation("net.java.dev.jna:jna:5.14.0") @@ -125,7 +129,7 @@ kotlin { android { namespace = "chat.simplex.common" - compileSdk = 34 + compileSdk = 35 sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { minSdk = 26 @@ -154,6 +158,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"]}") } } @@ -249,8 +255,11 @@ afterEvaluate { val fileRegex = Regex("MR/../strings.xml$|MR/..-.../strings.xml$|MR/..-../strings.xml$|MR/base/strings.xml$") val tree = kotlin.sourceSets["commonMain"].resources.filter { fileRegex.containsMatchIn(it.absolutePath.replace("\\", "/")) }.asFileTree val baseStringsFile = tree.firstOrNull { it.absolutePath.replace("\\", "/").endsWith("base/strings.xml") } ?: throw Exception("No base/strings.xml found") + val lvStringsFile = tree.firstOrNull { it.absolutePath.replace("\\", "/").endsWith("lv/strings.xml") } ?: throw Exception("No base/strings.xml found") val treeList = ArrayList(tree.toList()) treeList.remove(baseStringsFile) + // removed lv/strings.xml file with 100+ errors + treeList.remove(lvStringsFile) treeList.add(0, baseStringsFile) val baseFormatting = mutableMapOf>() treeList.forEachIndexed { index, file -> diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt index 825bc8b846..77e0f49cc8 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt @@ -73,6 +73,7 @@ class NetworkObserver { } private fun setNetworkInfo(info: UserNetworkInfo) { + getWakeLock(timeout = 180000) Log.d(TAG, "Network changed: $info") noNetworkJob.cancel() if (info.online) { 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 cd1672f3e9..a5e105ce93 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 @@ -13,7 +13,6 @@ import java.lang.ref.WeakReference import java.util.* import java.util.concurrent.Semaphore import kotlin.concurrent.thread -import kotlin.random.Random actual val appPlatform = AppPlatform.ANDROID 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/Images.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt index a94675eec2..62030f7e57 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt @@ -104,13 +104,13 @@ actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this actual fun ImageBitmap.hasAlpha(): Boolean = hasAlpha -actual fun ImageBitmap.addLogo(): ImageBitmap = asAndroidBitmap().applyCanvas { - val radius = (width * 0.16f) / 2 +actual fun ImageBitmap.addLogo(size: Float): ImageBitmap = asAndroidBitmap().applyCanvas { + val radius = (width * size) / 2 val paint = android.graphics.Paint() paint.color = android.graphics.Color.WHITE drawCircle(width / 2f, height / 2f, radius, paint) val logo = androidAppContext.resources.getDrawable(R.drawable.icon_foreground_android_common, null).toBitmap() - val logoSize = (width * 0.24).toInt() + val logoSize = (width * size * 1.5).toInt() translate((width - logoSize) / 2f, (height - logoSize) / 2f) drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null) }.asImageBitmap() 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 7b820aa67e..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 @@ -15,10 +15,12 @@ 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 @@ -40,7 +42,6 @@ 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.collect import kotlinx.coroutines.flow.filter import java.lang.reflect.Field import java.net.URI @@ -49,15 +50,16 @@ import java.net.URI actual fun PlatformTextField( composeState: MutableState, sendMsgEnabled: Boolean, + disabledText: String?, sendMsgButtonDisabled: Boolean, textStyle: MutableState, showDeleteTextButton: MutableState, - userIsObserver: Boolean, placeholder: String, showVoiceButton: Boolean, - onMessageChange: (String) -> Unit, + onMessageChange: (ComposeMessage) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List) -> Unit, + focusRequester: FocusRequester?, onDone: () -> Unit, ) { val cs = composeState.value @@ -117,6 +119,13 @@ 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 @@ -126,7 +135,8 @@ actual fun PlatformTextField( 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) + 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) { @@ -149,9 +159,10 @@ actual fun PlatformTextField( } 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("") } @@ -167,10 +178,9 @@ actual fun PlatformTextField( 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() @@ -186,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/ScrollableColumn.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt index b3d8e9b52f..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 @@ -95,8 +95,10 @@ actual fun LazyColumnWithScrollBarNoAppBar( 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() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/SimplexService.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/SimplexService.android.kt new file mode 100644 index 0000000000..3ccdcecab0 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/SimplexService.android.kt @@ -0,0 +1,31 @@ +package chat.simplex.common.platform + +import android.content.Context +import android.os.PowerManager + +actual fun getWakeLock(timeout: Long): (() -> Unit) { + val context = AppContextProvider.getApplicationContext() + ?: throw IllegalStateException("Application context not initialized") + var wakeLock: PowerManager.WakeLock? = (context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SimplexService::lock").apply { + acquire(timeout) + } + } + return { + val lock = wakeLock + if (lock != null) { + if (lock.isHeld) lock.release() + wakeLock = null + } + } +} + +object AppContextProvider { + private var applicationContext: Context? = null + + fun initialize(context: Context) { + this.applicationContext = context.applicationContext + } + + fun getApplicationContext(): Context? = applicationContext +} 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 a1698ae28a..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 @@ -14,12 +14,11 @@ 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.ChatModel.withChats -import chat.simplex.common.model.clear 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() @@ -76,13 +75,10 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { ModalManager.start.closeModal() } else if (chatModel.chatId.value != null) { withApi { - withChats { + withContext(Dispatchers.Main) { // Since no modals are open, the problem is probably in ChatView chatModel.chatId.value = null - chatItems.clearAndNotify() - } - withChats { - chatItems.clearAndNotify() + chatModel.chatsContext.chatItems.clearAndNotify() } } } else { 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 166f4ec355..22e53af849 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 @@ -709,9 +709,11 @@ fun WebRTCView(callCommand: SnapshotStateList, onResponse: (WVAPIM .filterNotNull() .collect { while (callCommand.isNotEmpty()) { - val cmd = callCommand.removeFirst() + val cmd = callCommand.removeFirstOrNull() Log.d(TAG, "WebRTCView LaunchedEffect executing $cmd") - processCommand(wv, cmd) + if (cmd != null) { + processCommand(wv, cmd) + } } } } 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 index b24150ed24..e81827cb9a 100644 --- 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 @@ -46,7 +46,7 @@ actual fun SaveOrOpenFileMenu( } ItemAction( stringResource(MR.strings.save_verb), - painterResource(if (encrypted) MR.images.ic_lock_open_right else MR.images.ic_download), + painterResource(MR.images.ic_download), color = MaterialTheme.colors.primary, onClick = { saveFile() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt index 9e8eb8ee8f..b0bc6dd970 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt @@ -28,7 +28,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) { @Composable actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState) { val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) - ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { + ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = { when (cItem.content.msgContent) { is MsgContent.MCImage -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.status == PermissionStatus.Granted) { 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 index 54e3061d25..a09ca2792b 100644 --- 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 @@ -91,7 +91,7 @@ fun UserPickerUserBox( 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) + userUnreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false) } } val user = userInfo.user 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 320a8e876a..47506d9532 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 @@ -159,11 +159,11 @@ fun AppearanceScope.AppearanceLayout( } } -private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> +private fun findEnabledIcon(): AppIcon = AppIcon.values().firstOrNull { icon -> androidAppContext.packageManager.getComponentEnabledSetting( ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}") ).let { it == COMPONENT_ENABLED_STATE_DEFAULT || it == COMPONENT_ENABLED_STATE_ENABLED } -} +} ?: AppIcon.DEFAULT @Preview @Composable diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/CMakeLists.txt b/apps/multiplatform/common/src/commonMain/cpp/android/CMakeLists.txt index 44cb31d424..fcba574974 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/CMakeLists.txt +++ b/apps/multiplatform/common/src/commonMain/cpp/android/CMakeLists.txt @@ -53,10 +53,20 @@ add_library( support SHARED IMPORTED ) set_target_properties( support PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsupport.so) +target_compile_options(app-lib PRIVATE + -g0 +) + # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this # build script, prebuilt third-party libraries, or system libraries. +# https://developer.android.com/guide/practices/page-sizes#cmake +target_link_options(app-lib PRIVATE + "-Wl,-z,max-page-size=16384" + "-Wl,--build-id=none" +) + target_link_libraries( # Specifies the target library. app-lib @@ -64,4 +74,10 @@ target_link_libraries( # Specifies the target library. # Links the target library to the log library # included in the NDK. - ${log-lib}) + ${log-lib} +) + +add_custom_command(TARGET app-lib POST_BUILD + COMMAND ${CMAKE_STRIP} --remove-section=.comment $ + COMMENT "Stripping .comment section from app-lib" +) diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index f913196da2..130a060519 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -59,12 +59,13 @@ typedef long* chat_ctrl; extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctrl); -extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); -extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctrl, const char *cmd, const int retryNum); +extern char *chat_send_remote_cmd_retry(chat_ctrl ctrl, const int rhId, const char *cmd, const int retryNum); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); +extern char *chat_parse_uri(const char *str, const int safe); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); extern int chat_json_length(const char *str); @@ -125,20 +126,20 @@ Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, __unused jc } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg, jint retryNum) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); //jint length = (jint) (*env)->GetStringUTFLength(env, msg); //for (int i = 0; i < length; ++i) // __android_log_print(ANDROID_LOG_ERROR, "simplex", "%d: %02x\n", i, _msg[i]); - jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); + jstring res = (*env)->NewStringUTF(env, chat_send_cmd_retry((void*)controller, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg, jint retryNum) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_send_remote_cmd((void*)controller, rhId, _msg)); + jstring res = (*env)->NewStringUTF(env, chat_send_remote_cmd_retry((void*)controller, rhId, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } @@ -169,6 +170,14 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, __unused j return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, __unused jclass clazz, jstring str, jint safe) { + const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_parse_uri(_str, safe)); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused jclass clazz, jstring pwd, jstring salt) { const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE); diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 76092e4079..2ea1e11af6 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -32,12 +32,13 @@ typedef long* chat_ctrl; extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctrl); -extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); -extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctrl, const char *cmd, const int retryNum); +extern char *chat_send_remote_cmd_retry(chat_ctrl ctrl, const int rhId, const char *cmd, const int retryNum); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); +extern char *chat_parse_uri(const char *str, const int safe); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); extern int chat_json_length(const char *str); @@ -118,17 +119,17 @@ Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, jclass claz } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendCmdRetry(JNIEnv *env, jclass clazz, jlong controller, jstring msg, jint retryNum) { const char *_msg = encode_to_utf8_chars(env, msg); - jstring res = decode_to_utf8_string(env, chat_send_cmd((void*)controller, _msg)); + jstring res = decode_to_utf8_string(env, chat_send_cmd_retry((void*)controller, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmd(JNIEnv *env, jclass clazz, jlong controller, jint rhId, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmdRetry(JNIEnv *env, jclass clazz, jlong controller, jint rhId, jstring msg, jint retryNum) { const char *_msg = encode_to_utf8_chars(env, msg); - jstring res = decode_to_utf8_string(env, chat_send_remote_cmd((void*)controller, rhId, _msg)); + jstring res = decode_to_utf8_string(env, chat_send_remote_cmd_retry((void*)controller, rhId, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } @@ -159,6 +160,14 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, jclass cla return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, jclass clazz, jstring str, jint safe) { + const char *_str = encode_to_utf8_chars(env, str); + jstring res = decode_to_utf8_string(env, chat_parse_uri(_str, safe)); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) { const char *_pwd = encode_to_utf8_chars(env, pwd); diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index ba1eda8a7c..70e0067260 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 @@ -194,7 +194,7 @@ fun MainScreen() { OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) OnboardingStage.Step3_ChooseServerOperators -> { val modalData = remember { ModalData() } - modalData.ChooseServerOperators(true) + modalData.OnboardingConditionsView() if (appPlatform.isDesktop) { ModalManager.fullscreen.showInView() } @@ -243,9 +243,9 @@ fun MainScreen() { ModalManager.fullscreen.showOneTimePasscodeInView() AlertManager.privacySensitive.showInView() if (onboarding == OnboardingStage.OnboardingComplete) { - LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) { + LaunchedEffect(chatModel.chatRunning.value, chatModel.currentUser.value, chatModel.appOpenUrl.value) { val (rhId, url) = chatModel.appOpenUrl.value ?: (null to null) - if (url != null) { + if (url != null && chatModel.chatRunning.value == true) { chatModel.appOpenUrl.value = null connectIfOpenedViaUri(rhId, url, chatModel) } @@ -339,7 +339,7 @@ fun AndroidScreen(userPickerState: MutableStateFlow) { .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } ) Box2@{ currentChatId.value?.let { - ChatView(currentChatId, reportsView = false, onComposed = onComposed) + ChatView(chatsCtx = chatModel.chatsContext, currentChatId, onComposed = onComposed) } } } @@ -393,7 +393,7 @@ fun CenterPartOfScreen() { ModalManager.center.showInView() } } - else -> ChatView(currentChatId, reportsView = false) {} + else -> ChatView(chatsCtx = chatModel.chatsContext, currentChatId) {} } } 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 c3bea3be90..8db2cc1a76 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 @@ -8,6 +8,7 @@ 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.* @@ -25,6 +26,8 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlin.collections.removeAll as remAll import kotlinx.datetime.* +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.serialization.* import kotlinx.serialization.descriptors.* @@ -37,10 +40,44 @@ 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.* +object ConnectProgressManager { + private val connectInProgress = mutableStateOf(null) + private val connectProgressByTimeout = mutableStateOf(false) + private var onCancel: (() -> Unit)? = null + + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + fun startConnectProgress(text: String, onCancel: (() -> Unit)? = null) { + connectInProgress.value = text + this.onCancel = onCancel + coroutineScope.launch { + delay(1000) + connectProgressByTimeout.value = connectInProgress.value != null + } + } + + fun stopConnectProgress() { + connectInProgress.value = null + onCancel = null + connectProgressByTimeout.value = false + } + + fun cancelConnectProgress() { + onCancel?.invoke() + stopConnectProgress() + } + + val showConnectProgress: String? get() = + if (connectProgressByTimeout.value) connectInProgress.value else null +} + +val connectProgressManager = ConnectProgressManager + /* * Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it * */ @@ -59,19 +96,22 @@ object ChatModel { val dbMigrationInProgress = mutableStateOf(false) 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 chatAgentConnId = mutableStateOf(null) + val chatSubStatus = mutableStateOf(null) + val openAroundItemId: MutableState = mutableStateOf(null) val chatsContext = ChatsContext(null) - val reportsChatsContext = ChatsContext(MsgContentTag.Report) + 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 groupMembers = mutableStateOf>(emptyList()) val groupMembersIndexes = mutableStateOf>(emptyMap()) + val membersLoaded = mutableStateOf(false) // Chat Tags val userTags = mutableStateOf(emptyList()) @@ -94,6 +134,7 @@ object ChatModel { // set when app is opened via contact or invitation URI (rhId, uri) val appOpenUrl = mutableStateOf?>(null) + val appOpenUrlConnecting = mutableStateOf(false) // Needed to check for bottom nav bar and to apply or not navigation bar color on Android val newChatSheetVisible = mutableStateOf(false) @@ -167,35 +208,7 @@ object ChatModel { // return true if you handled the click var centerPanelBackgroundClickHandler: (() -> Boolean)? = null - fun chatsForContent(contentTag: MsgContentTag?): State> = when(contentTag) { - null -> chatsContext.chats - MsgContentTag.Report -> reportsChatsContext.chats - else -> TODO() - } - - fun chatItemsForContent(contentTag: MsgContentTag?): State> = when(contentTag) { - null -> chatsContext.chatItems - MsgContentTag.Report -> reportsChatsContext.chatItems - else -> TODO() - } - - fun chatStateForContent(contentTag: MsgContentTag?): ActiveChatState = when(contentTag) { - null -> chatsContext.chatState - MsgContentTag.Report -> reportsChatsContext.chatState - else -> TODO() - } - - fun chatItemsChangesListenerForContent(contentTag: MsgContentTag?): ChatItemsChangesListener? = when(contentTag) { - null -> chatsContext.chatItemsChangesListener - MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener - else -> TODO() - } - - fun setChatItemsChangeListenerForContent(listener: ChatItemsChangesListener?, contentTag: MsgContentTag?) = when(contentTag) { - null -> chatsContext.chatItemsChangesListener = listener - MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener = listener - else -> TODO() - } + fun addressShortLinkDataSet(): Boolean = userAddress.value?.shortLinkDataSet ?: true fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value @@ -245,15 +258,12 @@ object ChatModel { } } - if (activeChatTagFilter.value is ActiveFilter.PresetTag && - (newPresetTags[(activeChatTagFilter.value as ActiveFilter.PresetTag).tag] ?: 0) == 0) { - activeChatTagFilter.value = null - } - presetTags.clear() presetTags.putAll(newPresetTags) unreadTags.clear() unreadTags.putAll(newUnreadTags) + + clearActiveChatFilterIfNeeded() } fun updateChatFavorite(favorite: Boolean, wasFavorite: Boolean) { @@ -263,9 +273,7 @@ object ChatModel { presetTags[PresetTagKind.FAVORITES] = (count ?: 0) + 1 } else if (!favorite && wasFavorite && count != null) { presetTags[PresetTagKind.FAVORITES] = maxOf(0, count - 1) - if (activeChatTagFilter.value == ActiveFilter.PresetTag(PresetTagKind.FAVORITES) && (presetTags[PresetTagKind.FAVORITES] ?: 0) == 0) { - activeChatTagFilter.value = null - } + clearActiveChatFilterIfNeeded() } } @@ -286,6 +294,7 @@ object ChatModel { } } } + clearActiveChatFilterIfNeeded() } fun moveChatTagUnread(chat: Chat, oldTags: List?, newTags: List) { @@ -325,37 +334,40 @@ object ChatModel { } } - // running everything inside the block on main thread. Make sure any heavy computation is moved to a background thread - suspend fun withChats(contentTag: MsgContentTag? = null, action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) { - when { - contentTag == null -> chatsContext.action() - contentTag == MsgContentTag.Report -> reportsChatsContext.action() - else -> TODO() - } - } - - suspend fun withReportsChatsIfOpen(action: suspend ChatsContext.() -> T) = withContext(Dispatchers.Main) { - if (ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) { - reportsChatsContext.action() - } - } - - class ChatsContext(private val contentTag: MsgContentTag?) { + class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) { val chats = mutableStateOf(SnapshotStateList()) - /** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on. + /** 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 [addAndNotify]. + * 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 - var chatItemsChangesListener: ChatItemsChangesListener? = null 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 } + val contentTag: MsgContentTag? = + when (secondaryContextFilter) { + null -> null + is SecondaryContextFilter.GroupChatScopeContext -> null + is SecondaryContextFilter.MsgContentTagContext -> secondaryContextFilter.contentTag + } + + val groupScopeInfo: GroupChatScopeInfo? = + when (secondaryContextFilter) { + null -> null + is SecondaryContextFilter.GroupChatScopeContext -> secondaryContextFilter.groupScopeInfo + is SecondaryContextFilter.MsgContentTagContext -> null + } + + val isUserSupportChat: Boolean = + when (groupScopeInfo) { + null -> false + is GroupChatScopeInfo.MemberSupport -> groupScopeInfo.groupMember_ == null + } + suspend fun addChat(chat: Chat) { chats.add(index = 0, chat) popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = 0) @@ -388,6 +400,8 @@ object ChatModel { ) ) } + } else if (currentCInfo is ChatInfo.Group && newCInfo is ChatInfo.Group && newCInfo.groupChatScope != null) { + newCInfo = newCInfo.copy(groupInfo = newCInfo.groupInfo, groupChatScope = null) } chats[i] = chats[i].copy(chatInfo = newCInfo) } @@ -410,7 +424,7 @@ object ChatModel { updateContact(rhId, updatedContact) } - suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) + suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo, groupChatScope = null)) private suspend fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { if (hasChat(rhId, cInfo.id)) { @@ -421,8 +435,19 @@ object ChatModel { } } - fun updateChats(newChats: List) { - chats.replaceAll(newChats) + fun updateChats(newChats: List, keepingChatId: String? = null) { + if (keepingChatId != null) { + val chatToKeep = getChat(keepingChatId) + val indexToRemove = newChats.indexOfFirst { it.id == keepingChatId } + if (chatToKeep != null && indexToRemove != -1) { + val remainingNewChats = newChats.toMutableList().apply { removeAt(indexToRemove) } + chats.replaceAll(listOf(chatToKeep) + remainingNewChats) + } else { + chats.replaceAll(newChats) + } + } else { + chats.replaceAll(newChats) + } popChatCollector.clear() val cId = chatId.value @@ -441,145 +466,196 @@ object ChatModel { addChat(chat) } } - 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) + + 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 remIndex: Int + val rem: ChatItem? + chatItems.value = SnapshotStateList().apply { + addAll(chatItems.value) + remIndex = lastIndex + rem = removeLastOrNull() } - // update previews + if (rem != null) { + val removed = Triple(rem.id, remIndex, rem.isRcvNew) + chatState.itemsRemoved(listOf(removed), chatItems.value) + } + } + + suspend fun addChatItem(rhId: Long?, chatInfo: ChatInfo, cItem: ChatItem) { + // updates membersRequireAttention + val cInfo = if (chatInfo is ChatInfo.Direct && chatInfo.chatDeleted) { + // mark chat non deleted + val updatedContact = chatInfo.contact.copy(chatDeleted = false) + ChatInfo.Direct(updatedContact) + } else { + chatInfo + } + updateChatInfo(rhId, cInfo) + // update chat list 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 + chat = chatsContext.chats[i] + // update preview (for chat from main scope to show new items for invitee in pending status) + if (cInfo.groupChatScope() == null || cInfo.groupInfo_?.membership?.memberPending == true) { + 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 { - currentPreviewItem + cItem } - } else { - cItem } - } - else -> cItem - } - val wasUnread = chat.unreadTag - chats[i] = chat.copy( - chatItems = arrayListOf(newPreviewItem), - chatStats = - if (cItem.meta.itemStatus is CIStatus.RcvNew) { - increaseUnreadCounter(rhId, currentUser.value!!) - chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1) - } - else - chat.chatStats - ) - updateChatTagReadNoContentTag(chats[i], wasUnread) + else -> cItem + } + val wasUnread = chat.unreadTag + chatsContext.chats[i] = chat.copy( + chatItems = arrayListOf(newPreviewItem), + chatStats = + if (cItem.meta.itemStatus is CIStatus.RcvNew) { + increaseUnreadCounter(rhId, currentUser.value!!) + chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions) + } else + chat.chatStats + ) + updateChatTagReadInPrimaryContext(chatsContext.chats[i], wasUnread) + } + // pop chat if (appPlatform.isDesktop && cItem.chatDir.sent) { - reorderChat(chats[i], 0) + reorderChat(chatsContext.chats[i], 0) } else { popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i) } } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + if (cInfo.groupChatScope() == null) { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = emptyList())) + } } + // add to current scope withContext(Dispatchers.Main) { - // add to current chat - if (chatId.value == cInfo.id) { + if (chatItemBelongsToScope(cInfo, cItem)) { // 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.addAndNotify(kotlin.math.max(0, chatItems.value.lastIndex), cItem, contentTag) + addToChatItems(kotlin.math.max(0, chatItems.value.lastIndex), cItem) } else { - chatItems.addAndNotify(cItem, contentTag) + addToChatItems(cItem) } } } } } - 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) + private fun chatItemBelongsToScope(cInfo: ChatInfo, cItem: ChatItem): Boolean = + when (secondaryContextFilter) { + null -> + chatId.value == cInfo.id && cInfo.groupChatScope() == null + is SecondaryContextFilter.GroupChatScopeContext -> { + val cInfoScope = cInfo.groupChatScope() + if (cInfoScope != null) { + chatId.value == cInfo.id && sameChatScope(cInfoScope, secondaryContextFilter.groupScopeInfo.toChatScope()) + } else { + false } } - res = false - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - res = true + is SecondaryContextFilter.MsgContentTagContext -> + chatId.value == cInfo.id && cItem.isReport } - return withContext(Dispatchers.Main) { - // update current chat - if (chatId.value == cInfo.id) { + + suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean { + var itemAdded = false + // update chat list + if (cInfo.groupChatScope() == null) { + 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(cItem)) + if (pItem.isRcvNew && !cItem.isRcvNew) { + // status changed from New to Read, update counter + decreaseCounterInPrimaryContext(rhId, cInfo.id) + } + } + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + itemAdded = true + } + } + // update current scope + withContext(Dispatchers.Main) { + if (chatItemBelongsToScope(cInfo, cItem)) { 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)) + val oldStatus = items[itemIndex].meta.itemStatus + val newStatus = cItem.meta.itemStatus + val ci = if (shouldKeepOldSndCIStatus(oldStatus, newStatus)) { + cItem.copy(meta = cItem.meta.copy(itemStatus = oldStatus)) } else { cItem } - chatItems.addAndNotify(ci, contentTag) - true + items[itemIndex] = ci + } else { + addToChatItems(cItem) + itemAdded = true } - } else { - res } } + return itemAdded } - suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { + suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null, atIndex: Int? = null) { withContext(Dispatchers.Main) { - if (chatId.value == cInfo.id) { + if (chatItemBelongsToScope(cInfo, cItem)) { val items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } + 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)) + // update chat list + if (cInfo.groupChatScope() == null) { + if (cItem.isRcvNew) { + decreaseCounterInPrimaryContext(rhId, cInfo.id) + } + // update preview + 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) { + // remove from current scope + if (chatItemBelongsToScope(cInfo, cItem)) { 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) @@ -590,24 +666,62 @@ object ChatModel { } } + 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 + } + + val cInfo = ChatInfo.Group(groupInfo, groupChatScope = null) // TODO [knocking] review + 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(chats[i]) + markChatTagRead(chatBefore) } // clear current chat if (chatId.value == cInfo.id) { - chatItemStatuses.clear() chatItems.clearAndNotify() } } - val popChatCollector = PopChatCollector(contentTag) + val popChatCollector = PopChatCollector(this) - class PopChatCollector(contentTag: MsgContentTag?) { + // 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() @@ -617,8 +731,8 @@ object ChatModel { subject .throttleLatest(2000) .collect { - withChats(contentTag) { - chats.replaceAll(popCollectedChats()) + withContext(Dispatchers.Main) { + chatsCtx.chats.replaceAll(popCollectedChats()) } } } @@ -656,7 +770,7 @@ object ChatModel { } fun markChatItemsRead(remoteHostId: Long?, id: ChatId, itemIds: List? = null) { - val markedRead = markItemsReadInCurrentChat(id, itemIds) + val (markedRead, mentionsMarkedRead) = markItemsReadInCurrentChat(id, itemIds) // update preview val chatIdx = getChatIndex(remoteHostId, id) if (chatIdx >= 0) { @@ -665,17 +779,19 @@ object ChatModel { 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) + chatStats = chat.chatStats.copy(unreadCount = unreadCount, unreadMentions = unreadMentions) ) - updateChatTagReadNoContentTag(chats[chatIdx], wasUnread) + updateChatTagReadInPrimaryContext(chats[chatIdx], wasUnread) } } } - private fun markItemsReadInCurrentChat(id: ChatId, itemIds: List? = null): Int { + private fun markItemsReadInCurrentChat(id: ChatId, itemIds: List? = null): Pair { var markedRead = 0 + var mentionsMarkedRead = 0 if (chatId.value == id) { val items = chatItems.value var i = items.lastIndex @@ -693,6 +809,9 @@ object ChatModel { } 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 @@ -701,14 +820,14 @@ object ChatModel { } i-- } - chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items) + chatState.itemsRead(if (itemIds != null) markedReadIds else null, items) } - return markedRead + return markedRead to mentionsMarkedRead } - private fun decreaseCounterInChatNoContentTag(rhId: Long?, chatId: ChatId) { + private fun decreaseCounterInPrimaryContext(rhId: Long?, chatId: ChatId) { // updates anything only in main ChatView, not GroupReportsView or anything else from the future - if (contentTag != null) return + if (secondaryContextFilter != null) return val chatIndex = getChatIndex(rhId, chatId) if (chatIndex == -1) return @@ -722,7 +841,7 @@ object ChatModel { unreadCount = unreadCount, ) ) - updateChatTagReadNoContentTag(chats[chatIndex], wasUnread) + updateChatTagReadInPrimaryContext(chats[chatIndex], wasUnread) } fun removeChat(rhId: Long?, id: String) { @@ -742,6 +861,11 @@ object ChatModel { } // 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 @@ -786,16 +910,16 @@ object ChatModel { } fun increaseUnreadCounter(rhId: Long?, user: UserLike) { - changeUnreadCounterNoContentTag(rhId, user, 1) + changeUnreadCounterInPrimaryContext(rhId, user, 1) } fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) { - changeUnreadCounterNoContentTag(rhId, user, -by) + changeUnreadCounterInPrimaryContext(rhId, user, -by) } - private fun changeUnreadCounterNoContentTag(rhId: Long?, user: UserLike, by: Int) { + private fun changeUnreadCounterInPrimaryContext(rhId: Long?, user: UserLike, by: Int) { // updates anything only in main ChatView, not GroupReportsView or anything else from the future - if (contentTag != null) return + if (secondaryContextFilter != null) return val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId } if (i != -1) { @@ -803,9 +927,9 @@ object ChatModel { } } - fun updateChatTagReadNoContentTag(chat: Chat, wasUnread: Boolean) { + fun updateChatTagReadInPrimaryContext(chat: Chat, wasUnread: Boolean) { // updates anything only in main ChatView, not GroupReportsView or anything else from the future - if (contentTag != null) return + if (secondaryContextFilter != null) return val tags = chat.chatInfo.chatTags ?: return val nowUnread = chat.unreadTag @@ -815,21 +939,21 @@ object ChatModel { unreadTags[tag] = (unreadTags[tag] ?: 0) + 1 } } else if (!nowUnread && wasUnread) { - markChatTagReadNoContentTag_(chat, tags) + markChatTagReadInPrimaryContext_(chat, tags) } } fun markChatTagRead(chat: Chat) { if (chat.unreadTag) { chat.chatInfo.chatTags?.let { tags -> - markChatTagReadNoContentTag_(chat, tags) + markChatTagReadInPrimaryContext_(chat, tags) } } } - private fun markChatTagReadNoContentTag_(chat: Chat, tags: List) { + private fun markChatTagReadInPrimaryContext_(chat: Chat, tags: List) { // updates anything only in main ChatView, not GroupReportsView or anything else from the future - if (contentTag != null) return + if (secondaryContextFilter != null) return for (tag in tags) { val count = unreadTags[tag] @@ -843,8 +967,8 @@ object ChatModel { changeGroupReportsCounter(rhId, chatId, 1) } - fun decreaseGroupReportsCounter(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) { @@ -861,16 +985,26 @@ object ChatModel { 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) + changeGroupReportsTagInPrimaryContext(by) } } - private fun changeGroupReportsTagNoContentTag(by: Int = 0) { - if (by == 0 || contentTag != null) return - presetTags[PresetTagKind.GROUP_REPORTS] = (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by + private fun changeGroupReportsTagInPrimaryContext(by: Int = 0) { + if (by == 0 || secondaryContextFilter != null) return + presetTags[PresetTagKind.GROUP_REPORTS] = kotlin.math.max(0, (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by) + clearActiveChatFilterIfNeeded() } } + 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) { val current = currentUser.value ?: return val updated = current.copy( @@ -898,17 +1032,17 @@ object ChatModel { suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) - withChats { - chatItems.addAndNotify(cItem, contentTag = null) + withContext(Dispatchers.Main) { + chatsContext.addToChatItems(cItem) } return cItem } fun removeLiveDummy() { - if (chatItemsForContent(null).value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + if (chatsContext.chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { withApi { - withChats { - chatItems.removeLastAndNotify(contentTag = null) + withContext(Dispatchers.Main) { + chatsContext.removeLastChatItems() } } } @@ -980,9 +1114,9 @@ object ChatModel { fun replaceConnReqView(id: String, withId: String) { if (id == showingInvitation.value?.connId) { withApi { - withChats { + withContext(Dispatchers.Main) { showingInvitation.value = null - chatItems.clearAndNotify() + chatsContext.chatItems.clearAndNotify() chatModel.chatId.value = withId } } @@ -993,9 +1127,9 @@ object ChatModel { fun dismissConnReqView(id: String) = withApi { if (id == showingInvitation.value?.connId) { - withChats { + withContext(Dispatchers.Main) { showingInvitation.value = null - chatItems.clearAndNotify() + chatsContext.chatItems.clearAndNotify() chatModel.chatId.value = null } // Close NewChatView @@ -1009,21 +1143,6 @@ object ChatModel { showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true) } - fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) { - val conn = contact.activeConn - if (conn != null) { - networkStatuses[conn.agentConnId] = status - } - } - - fun contactNetworkStatus(contact: Contact): NetworkStatus { - val conn = contact.activeConn - return if (conn != null) - networkStatuses[conn.agentConnId] ?: NetworkStatus.Unknown() - else - NetworkStatus.Unknown() - } - fun addTerminalItem(item: TerminalItem) { val maxItems = if (appPreferences.developerTools.get()) 500 else 200 if (terminalsVisible.isNotEmpty()) { @@ -1046,18 +1165,9 @@ object ChatModel { fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true } -interface ChatItemsChangesListener { - // pass null itemIds if the whole chat now read - fun read(itemIds: Set?, newItems: List) - fun added(item: Pair, index: Int) - // itemId, index in old chatModel.chatItems (before the update), isRcvNew (is item unread or not) - fun removed(itemIds: List>, newItems: List) - fun cleared() -} - data class ShowingInvitation( val connId: String, - val connReq: String, + val connLink: CreatedConnLink, val connChatUsed: Boolean, val conn: PendingContactConnection ) @@ -1070,6 +1180,28 @@ enum class ChatType(val type: String) { ContactConnection(":"); } +sealed class GroupChatScope { + class MemberSupport(val groupMemberId_: Long?): GroupChatScope() +} + +fun sameChatScope(scope1: GroupChatScope, scope2: GroupChatScope) = + scope1 is GroupChatScope.MemberSupport + && scope2 is GroupChatScope.MemberSupport + && scope1.groupMemberId_ == scope2.groupMemberId_ + +@Serializable +sealed class GroupChatScopeInfo { + @Serializable @SerialName("memberSupport") data class MemberSupport(val groupMember_: GroupMember?) : GroupChatScopeInfo() + + fun toChatScope(): GroupChatScope = + when (this) { + is MemberSupport -> when (groupMember_) { + null -> GroupChatScope.MemberSupport(groupMemberId_ = null) + else -> GroupChatScope.MemberSupport(groupMemberId_ = groupMember_.groupMemberId) + } + } +} + @Serializable data class User( val remoteHostId: Long?, @@ -1083,11 +1215,13 @@ data class User( override val showNtfs: Boolean, val sendRcptsContacts: Boolean, val sendRcptsSmallGroups: Boolean, + val autoAcceptMemberContacts: Boolean, val viewPwdHash: UserPwdHash?, val uiThemes: ThemeModeOverrides? = null, ): NamedChat, UserLike { override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName + override val shortDescr: String? get() = profile.shortDescr override val image: String? get() = profile.image override val localAlias: String = "" @@ -1111,6 +1245,7 @@ data class User( showNtfs = true, sendRcptsContacts = true, sendRcptsSmallGroups = false, + autoAcceptMemberContacts = false, viewPwdHash = null, uiThemes = null, ) @@ -1157,6 +1292,7 @@ typealias ChatId = String interface NamedChat { val displayName: String val fullName: String + val shortDescr: String? val image: String? val localAlias: String val chatViewName: String @@ -1175,8 +1311,9 @@ interface SomeChat { val apiId: Long val ready: Boolean val chatDeleted: Boolean - val sendMsgEnabled: Boolean - val ntfsEnabled: Boolean + val nextConnect: Boolean + val nextConnectPrepared: Boolean + val profileChangeProhibited: Boolean val incognito: Boolean fun featureEnabled(feature: ChatFeature): Boolean val timedMessagesTTL: Int? @@ -1200,32 +1337,27 @@ 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 unreadTag: Boolean get() = chatInfo.ntfsEnabled && (chatStats.unreadCount > 0 || chatStats.unreadChat) - val id: String get() = chatInfo.id + val supportUnreadCount: Int get() = when (chatInfo) { + is ChatInfo.Group -> + if (chatInfo.groupInfo.canModerate) { + chatInfo.groupInfo.membersRequireAttention + } else { + chatInfo.groupInfo.membership.supportChat?.unread ?: 0 + } + else -> 0 + } + 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 } @@ -1233,9 +1365,11 @@ data class Chat( @Serializable 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 ) @@ -1259,8 +1393,9 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contact.apiId override val ready get() = contact.ready override val chatDeleted get() = contact.chatDeleted - override val sendMsgEnabled get() = contact.sendMsgEnabled - override val ntfsEnabled get() = contact.ntfsEnabled + override val nextConnect get() = contact.nextConnect + override val nextConnectPrepared get() = contact.nextConnectPrepared + override val profileChangeProhibited get() = contact.profileChangeProhibited override val incognito get() = contact.incognito override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL @@ -1268,6 +1403,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val updatedAt get() = contact.updatedAt override val displayName get() = contact.displayName override val fullName get() = contact.fullName + override val shortDescr get() = contact.profile.shortDescr override val image get() = contact.image override val localAlias: String get() = contact.localAlias override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase) @@ -1278,15 +1414,16 @@ sealed class ChatInfo: SomeChat, NamedChat { } @Serializable @SerialName("group") - data class Group(val groupInfo: GroupInfo): ChatInfo() { + data class Group(val groupInfo: GroupInfo, val groupChatScope: GroupChatScopeInfo?): ChatInfo() { override val chatType get() = ChatType.Group override val localDisplayName get() = groupInfo.localDisplayName 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 sendMsgEnabled get() = groupInfo.sendMsgEnabled - override val ntfsEnabled get() = groupInfo.ntfsEnabled + override val nextConnect get() = groupInfo.nextConnect + override val nextConnectPrepared get() = groupInfo.nextConnectPrepared + override val profileChangeProhibited get() = groupInfo.profileChangeProhibited override val incognito get() = groupInfo.incognito override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature) override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL @@ -1294,11 +1431,12 @@ sealed class ChatInfo: SomeChat, NamedChat { override val updatedAt get() = groupInfo.updatedAt override val displayName get() = groupInfo.displayName override val fullName get() = groupInfo.fullName + override val shortDescr get() = groupInfo.groupProfile.shortDescr override val image get() = groupInfo.image override val localAlias get() = groupInfo.localAlias companion object { - val sampleData = Group(GroupInfo.sampleData) + val sampleData = Group(GroupInfo.sampleData, groupChatScope = null) } } @@ -1310,8 +1448,9 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = noteFolder.apiId override val ready get() = noteFolder.ready override val chatDeleted get() = noteFolder.chatDeleted - override val sendMsgEnabled get() = noteFolder.sendMsgEnabled - override val ntfsEnabled get() = noteFolder.ntfsEnabled + override val nextConnect get() = noteFolder.nextConnect + override val nextConnectPrepared get() = noteFolder.nextConnectPrepared + override val profileChangeProhibited get() = noteFolder.profileChangeProhibited override val incognito get() = noteFolder.incognito override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature) override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL @@ -1319,6 +1458,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val updatedAt get() = noteFolder.updatedAt override val displayName get() = noteFolder.displayName override val fullName get() = noteFolder.fullName + override val shortDescr get() = null override val image get() = noteFolder.image override val localAlias get() = noteFolder.localAlias @@ -1335,8 +1475,9 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready override val chatDeleted get() = contactRequest.chatDeleted - override val sendMsgEnabled get() = contactRequest.sendMsgEnabled - override val ntfsEnabled get() = contactRequest.ntfsEnabled + override val nextConnect get() = contactRequest.nextConnect + override val nextConnectPrepared get() = contactRequest.nextConnectPrepared + override val profileChangeProhibited get() = contactRequest.profileChangeProhibited override val incognito get() = contactRequest.incognito override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL @@ -1344,6 +1485,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val updatedAt get() = contactRequest.updatedAt override val displayName get() = contactRequest.displayName override val fullName get() = contactRequest.fullName + override val shortDescr get() = contactRequest.profile.shortDescr override val image get() = contactRequest.image override val localAlias get() = contactRequest.localAlias @@ -1360,8 +1502,9 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready override val chatDeleted get() = contactConnection.chatDeleted - override val sendMsgEnabled get() = contactConnection.sendMsgEnabled - override val ntfsEnabled get() = false + override val nextConnect get() = contactConnection.nextConnect + override val nextConnectPrepared get() = contactConnection.nextConnectPrepared + override val profileChangeProhibited get() = contactConnection.profileChangeProhibited override val incognito get() = contactConnection.incognito override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL @@ -1369,6 +1512,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val updatedAt get() = contactConnection.updatedAt override val displayName get() = contactConnection.displayName override val fullName get() = contactConnection.fullName + override val shortDescr get() = null override val image get() = contactConnection.image override val localAlias get() = contactConnection.localAlias @@ -1379,30 +1523,130 @@ 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 sendMsgEnabled get() = false - override val ntfsEnabled get() = false + override val nextConnect get() = false + override val nextConnectPrepared get() = false + override val profileChangeProhibited 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 shortDescr get() = null override val image get() = null override val localAlias get() = "" companion object { private val invalidChatName = generalGetString(MR.strings.invalid_chat) + private val idGenerator = AtomicLong(0) } } + val userCantSendReason: Pair? + get() { + when (this) { + is Direct -> { + if (contact.sendMsgToConnect) return null + if (contact.nextAcceptContactRequest) { return generalGetString(MR.strings.cant_send_message_generic) to null } + if (!contact.active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null + if (!contact.sndReady) { + return if (contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) { + generalGetString(MR.strings.cant_send_message_request_is_sent) to null + } else { + generalGetString(MR.strings.cant_send_message_contact_not_ready) to null + } + } + if (contact.activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null + if (contact.activeConn?.connDisabled == true) return generalGetString(MR.strings.cant_send_message_contact_disabled) to null + return null + } + is Group -> { + if (groupInfo.membership.memberActive) { + when (groupChatScope) { + null -> { + if (groupInfo.membership.memberPending) { + return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc) + } + if (groupInfo.membership.memberRole == GroupMemberRole.Observer) { + return generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc) + } + return null + } + is GroupChatScopeInfo.MemberSupport -> + if (groupChatScope.groupMember_ != null) { + if ( + groupChatScope.groupMember_.versionRange.maxVersion < GROUP_KNOCKING_VERSION + && !groupChatScope.groupMember_.memberPending + ) { + return generalGetString(MR.strings.cant_send_message_member_has_old_version) to null + } + return null + } else { + return null + } + } + } else if (groupInfo.nextConnectPrepared) { + return null + } else { + return when (groupInfo.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 + } + } + } + is Local -> + return null + is ContactRequest -> + return generalGetString(MR.strings.cant_send_message_generic) to null + is ContactConnection -> + return generalGetString(MR.strings.cant_send_message_generic) to null + is InvalidJSON -> + return generalGetString(MR.strings.cant_send_message_generic) to null + } + } + + val sendMsgEnabled get() = userCantSendReason == null + + val sndReady: Boolean get() = + when(this) { + is Direct -> contact.sndReady + is Group -> + groupInfo.membership.memberActive + && (groupChatScope != null || (!groupInfo.membership.memberPending && groupInfo.membership.memberRole != GroupMemberRole.Observer)) + is Local -> true + is ContactRequest -> false + is ContactConnection -> false + is InvalidJSON -> false + } + + fun groupChatScope(): GroupChatScope? = when (this) { + is Group -> groupChatScope?.toChatScope() + else -> null + } + + 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 @@ -1420,14 +1664,6 @@ sealed class ChatInfo: SomeChat, NamedChat { is InvalidJSON -> updatedAt } - val userCanSend: Boolean - get() = when (this) { - is ChatInfo.Direct -> true - is ChatInfo.Group -> groupInfo.membership.memberRole >= GroupMemberRole.Member - is ChatInfo.Local -> true - else -> false - } - val chatTags: List? get() = when (this) { is Direct -> contact.chatTags @@ -1435,37 +1671,37 @@ sealed class ChatInfo: SomeChat, NamedChat { else -> null } - val contactCard: Boolean - get() = when (this) { - is Direct -> contact.activeConn == null && contact.profile.contactLink != null && contact.active - else -> false - } + val nextNtfMode: MsgFilter? get() = this.chatSettings?.enableNtfs?.nextMode(mentions = this.hasMentions) + + val hasMentions: Boolean get() = this is Group + + val useCommands: Boolean get() = when(this) { + is Direct -> contact.isBot + is Group -> groupInfo.groupProfile.groupPreferences?.commands?.isNotEmpty() ?: false + else -> false } -@Serializable -sealed class NetworkStatus { - val statusString: String get() = - when (this) { - is Connected -> generalGetString(MR.strings.server_connected) - is Error -> generalGetString(MR.strings.server_error) - else -> generalGetString(MR.strings.server_connecting) - } - val statusExplanation: String get() = - when (this) { - is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact) - is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), connectionError) - else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages) + val menuCommands: List get() = when(this) { + is Direct -> + if (contact.isBot) contact.profile.preferences?.commands ?: emptyList() + else emptyList() + is Group -> groupInfo.groupProfile.groupPreferences?.commands ?: emptyList() + else -> emptyList() + } + + val contactCard: Boolean + get() = when (this) { + is Direct -> contact.isContactCard + else -> false } - @Serializable @SerialName("unknown") class Unknown: NetworkStatus() - @Serializable @SerialName("connected") class Connected: NetworkStatus() - @Serializable @SerialName("disconnected") class Disconnected: NetworkStatus() - @Serializable @SerialName("error") class Error(val connectionError: String): NetworkStatus() + val groupInfo_: GroupInfo? + get() = when (this) { + is Group -> groupInfo + else -> null + } } -@Serializable -data class ConnNetworkStatus(val agentConnId: String, val networkStatus: NetworkStatus) - @Serializable data class Contact( val contactId: Long, @@ -1481,8 +1717,11 @@ data class Contact( override val createdAt: Instant, override val updatedAt: Instant, val chatTs: Instant?, + val preparedContact: PreparedContact?, + val contactRequestId: Long?, val contactGroupMemberId: Long? = null, val contactGrpInvSent: Boolean, + val groupDirectInv: GroupDirectInvitation? = null, val chatTags: List, val chatItemTTL: Long?, override val chatDeleted: Boolean, @@ -1494,26 +1733,28 @@ data class Contact( 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() = ( - sndReady - && active - && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false) - && !(activeConn?.connDisabled ?: true) - ) - || nextSendGrpInv + override val nextConnect get() = sendMsgToConnect val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent - override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All + override val nextConnectPrepared get() = active && preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared) + override val profileChangeProhibited get() = activeConn != null + val nextAcceptContactRequest get() = + active && + (contactRequestId != null || groupDirectInv != null) && + (activeConn == null || activeConn.connStatus == ConnStatus.New || activeConn.connStatus == ConnStatus.Prepared) + val sendMsgToConnect get() = nextSendGrpInv || nextConnectPrepared override val incognito get() = contactConnIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser ChatFeature.Reactions -> mergedPreferences.reactions.enabled.forUser ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser + ChatFeature.Files -> mergedPreferences.files.enabled.forUser ChatFeature.Calls -> mergedPreferences.calls.enabled.forUser } override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null } override val displayName get() = localAlias.ifEmpty { profile.displayName } override val fullName get() = profile.fullName + override val shortDescr get() = profile.shortDescr override val image get() = profile.image val contactLink: String? = profile.contactLink override val localAlias get() = profile.localAlias @@ -1524,7 +1765,6 @@ data class Contact( return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s) } - val directOrUsed: Boolean get() = if (activeConn != null) { (activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed @@ -1532,13 +1772,22 @@ data class Contact( true } - val contactConnIncognito = + val isContactCard: Boolean get() = + (activeConn == null || activeConn.connStatus == ConnStatus.Prepared) && profile.contactLink != null && active && preparedContact == null && contactRequestId == null + + val isBot: Boolean get() = profile.peerType == ChatPeerType.Bot + + val contactConnIncognito: Boolean get() = activeConn?.customUserProfileId != null + val chatIconName: ImageResource get() = + if (isBot) MR.images.ic_cube else MR.images.ic_account_circle_filled + fun allowsFeature(feature: ChatFeature): Boolean = when (feature) { ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO + ChatFeature.Files -> mergedPreferences.files.contactPreference.allow != FeatureAllowed.NO ChatFeature.Reactions -> mergedPreferences.reactions.contactPreference.allow != FeatureAllowed.NO ChatFeature.Calls -> mergedPreferences.calls.contactPreference.allow != FeatureAllowed.NO } @@ -1548,6 +1797,7 @@ data class Contact( ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Reactions -> mergedPreferences.reactions.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO + ChatFeature.Files -> mergedPreferences.files.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO } @@ -1565,6 +1815,8 @@ data class Contact( createdAt = Clock.System.now(), updatedAt = Clock.System.now(), chatTs = Clock.System.now(), + preparedContact = null, + contactRequestId = null, contactGrpInvSent = false, chatDeleted = false, uiThemes = null, @@ -1580,6 +1832,30 @@ data class NavigationInfo( val afterTotal: Int = 0 ) +@Serializable +data class PreparedContact ( + val connLinkToConnect: CreatedConnLink, + val uiConnLinkType: ConnectionMode +) + +@Serializable +data class GroupDirectInvitation ( + val groupDirectInvLink: String, + val fromGroupId_: Long?, + val fromGroupMemberId_: Long?, + val fromGroupMemberConnId_: Long?, + val groupDirectInvStartedConnection: Boolean +) { + val memberRemoved: Boolean + get() = fromGroupId_ == null || fromGroupMemberId_ == null || fromGroupMemberConnId_ == null +} + +@Serializable +enum class ConnectionMode { + @SerialName("inv") Inv, + @SerialName("con") Con +} + @Serializable enum class ContactStatus { @SerialName("active") Active, @@ -1597,12 +1873,6 @@ class ContactRef( val id: ChatId get() = "@$contactId" } -@Serializable -class ContactSubStatus( - val contact: Contact, - val contactError: ChatError? = null -) - @Serializable data class Connection( val connId: Long, @@ -1647,22 +1917,25 @@ data class SecurityCode(val securityCode: String, val verifiedAt: Instant) data class Profile( override val displayName: String, override val fullName: String, + override val shortDescr: String?, override val image: String? = null, override val localAlias : String = "", val contactLink: String? = null, - val preferences: ChatPreferences? = null + val preferences: ChatPreferences? = null, + val peerType: ChatPeerType? = null ): NamedChat { val profileViewName: String get() { return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, image, localAlias, contactLink, preferences) + fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) companion object { val sampleData = Profile( displayName = "alice", - fullName = "Alice" + fullName = "Alice", + shortDescr = null, ) } } @@ -1672,26 +1945,35 @@ data class LocalProfile( val profileId: Long, override val displayName: String, override val fullName: String, + override val shortDescr: String?, override val image: String? = null, override val localAlias: String, val contactLink: String? = null, - val preferences: ChatPreferences? = null + val preferences: ChatPreferences? = null, + val peerType: ChatPeerType? = null ): NamedChat { val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toProfile(): Profile = Profile(displayName, fullName, image, localAlias, contactLink, preferences) + fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) companion object { val sampleData = LocalProfile( profileId = 1L, displayName = "alice", fullName = "Alice", + shortDescr = null, preferences = ChatPreferences.sampleData, localAlias = "" ) } } +@Serializable +enum class ChatPeerType { + @SerialName("human") Human, + @SerialName("bot") Bot +} + @Serializable data class UserProfileUpdateSummary( val updateSuccesses: Int, @@ -1721,12 +2003,13 @@ data class GroupInfo ( 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 preparedGroup: PreparedGroup?, val uiThemes: ThemeModeOverrides? = null, + val membersRequireAttention: Int, val chatTags: List, val chatItemTTL: Long?, override val localAlias: String, @@ -1735,27 +2018,30 @@ data class GroupInfo ( override val id get() = "#$groupId" override val apiId get() = groupId override val ready get() = membership.memberActive + override val nextConnect get() = nextConnectPrepared + override val nextConnectPrepared = if (preparedGroup != null) !preparedGroup.connLinkStartedConnection else false + override val profileChangeProhibited get() = preparedGroup?.connLinkPreparedConnection ?: false override val chatDeleted get() = false - override val sendMsgEnabled get() = membership.memberActive - override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All override val incognito get() = membership.memberIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on ChatFeature.Reactions -> fullGroupPreferences.reactions.on ChatFeature.Voice -> fullGroupPreferences.voice.on(membership) + ChatFeature.Files -> fullGroupPreferences.files.on(membership) ChatFeature.Calls -> false } override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null } override val displayName get() = localAlias.ifEmpty { groupProfile.displayName } override val fullName get() = groupProfile.fullName + override val shortDescr get() = groupProfile.shortDescr override val image get() = groupProfile.image val isOwner: Boolean get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent val canDelete: Boolean - get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrent + get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrentOrPending val canAddMembers: Boolean get() = membership.memberRole >= GroupMemberRole.Admin && membership.memberActive @@ -1763,6 +2049,28 @@ data class GroupInfo ( val canModerate: Boolean get() = membership.memberRole >= GroupMemberRole.Moderator && membership.memberActive + val chatIconName: ImageResource + get() = when (businessChat?.chatType) { + null -> MR.images.ic_supervised_user_circle_filled + BusinessChatType.Business -> MR.images.ic_work_filled_padded + BusinessChatType.Customer -> MR.images.ic_account_circle_filled + } + + 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, @@ -1770,12 +2078,13 @@ 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(), + preparedGroup = null, uiThemes = null, + membersRequireAttention = 0, chatTags = emptyList(), localAlias = "", chatItemTTL = null @@ -1783,6 +2092,13 @@ data class GroupInfo ( } } +@Serializable +data class PreparedGroup ( + val connLinkToConnect: CreatedConnLink, + val connLinkPreparedConnection: Boolean, + val connLinkStartedConnection: Boolean +) + @Serializable data class GroupRef(val groupId: Long, val localDisplayName: String) @@ -1790,19 +2106,55 @@ data class GroupRef(val groupId: Long, val localDisplayName: String) data class GroupProfile ( override val displayName: String, override val fullName: String, + override val shortDescr: String?, val description: String? = null, override val image: String? = null, override val localAlias: String = "", - val groupPreferences: GroupPreferences? = null + val groupPreferences: GroupPreferences? = null, + val memberAdmission: GroupMemberAdmission? = null ): NamedChat { companion object { val sampleData = GroupProfile( displayName = "team", - fullName = "My Team" + fullName = "My Team", + shortDescr = null, ) } } +@Serializable +data class GroupMemberAdmission( + val review: MemberCriteria? = null, +) { + companion object { + val sampleData = GroupMemberAdmission( + review = null, + ) + } +} + +@Serializable +enum class MemberCriteria { + @SerialName("all") All; + + val text: String + get() = when(this) { + MemberCriteria.All -> generalGetString(MR.strings.member_criteria_all) + } +} + +@Serializable +data class ContactShortLinkData ( + val profile: Profile, + val message: MsgContent?, + val business: Boolean +) + +@Serializable +data class GroupShortLinkData ( + val groupProfile: GroupProfile +) + @Serializable data class BusinessChatInfo ( val chatType: BusinessChatType, @@ -1831,7 +2183,9 @@ data class GroupMember ( val memberProfile: LocalProfile, val memberContactId: Long? = null, val memberContactProfileId: Long, - var activeConn: Connection? = null + var activeConn: Connection? = null, + val supportChat: GroupSupportChat? = null, + val memberChatVRange: VersionRange ): NamedChat { val id: String get() = "#$groupId @$groupMemberId" val ready get() = activeConn?.connStatus == ConnStatus.Ready @@ -1845,9 +2199,10 @@ data class GroupMember ( get() { val p = memberProfile val name = p.localAlias.ifEmpty { p.displayName } - return pastMember(name) + return unknownMember(name) } override val fullName: String get() = memberProfile.fullName + override val shortDescr: String? get() = memberProfile.shortDescr override val image: String? get() = memberProfile.image val contactLink: String? = memberProfile.contactLink val verified get() = activeConn?.connectionCode != null @@ -1859,22 +2214,44 @@ data class GroupMember ( get() { val p = memberProfile val name = p.localAlias.ifEmpty { p.displayName + (if (p.fullName == "" || p.fullName == p.displayName) "" else " / ${p.fullName}") } - return pastMember(name) + return unknownMember(name) } - private fun pastMember(name: String): String { - return if (memberStatus == GroupMemberStatus.MemUnknown) - String.format(generalGetString(MR.strings.past_member_vName), name) - else + private fun unknownMember(name: String): String { + return if (memberStatus == GroupMemberStatus.MemUnknown) { + if (memberId.startsWith(name)) { + // unknown member was created using memberId for name + String.format(generalGetString(MR.strings.past_member_vName), name) + } else { + // unknown member was created with name + name + } + } else name } + val localAliasAndFullName: String + get() { + val p = memberProfile + val fullName = p.displayName + (if (p.fullName == "" || p.fullName == p.displayName) "" else " / ${p.fullName}") + + val name = if (p.localAlias.isNotEmpty()) { + "${p.localAlias} ($fullName)" + } else { + fullName + } + return unknownMember(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.MemPendingReview -> true GroupMemberStatus.MemIntroduced -> false GroupMemberStatus.MemIntroInvited -> false GroupMemberStatus.MemAccepted -> false @@ -1885,11 +2262,14 @@ 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.MemPendingReview -> false GroupMemberStatus.MemIntroduced -> true GroupMemberStatus.MemIntroInvited -> true GroupMemberStatus.MemAccepted -> true @@ -1899,24 +2279,41 @@ data class GroupMember ( GroupMemberStatus.MemCreator -> true } + val memberPending: Boolean get() = when (this.memberStatus) { + GroupMemberStatus.MemPendingApproval -> true + GroupMemberStatus.MemPendingReview -> true + else -> false + } + + val memberCurrentOrPending: Boolean get() = + memberCurrent || memberPending + fun canBeRemoved(groupInfo: GroupInfo): Boolean { val userRole = groupInfo.membership.memberRole - return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft - && userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive + return userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive } fun canChangeRoleTo(groupInfo: GroupInfo): List? = - if (!canBeRemoved(groupInfo)) null + if (!canBeRemoved(groupInfo) || memberStatus == GroupMemberStatus.MemRemoved || memberStatus == GroupMemberStatus.MemLeft || memberPending) null else groupInfo.membership.memberRole.let { userRole -> 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 memberRole < GroupMemberRole.Moderator + && userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive + && !memberPending } + val supportChatNotRead: Boolean get() = + if (supportChat != null) + supportChat.memberAttention > 0 || supportChat.mentions > 0 || supportChat.unread > 0 + else + false + + val versionRange: VersionRange = activeConn?.peerChatVRange ?: memberChatVRange + val memberIncognito = memberProfile.profileId != memberContactProfileId companion object { @@ -1934,11 +2331,20 @@ data class GroupMember ( memberProfile = LocalProfile.sampleData, memberContactId = 1, memberContactProfileId = 1L, - activeConn = Connection.sampleData + activeConn = Connection.sampleData, + memberChatVRange = VersionRange(minVersion = 1, maxVersion = 15) ) } } +@Serializable +class GroupSupportChat ( + val chatTs: Instant, + val unread: Int, + val memberAttention: Int, + val mentions: Int +) + @Serializable data class GroupMemberSettings(val showMessages: Boolean) {} @@ -1964,7 +2370,7 @@ enum class GroupMemberRole(val memberRole: String) { @SerialName("owner") Owner("owner"); companion object { - val selectableRoles: List = listOf(Observer, Member, Admin, Owner) + val selectableRoles: List = listOf(Observer, Member, Moderator, Admin, Owner) } val text: String get() = when (this) { @@ -1988,11 +2394,14 @@ 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("pending_review") MemPendingReview, @SerialName("introduced") MemIntroduced, @SerialName("intro-inv") MemIntroInvited, @SerialName("accepted") MemAccepted, @@ -2002,11 +2411,14 @@ 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) + MemPendingReview -> generalGetString(MR.strings.group_member_status_pending_review) 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) @@ -2017,11 +2429,14 @@ 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) + MemPendingReview -> generalGetString(MR.strings.group_member_status_pending_review_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) @@ -2056,12 +2471,6 @@ class LinkPreview ( } } -@Serializable -class MemberSubError ( - val member: GroupMemberIds, - val memberError: ChatError -) - @Serializable class NoteFolder( val noteFolderId: Long, @@ -2076,13 +2485,22 @@ class NoteFolder( override val apiId get() = noteFolderId override val chatDeleted get() = false override val ready get() = true - override val sendMsgEnabled get() = true - override val ntfsEnabled get() = false + override val nextConnect get() = false + override val nextConnectPrepared get() = false + override val profileChangeProhibited get() = false override val incognito get() = false - override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice + override fun featureEnabled(feature: ChatFeature) = when (feature) { + ChatFeature.TimedMessages -> false + ChatFeature.FullDelete -> false + ChatFeature.Reactions -> false + ChatFeature.Voice -> true + ChatFeature.Files -> true + ChatFeature.Calls -> false + } override val timedMessagesTTL: Int? get() = null override val displayName get() = generalGetString(MR.strings.note_folder_local_display_name) override val fullName get() = "" + override val shortDescr get() = null override val image get() = null override val localAlias get() = "" override val localDisplayName: String get() = "" @@ -2109,17 +2527,19 @@ class UserContactRequest ( override val updatedAt: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.ContactRequest - override val id get() = "<@$contactRequestId" + override val id get() = contactRequestChatId(contactRequestId) override val apiId get() = contactRequestId override val chatDeleted get() = false override val ready get() = true - override val sendMsgEnabled get() = false - override val ntfsEnabled get() = false + override val nextConnect get() = false + override val nextConnectPrepared get() = false + override val profileChangeProhibited get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null override val displayName get() = profile.displayName override val fullName get() = profile.fullName + override val shortDescr get() = profile.shortDescr override val image get() = profile.image override val localAlias get() = "" @@ -2135,6 +2555,8 @@ class UserContactRequest ( } } +fun contactRequestChatId(contactRequestId: Long): String = "<@$contactRequestId" + @Serializable class PendingContactConnection( val pccConnId: Long, @@ -2143,7 +2565,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 @@ -2153,8 +2575,9 @@ class PendingContactConnection( override val apiId get() = pccConnId override val chatDeleted get() = false override val ready get() = false - override val sendMsgEnabled get() = false - override val ntfsEnabled get() = false + override val nextConnect get() = false + override val nextConnectPrepared get() = false + override val profileChangeProhibited get() = false override val incognito get() = customUserProfileId != null override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -2174,6 +2597,7 @@ class PendingContactConnection( } } override val fullName get() = "" + override val shortDescr get() = null override val image get() = null val initiated get() = (pccConnStatus.initiated ?: false) && !viaContactUri @@ -2255,6 +2679,30 @@ data class MemberReaction( 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, @@ -2269,6 +2717,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 @@ -2341,39 +2790,43 @@ data class ChatItem ( } val mergeCategory: CIMergeCategory? - get() = when (content) { - is CIContent.RcvChatFeature, - is CIContent.SndChatFeature, - is CIContent.RcvGroupFeature, - is CIContent.SndGroupFeature -> CIMergeCategory.ChatFeature - is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { - is RcvGroupEvent.UserRole, is RcvGroupEvent.UserDeleted, is RcvGroupEvent.GroupDeleted, is RcvGroupEvent.MemberCreatedContact -> null - else -> CIMergeCategory.RcvGroupEvent - } - is CIContent.SndGroupEventContent -> when (content.sndGroupEvent) { - is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft -> null - else -> CIMergeCategory.SndGroupEvent - } - else -> { - if (meta.itemDeleted == null) { - null - } else { - if (chatDir.sent) CIMergeCategory.SndItemDeleted else CIMergeCategory.RcvItemDeleted + get() = if (meta.itemDeleted != null) { + if (chatDir.sent) CIMergeCategory.SndItemDeleted else CIMergeCategory.RcvItemDeleted + } else { + when (content) { + is CIContent.RcvChatFeature, + is CIContent.SndChatFeature, + is CIContent.RcvGroupFeature, + is CIContent.SndGroupFeature -> CIMergeCategory.ChatFeature + is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { + is RcvGroupEvent.UserRole, + is RcvGroupEvent.UserDeleted, + is RcvGroupEvent.GroupDeleted, + is RcvGroupEvent.MemberCreatedContact, + is RcvGroupEvent.NewMemberPendingReview -> + null + else -> CIMergeCategory.RcvGroupEvent } + is CIContent.SndGroupEventContent -> when (content.sndGroupEvent) { + is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft, is SndGroupEvent.MemberAccepted, is SndGroupEvent.UserPendingReview -> null + else -> CIMergeCategory.SndGroupEvent + } + else -> + null } } 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.Admin) { + if (m.memberRole >= GroupMemberRole.Moderator) { chatInfo.groupInfo to null } else { null @@ -2389,6 +2842,7 @@ data class ChatItem ( is CIContent.RcvDirectE2EEInfo -> false is CIContent.SndGroupE2EEInfo -> false is CIContent.RcvGroupE2EEInfo -> false + is CIContent.ChatBanner -> false else -> true } @@ -2418,10 +2872,13 @@ data class ChatItem ( is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) { is RcvDirectEvent.ContactDeleted -> false is RcvDirectEvent.ProfileUpdated -> false + is RcvDirectEvent.GroupInvLinkReceived -> true } is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { is RcvGroupEvent.MemberAdded -> false is RcvGroupEvent.MemberConnected -> false + is RcvGroupEvent.MemberAccepted -> false + is RcvGroupEvent.UserAccepted -> false is RcvGroupEvent.MemberLeft -> false is RcvGroupEvent.MemberRole -> false is RcvGroupEvent.MemberBlocked -> false @@ -2433,6 +2890,7 @@ data class ChatItem ( is RcvGroupEvent.InvitedViaGroupLink -> false is RcvGroupEvent.MemberCreatedContact -> false is RcvGroupEvent.MemberProfileUpdated -> false + is RcvGroupEvent.NewMemberPendingReview -> true } is CIContent.SndGroupEventContent -> false is CIContent.RcvConnEventContent -> false @@ -2452,6 +2910,7 @@ data class ChatItem ( is CIContent.RcvDirectE2EEInfo -> false is CIContent.SndGroupE2EEInfo -> false is CIContent.RcvGroupE2EEInfo -> false + is CIContent.ChatBanner -> false is CIContent.InvalidJSON -> false } @@ -2568,7 +3027,9 @@ data class ChatItem ( itemTimed = null, itemLive = false, deletable = false, - editable = false + editable = false, + userMention = false, + showGroupAsSender = false, ), content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast), quotedItem = null, @@ -2592,7 +3053,9 @@ data class ChatItem ( itemTimed = null, itemLive = true, deletable = false, - editable = false + editable = false, + userMention = false, + showGroupAsSender = false ), content = CIContent.SndMsgContent(MsgContent.MCText("")), quotedItem = null, @@ -2612,12 +3075,13 @@ data class ChatItem ( } } -fun MutableState>.add(index: Int, elem: Chat) { - value = SnapshotStateList().apply { addAll(value); add(index, elem) } +sealed class SecondaryContextFilter { + class GroupChatScopeContext(val groupScopeInfo: GroupChatScopeInfo): SecondaryContextFilter() + class MsgContentTagContext(val contentTag: MsgContentTag): SecondaryContextFilter() } -fun MutableState>.addAndNotify(index: Int, elem: ChatItem, contentTag: MsgContentTag?) { - value = SnapshotStateList().apply { addAll(value); add(index, elem); chatModel.chatItemsChangesListenerForContent(contentTag)?.added(elem.id to elem.isRcvNew, index) } +fun MutableState>.add(index: Int, elem: Chat) { + value = SnapshotStateList().apply { addAll(value); add(index, elem) } } fun MutableState>.add(elem: Chat) { @@ -2627,11 +3091,6 @@ fun MutableState>.add(elem: Chat) { // For some reason, Kotlin version crashes if the list is empty fun MutableList.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmpty()) false else remAll(predicate) -// Adds item to chatItems and notifies a listener about newly added item -fun MutableState>.addAndNotify(elem: ChatItem, contentTag: MsgContentTag?) { - value = SnapshotStateList().apply { addAll(value); add(elem); chatModel.chatItemsChangesListenerForContent(contentTag)?.added(elem.id to elem.isRcvNew, lastIndex) } -} - fun MutableState>.addAll(index: Int, elems: List) { value = SnapshotStateList().apply { addAll(value); addAll(index, elems) } } @@ -2644,6 +3103,7 @@ fun MutableState>.removeAll(block: (Chat) -> Boolean) { value = SnapshotStateList().apply { addAll(value); removeAll(block) } } +// TODO [contexts] operates with both contexts? // Removes item(s) from chatItems and notifies a listener about removed item(s) fun MutableState>.removeAllAndNotify(block: (ChatItem) -> Boolean) { val toRemove = ArrayList>() @@ -2658,8 +3118,8 @@ fun MutableState>.removeAllAndNotify(block: (ChatIte } } if (toRemove.isNotEmpty()) { - chatModel.chatsContext.chatItemsChangesListener?.removed(toRemove, value) - chatModel.reportsChatsContext.chatItemsChangesListener?.removed(toRemove, value) + chatModel.chatsContext.chatState.itemsRemoved(toRemove, value) + chatModel.secondaryChatsContext.value?.chatState?.itemsRemoved(toRemove, value) } } @@ -2671,17 +3131,6 @@ fun MutableState>.removeAt(index: Int): Chat { return res } -fun MutableState>.removeLastAndNotify(contentTag: MsgContentTag?) { - val removed: Triple - value = SnapshotStateList().apply { - addAll(value) - val remIndex = lastIndex - val rem = removeLast() - removed = Triple(rem.id, remIndex, rem.isRcvNew) - } - chatModel.chatItemsChangesListenerForContent(contentTag)?.removed(listOf(removed), value) -} - fun MutableState>.replaceAll(elems: List) { value = SnapshotStateList().apply { addAll(elems) } } @@ -2690,11 +3139,12 @@ fun MutableState>.clear() { value = SnapshotStateList() } +// TODO [contexts] operates with both contexts? // Removes all chatItems and notifies a listener about it fun MutableState>.clearAndNotify() { value = SnapshotStateList() - chatModel.chatsContext.chatItemsChangesListener?.cleared() - chatModel.reportsChatsContext.chatItemsChangesListener?.cleared() + chatModel.chatsContext.chatState.clear() + chatModel.secondaryChatsContext.value?.chatState?.clear() } fun State>.asReversed(): MutableList = value.asReversed() @@ -2749,8 +3199,10 @@ data class CIMeta ( val itemEdited: Boolean, val itemTimed: CITimed?, val itemLive: Boolean?, + val userMention: Boolean, val deletable: Boolean, - val editable: Boolean + val editable: Boolean, + val showGroupAsSender: Boolean ) { val timestampText: String get() = getTimestampText(itemTs, true) @@ -2787,7 +3239,9 @@ data class CIMeta ( itemTimed = itemTimed, itemLive = itemLive, deletable = deletable, - editable = editable + editable = editable, + userMention = false, + showGroupAsSender = false ) fun invalidJSON(): CIMeta = @@ -2806,7 +3260,9 @@ data class CIMeta ( itemTimed = null, itemLive = false, deletable = false, - editable = false + editable = false, + userMention = false, + showGroupAsSender = false ) } } @@ -2885,6 +3341,19 @@ sealed class CIStatus { @Serializable @SerialName("rcvRead") class RcvRead: CIStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus() + // as in corresponds to SENT response from agent + fun isSent(): Boolean = when(this) { + is SndNew -> false + is SndSent -> true + is SndRcvd -> false + is SndErrorAuth -> true + is CISSndError -> true + is SndWarning -> true + is RcvNew -> false + is RcvRead -> false + is Invalid -> false + } + fun statusIcon( primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary, @@ -2924,6 +3393,13 @@ sealed class CIStatus { } } +fun shouldKeepOldSndCIStatus(oldStatus: CIStatus, newStatus: CIStatus): Boolean = + when { + oldStatus is CIStatus.SndRcvd && newStatus !is CIStatus.SndRcvd -> true + oldStatus.isSent() && newStatus is CIStatus.SndNew -> true + else -> false + } + @Serializable sealed class SndError { @Serializable @SerialName("auth") class Auth: SndError() @@ -3039,6 +3515,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()) { @@ -3098,6 +3581,7 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("rcvDirectE2EEInfo") class RcvDirectE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndGroupE2EEInfo") class SndGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvGroupE2EEInfo") class RcvGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("chatBanner") object ChatBanner: CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null } override val text: String get() = when (this) { @@ -3131,9 +3615,17 @@ sealed class CIContent: ItemContent { is RcvDirectE2EEInfo -> directE2EEInfoStr(e2eeInfo) is SndGroupE2EEInfo -> e2eeInfoNoPQStr is RcvGroupE2EEInfo -> e2eeInfoNoPQStr + is ChatBanner -> "" is InvalidJSON -> "invalid data" } + val hasMsgContent: Boolean get() = + if (msgContent != null) { + (msgContent as MsgContent).text.trim().isNotEmpty() + } else { + false + } + val showMemberName: Boolean get() = when (this) { is RcvMsgContent -> true @@ -3150,7 +3642,7 @@ sealed class CIContent: ItemContent { companion object { fun directE2EEInfoStr(e2EEInfo: E2EEInfo): String = - if (e2EEInfo.pqEnabled) { + if (e2EEInfo.pqEnabled == true) { generalGetString(MR.strings.e2ee_info_pq_short) } else { e2eeInfoNoPQStr @@ -3174,6 +3666,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) } @@ -3251,18 +3744,20 @@ sealed class MsgReaction { MREmojiChar.Heart -> "❤️" else -> emoji.value } - is Unknown -> "" + is Unknown -> "?" } companion object { - val values: List get() = MREmojiChar.values().map(::Emoji) - val old: List get() = listOf( + 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.Launch, + MREmojiChar.Check ).map(::Emoji) } } @@ -3281,8 +3776,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) } @@ -3621,6 +4121,7 @@ sealed class MsgContent { @Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCReport(override val text: String, val reason: ReportReason): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() val isVoice: Boolean get() = @@ -3674,7 +4175,7 @@ enum class CIGroupInvitationStatus { } @Serializable -class E2EEInfo (val pqEnabled: Boolean) {} +class E2EEInfo (val pqEnabled: Boolean?) {} object MsgContentSerializer : KSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) { @@ -3701,6 +4202,10 @@ object MsgContentSerializer : KSerializer { element("text") element("reason") }) + element("MCChat", buildClassSerialDescriptor("MCChat") { + element("text") + element("chatLink") + }) element("MCUnknown", buildClassSerialDescriptor("MCUnknown")) } @@ -3735,6 +4240,10 @@ object MsgContentSerializer : KSerializer { val reason = Json.decodeFromString(json["reason"].toString()) MsgContent.MCReport(text, reason) } + "chat" -> { + val chatLink = Json.decodeFromString(json["chatLink"].toString()) + MsgContent.MCChat(text, chatLink) + } else -> MsgContent.MCUnknown(t, text, json) } } else { @@ -3789,6 +4298,12 @@ object MsgContentSerializer : KSerializer { put("text", value.text) put("reason", json.encodeToJsonElement(value.reason)) } + is MsgContent.MCChat -> + buildJsonObject { + put("type", "chat") + put("text", value.text) + put("chatLink", json.encodeToJsonElement(value.chatLink)) + } is MsgContent.MCUnknown -> value.json } encoder.encodeJsonElement(json) @@ -3804,25 +4319,28 @@ enum class MsgContentTag { @SerialName("voice") Voice, @SerialName("file") File, @SerialName("report") Report, + @SerialName("chat") Chat, +} + +@Serializable +sealed class MsgChatLink { + @Serializable @SerialName("contact") data class Contact(val connLink: String, val profile: Profile, val business: Boolean) : MsgChatLink() + @Serializable @SerialName("invitation") data class Invitation(val invLink: String, val profile: Profile) : MsgChatLink() + @Serializable @SerialName("group") data class Group(val connLink: String, val groupProfile: GroupProfile) : MsgChatLink() } @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.SimplexLink -> if (mode == SimplexLinkMode.BROWSER) text else format.simplexUri - is Format.Email -> "mailto:$text" - is Format.Phone -> "tel:$text" - else -> null + val linkUri: String? get() = + when (format) { + is Format.Uri -> text + is Format.HyperLink -> format.linkUri + else -> null + } + + companion object { + fun plain(text: String): List = if (text.isEmpty()) emptyList() else listOf(FormattedText(text)) } - - // TODO make it dependent on simplexLinkMode preference - fun viewText(mode: SimplexLinkMode): String = - if (format is Format.SimplexLink && mode == SimplexLinkMode.DESCRIPTION) simplexLinkText(format.linkType, format.smpHosts) else text - - fun simplexLinkText(linkType: SimplexLinkType, smpHosts: List): String = - "${linkType.description} (${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})" } @Serializable @@ -3834,9 +4352,18 @@ sealed class Format { @Serializable @SerialName("secret") class Secret: Format() @Serializable @SerialName("colored") class Colored(val color: FormatColor): Format() @Serializable @SerialName("uri") class Uri: Format() - @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format() + @Serializable @SerialName("hyperLink") class HyperLink(val showText: String?, val linkUri: String): Format() + @Serializable @SerialName("simplexLink") class SimplexLink(val showText: String?, val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format() { + val simplexLinkText: String get() = + "${linkType.description} $viaHosts" + val viaHosts: String get() = + "(${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})" + } + @Serializable @SerialName("command") class Command(val commandStr: String): Format() + @Serializable @SerialName("mention") class Mention(val memberName: String): Format() @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() + @Serializable @SerialName("unknown") class Unknown: Format() val style: SpanStyle @Composable get() = when (this) { is Bold -> SpanStyle(fontWeight = FontWeight.Bold) @@ -3846,9 +4373,13 @@ sealed class Format { is Secret -> SpanStyle(color = Color.Transparent, background = SecretColor) is Colored -> SpanStyle(color = this.color.uiColor) is Uri -> linkStyle + is HyperLink -> linkStyle is SimplexLink -> linkStyle + is Command -> SpanStyle(color = MaterialTheme.colors.primary, fontFamily = FontFamily.Monospace) + is Mention -> SpanStyle(fontWeight = FontWeight.Medium) is Email -> linkStyle is Phone -> linkStyle + is Unknown -> SpanStyle() } val isSimplexLink = this is SimplexLink @@ -3862,12 +4393,16 @@ sealed class Format { enum class SimplexLinkType(val linkType: String) { contact("contact"), invitation("invitation"), - group("group"); + group("group"), + channel("channel"), + relay("relay"); 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 + relay -> MR.strings.simplex_link_relay }) } @@ -4010,10 +4545,12 @@ sealed class MsgErrorType() { sealed class RcvDirectEvent() { @Serializable @SerialName("contactDeleted") class ContactDeleted(): RcvDirectEvent() @Serializable @SerialName("profileUpdated") class ProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvDirectEvent() + @Serializable @SerialName("groupInvLinkReceived") class GroupInvLinkReceived(val groupProfile: GroupProfile): RcvDirectEvent() val text: String get() = when (this) { is ContactDeleted -> generalGetString(MR.strings.rcv_direct_event_contact_deleted) is ProfileUpdated -> profileUpdatedText(fromProfile, toProfile) + is GroupInvLinkReceived -> generalGetString(MR.strings.rcv_direct_event_group_inv_link_received).format(groupProfile.displayName) } private fun profileUpdatedText(from: Profile, to: Profile): String = @@ -4039,6 +4576,8 @@ sealed class RcvDirectEvent() { sealed class RcvGroupEvent() { @Serializable @SerialName("memberAdded") class MemberAdded(val groupMemberId: Long, val profile: Profile): RcvGroupEvent() @Serializable @SerialName("memberConnected") class MemberConnected(): RcvGroupEvent() + @Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): RcvGroupEvent() + @Serializable @SerialName("userAccepted") class UserAccepted(): RcvGroupEvent() @Serializable @SerialName("memberLeft") class MemberLeft(): RcvGroupEvent() @Serializable @SerialName("memberRole") class MemberRole(val groupMemberId: Long, val profile: Profile, val role: GroupMemberRole): RcvGroupEvent() @Serializable @SerialName("memberBlocked") class MemberBlocked(val groupMemberId: Long, val profile: Profile, val blocked: Boolean): RcvGroupEvent() @@ -4050,10 +4589,13 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent() @Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent() @Serializable @SerialName("memberProfileUpdated") class MemberProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvGroupEvent() + @Serializable @SerialName("newMemberPendingReview") class NewMemberPendingReview(): RcvGroupEvent() val text: String get() = when (this) { is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName) is MemberConnected -> generalGetString(MR.strings.rcv_group_event_member_connected) + is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName) + is UserAccepted -> generalGetString(MR.strings.rcv_group_event_user_accepted) is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left) is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text) is MemberBlocked -> if (blocked) { @@ -4069,6 +4611,7 @@ sealed class RcvGroupEvent() { is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link) is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact) is MemberProfileUpdated -> profileUpdatedText(fromProfile, toProfile) + is NewMemberPendingReview -> generalGetString(MR.strings.rcv_group_event_new_member_pending_review) } private fun profileUpdatedText(from: Profile, to: Profile): String = @@ -4093,6 +4636,8 @@ sealed class SndGroupEvent() { @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): SndGroupEvent() @Serializable @SerialName("userLeft") class UserLeft(): SndGroupEvent() @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent() + @Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): SndGroupEvent() + @Serializable @SerialName("userPendingReview") class UserPendingReview(): SndGroupEvent() val text: String get() = when (this) { is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text) @@ -4105,6 +4650,8 @@ sealed class SndGroupEvent() { is MemberDeleted -> String.format(generalGetString(MR.strings.snd_group_event_member_deleted), profile.profileViewName) is UserLeft -> generalGetString(MR.strings.snd_group_event_user_left) is GroupUpdated -> generalGetString(MR.strings.snd_group_event_group_profile_updated) + is MemberAccepted -> generalGetString(MR.strings.snd_group_event_member_accepted) + is UserPendingReview -> generalGetString(MR.strings.snd_group_event_user_pending_review) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt index 661479d6ad..cb7c8324dc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt @@ -22,7 +22,7 @@ sealed class WriteFileResult { * */ fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs { - val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized") + val ctrl = ChatController.getChatCtrl() ?: throw Exception("Controller is not initialized") val buffer = ByteBuffer.allocateDirect(data.size) buffer.put(data) buffer.rewind() @@ -58,7 +58,7 @@ fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray { } fun encryptCryptoFile(fromPath: String, toPath: String): CryptoFileArgs { - val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized") + val ctrl = ChatController.getChatCtrl() ?: throw Exception("Controller is not initialized") val str = chatEncryptFile(ctrl, fromPath, toPath) val d = json.decodeFromString(WriteFileResult.serializer(), str) return when (d) { 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 bf011c0bb7..a244293edb 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 @@ -5,46 +5,61 @@ 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.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg import chat.simplex.common.model.ChatModel.changingActiveUserMutex -import chat.simplex.common.model.ChatModel.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen -import chat.simplex.common.model.SMPErrorType.BLOCKED +import chat.simplex.common.model.GroupFeature.Files +import chat.simplex.common.model.MsgContent.MCUnknown +import chat.simplex.common.model.SMPProxyFallback.AllowProtected +import chat.simplex.common.model.SMPProxyMode.Always 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.contentModerationPostLink 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.defaultConditionsLink 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.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.* import kotlinx.serialization.builtins.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle import java.util.Date typealias ChatCtrl = Long @@ -52,6 +67,9 @@ typealias ChatCtrl = Long // version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core) val CREATE_MEMBER_CONTACT_VERSION = 2 +// support group knocking (MsgScope) +val GROUP_KNOCKING_VERSION = 15 + enum class CallOnLockScreen { DISABLE, SHOW, @@ -85,18 +103,7 @@ class AppPreferences { 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) @@ -105,19 +112,11 @@ class AppPreferences { val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true) val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true) val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true) + val privacyLinkPreviewsShowAlert = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, true) + val privacySanitizeLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SANITIZE_LINKS, false) + // TODO remove val privacyChatListOpenLinks = mkEnumPreference(SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS, PrivacyChatListOpenLinksMode.ASK) { PrivacyChatListOpenLinksMode.values().firstOrNull { it.name == this } } - 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 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 privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) @@ -156,24 +155,16 @@ class AppPreferences { }, set = fun(proxy: NetworkProxy) { _networkProxy.set(json.encodeToString(proxy)) } ) - 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 - return try { - TransportSessionMode.valueOf(value) - } catch (e: Throwable) { - TransportSessionMode.default - } - }, - set = fun(mode: TransportSessionMode) { _networkSessionMode.set(mode.name) } - ) - val networkSMPProxyMode = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, NetCfg.defaults.smpProxyMode.name) - val networkSMPProxyFallback = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, NetCfg.defaults.smpProxyFallback.name) - 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 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 networkSMPWebPortServers: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS, SMPWebPortServers.default) + val networkTCPConnectTimeoutBackground = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND, NetCfg.defaults.tcpConnectTimeout.backgroundTimeout, NetCfg.proxyDefaults.tcpConnectTimeout.backgroundTimeout) + val networkTCPConnectTimeoutInteractive = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE, NetCfg.defaults.tcpConnectTimeout.interactiveTimeout, NetCfg.proxyDefaults.tcpConnectTimeout.interactiveTimeout) + val networkTCPTimeoutBackground = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_BACKGROUND, NetCfg.defaults.tcpTimeout.backgroundTimeout, NetCfg.proxyDefaults.tcpTimeout.backgroundTimeout) + val networkTCPTimeoutInteractive = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_INTERACTIVE, NetCfg.defaults.tcpTimeout.interactiveTimeout, NetCfg.proxyDefaults.tcpTimeout.interactiveTimeout) val networkTCPTimeoutPerKb = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB, NetCfg.defaults.tcpTimeoutPerKb, NetCfg.proxyDefaults.tcpTimeoutPerKb) val networkRcvConcurrency = mkIntPreference(SHARED_PREFS_NETWORK_RCV_CONCURRENCY, NetCfg.defaults.rcvConcurrency) val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval) @@ -188,6 +179,7 @@ class AppPreferences { 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 showReportsInSupportChatAlert = mkBoolPreference(SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_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, "") @@ -270,6 +262,7 @@ class AppPreferences { liveMessageAlertShown to false, showHiddenProfilesNotice to true, showMuteProfileAlert to true, + showReportsInSupportChatAlert to true, showDeleteConversationNotice to true, showDeleteContactNotice to true, ) @@ -292,13 +285,14 @@ class AppPreferences { set = fun(value) = settings.putFloat(prefName, value) ) - private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference { - val d = if (networkUseSocksProxy.get()) proxyDefault else default - return SharedPreference( - get = fun() = settings.getLong(prefName, d), + private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference = + SharedPreference( + get = { + val d = if (networkUseSocksProxy.get()) proxyDefault else default + settings.getLong(prefName, d) + }, set = fun(value) = settings.putLong(prefName, value) ) - } private fun mkBoolPreference(prefName: String, default: Boolean) = SharedPreference( @@ -326,7 +320,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 = { @@ -374,7 +380,9 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages" private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" - private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" + private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "PrivacyLinkPreviewsShowAlert" + private const val SHARED_PREFS_PRIVACY_SANITIZE_LINKS = "PrivacySanitizeLinks" + private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" // TODO remove private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" @@ -410,8 +418,13 @@ class AppPreferences { 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_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" - private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" + 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_CONNECT_TIMEOUT_BACKGROUND = "NetworkTCPConnectTimeoutBackground" + private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE = "NetworkTCPConnectTimeoutInteractive" + private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_BACKGROUND = "NetworkTCPTimeoutBackground" + private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_INTERACTIVE = "NetworkTCPTimeoutInteractive" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" private const val SHARED_PREFS_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" @@ -426,6 +439,7 @@ class AppPreferences { 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_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "ShowReportsInSupportChatAlert" private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase" private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase" @@ -473,27 +487,35 @@ class AppPreferences { } } -private const val MESSAGE_TIMEOUT: Int = 15_000_000 +private const val MESSAGE_TIMEOUT: Int = 300_000_000 object ChatController { - var ctrl: ChatCtrl? = -1 + private var chatCtrl: ChatCtrl? = -1 val appPrefs: AppPreferences by lazy { AppPreferences() } - val messagesChannel: Channel = Channel() + val messagesChannel: Channel = Channel() val chatModel = ChatModel - private var receiverStarted = false + private var receiverJob: Job? = null var lastMsgReceivedTimestamp: Long = System.currentTimeMillis() private set - fun hasChatCtrl() = ctrl != -1L && ctrl != null + fun hasChatCtrl() = chatCtrl != -1L && chatCtrl != null + + fun getChatCtrl(): ChatCtrl? = chatCtrl + + fun setChatCtrl(ctrl: ChatCtrl?) { + val wasRunning = receiverJob != null + stopReceiver() + chatCtrl = ctrl + if (wasRunning && ctrl != null) startReceiver() + } suspend fun getAgentSubsTotal(rh: Long?): Pair? { val userId = currentUserId("getAgentSubsTotal") val r = sendCmd(rh, CC.GetAgentSubsTotal(userId), log = false) - - if (r is CR.AgentSubsTotal) return r.subsTotal to r.hasSession + 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 } @@ -502,8 +524,7 @@ object ChatController { val userId = currentUserId("getAgentServersSummary") val r = sendCmd(rh, CC.GetAgentServersSummary(userId), log = false) - - if (r is CR.AgentServersSummary) return r.serversSummary + 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 } @@ -541,9 +562,9 @@ object ChatController { } Log.d(TAG, "startChat: started") } else { - withChats { + withContext(Dispatchers.Main) { val chats = apiGetChats(null) - updateChats(chats) + chatModel.chatsContext.updateChats(chats) } Log.d(TAG, "startChat: running") } @@ -575,7 +596,7 @@ 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) + val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = "", shortDescr = null), ctrl = ctrl) if (!apiSetNetworkConfig(netCfg, ctrl = ctrl)) { Log.e(TAG, "Error setting network config, stopping migration") return null @@ -600,7 +621,7 @@ object ChatController { } } - suspend fun changeActiveUser_(rhId: Long?, toUserId: Long?, viewPwd: String?) { + suspend fun changeActiveUser_(rhId: Long?, toUserId: Long?, viewPwd: String?, keepingChatId: String? = null) { val prevActiveUser = chatModel.currentUser.value val currentUser = changingActiveUserMutex.withLock { (if (toUserId != null) apiSetActiveUser(rhId, toUserId, viewPwd) else apiGetActiveUser(rhId)).also { @@ -613,53 +634,62 @@ object ChatController { val users = listUsers(rhId) chatModel.users.clear() chatModel.users.addAll(users) - getUserChatData(rhId) + getUserChatData(rhId, keepingChatId = keepingChatId) val invitation = chatModel.callInvitations.values.firstOrNull { inv -> inv.user.userId == toUserId } if (invitation != null && currentUser != null) { chatModel.callManager.reportNewIncomingCall(invitation.copy(user = currentUser)) } } - suspend fun getUserChatData(rhId: Long?) { + suspend fun getUserChatData(rhId: Long?, keepingChatId: String? = null) { 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 - withChats { + withContext(Dispatchers.Main) { val chats = apiGetChats(rhId) - updateChats(chats) + chatModel.chatsContext.updateChats(chats, keepingChatId = keepingChatId) } - chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList() + chatModel.userTags.value = if (hasUser) apiGetChatTags(rhId) ?: emptyList() else emptyList() chatModel.activeChatTagFilter.value = null chatModel.updateChatTags(rhId) } private fun startReceiver() { Log.d(TAG, "ChatController startReceiver") - if (receiverStarted) return - receiverStarted = true - CoroutineScope(Dispatchers.IO).launch { - while (true) { + if (receiverJob != null || chatCtrl == null) return + receiverJob = CoroutineScope(Dispatchers.IO).launch { + var releaseLock: (() -> Unit) = {} + while (isActive) { /** Global [ctrl] can be null. It's needed for having the same [ChatModel] that already made in [ChatController] without the need * to change it everywhere in code after changing a database. * Since it can be changed in background thread, making this check to prevent NullPointerException */ - val ctrl = ctrl + val ctrl = chatCtrl if (ctrl == null) { - receiverStarted = false + stopReceiver() break } try { + val release = releaseLock + // delaying the release of wake lock in order to: + // 1. avoid race condition with the incoming call activity that fails to show if called after wake lock release (primary reason), + // 2. allow any other necessary processing for a bit of time with wakelock held. + launch { + delay(30000) + release() + } val msg = recvMsg(ctrl) + releaseLock = getWakeLock(timeout = 60000) if (msg != null) { val finishedWithoutTimeout = withTimeoutOrNull(60_000L) { processReceivedMsg(msg) messagesChannel.trySend(msg) } if (finishedWithoutTimeout == null) { - Log.e(TAG, "Timeout reached while processing received message: " + msg.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 ) } @@ -675,8 +705,100 @@ object ChatController { } } - suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): CR { - val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized") + private fun stopReceiver() { + Log.d(TAG, "ChatController stopReceiver") + val job = receiverJob + if (job != null) { + receiverJob = null + job.cancel() + } + } + + private suspend fun sendCmdWithRetry(rhId: Long?, cmd: CC, inProgress: MutableState? = null, retryNum: Int = 0): API? { + val r = sendCmd(rhId, cmd, retryNum = retryNum) + val alert = if (r is API.Error) retryableNetworkErrorAlert(r.err) else null + if ((inProgress == null || inProgress.value) && alert != null) { + return suspendCancellableCoroutine { cont -> + showRetryAlert( + alert, + onCancel = { + cont.resumeWith(Result.success(null)) + }, + onRetry = { + withLongRunningApi { + cont.resumeWith( + runCatching { + coroutineScope { + sendCmdWithRetry(rhId, cmd, inProgress = inProgress, retryNum = retryNum + 1) + } + } + ) + } + } + ) + + cont.invokeOnCancellation { + cont.resumeWith(Result.success(null)) + } + } + } else { + return r + } + } + + private fun showRetryAlert(alert: Pair, onCancel: () -> Unit, onRetry: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(alert.first), + text = alert.second, + confirmText = generalGetString(MR.strings.retry_verb), + onConfirm = onRetry, + onDismiss = onCancel, + onDismissRequest = onCancel + ) + } + + private fun retryableNetworkErrorAlert(err: ChatError): Pair? { + if (err !is ChatError.ChatErrorAgent) return null + val e = err.agentError + when (e) { + is AgentErrorType.BROKER -> { + val message = { String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.brokerAddress)) } + return when (e.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.connection_timeout to message() + is BrokerErrorType.NETWORK -> if (e.brokerErr.networkError is NetworkError.UnknownCAError) null else MR.strings.connection_error to message() + else -> null + } + } + is AgentErrorType.SMP -> + if (e.smpErr is SMPErrorType.PROXY && e.smpErr.proxyErr is ProxyError.BROKER) { + val message = { String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(e.serverAddress)) } + return when (e.smpErr.proxyErr.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.private_routing_timeout to message() + is BrokerErrorType.NETWORK -> if (e.smpErr.proxyErr.brokerErr.networkError is NetworkError.UnknownCAError) null else MR.strings.private_routing_error to message() + else -> null + } + } + is AgentErrorType.PROXY -> + if (e.proxyErr is ProxyClientError.ProxyProtocolError && e.proxyErr.protocolErr is SMPErrorType.PROXY) { + val message = { String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(e.proxyServer), serverHostname(e.relayServer)) } + return when (e.proxyErr.protocolErr.proxyErr) { + is ProxyError.BROKER -> + when (e.proxyErr.protocolErr.proxyErr.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.private_routing_timeout to message() + is BrokerErrorType.NETWORK -> if (e.proxyErr.protocolErr.proxyErr.brokerErr.networkError is NetworkError.UnknownCAError) null else MR.strings.private_routing_error to message() + else -> null + } + is ProxyError.NO_SESSION -> MR.strings.private_routing_no_session to message() + else -> null + } + } + else -> return null + } + return null + } + + suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, retryNum: Int = 0, log: Boolean = true): API { + val ctrl = otherCtrl ?: chatCtrl ?: throw Exception("Controller is not initialized") return withContext(Dispatchers.IO) { val c = cmd.cmdString @@ -684,37 +806,36 @@ object ChatController { 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 rStr = if (rhId == null) chatSendCmdRetry(ctrl, c, retryNum) else chatSendRemoteCmdRetry(ctrl, rhId.toInt(), c, retryNum) // coroutine was cancelled already, no need to process response (helps with apiListMembers - very heavy query in large groups) interruptIfCancelled() - val r = APIResponse.decodeStr(json) + val r = json.decodeFromString(rStr) if (log) { - 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") + 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.resp)) + chatModel.addTerminalItem(TerminalItem.resp(rhId, r)) } - r.resp + 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 @@ -724,14 +845,15 @@ object ChatController { 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 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 + 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 { @@ -743,8 +865,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}") @@ -753,29 +875,35 @@ 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}") } + suspend fun apiSetUserAutoAcceptMemberContacts(u: User, enable: Boolean) { + val r = sendCmd(u.remoteHostId, CC.ApiSetUserAutoAcceptMemberContacts(u.userId, enable)) + if (r.result is CR.CmdOk) return + throw Exception("failed to set auto-accept ${r.responseType} ${r.details}") + } + suspend fun apiHideUser(u: User, viewPwd: String): User = setUserPrivacy(u.remoteHostId, CC.ApiHideUser(u.userId, viewPwd)) @@ -790,20 +918,20 @@ 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}") @@ -812,7 +940,7 @@ object ChatController { private suspend fun apiCheckChatRunning(): Boolean { val r = sendCmd(null, CC.CheckChatRunning()) - when (r) { + when (r.result) { is CR.ChatRunning -> return true is CR.ChatStopped -> return false else -> throw Exception("failed check chat running: ${r.responseType} ${r.details}") @@ -821,15 +949,13 @@ object ChatController { 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}") - } + if (r.result is CR.ChatStopped) return true + throw Exception("failed stopping chat: ${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 is CR.CmdOk) return + if (r.result is CR.CmdOk) return throw Exception("failed to set app file paths: ${r.responseType} ${r.details}") } @@ -837,52 +963,52 @@ object ChatController { 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): List { val r = sendCmd(null, CC.ApiExportArchive(config)) - if (r is CR.ArchiveExported) return r.archiveErrors + 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() @@ -891,18 +1017,18 @@ object ChatController { private suspend fun apiGetChatTags(rh: Long?): List?{ val userId = currentUserId("apiGetChatTags") val r = sendCmd(rh, CC.ApiGetChatTags(userId)) - - if (r is CR.ChatTags) return r.userTags + 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 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 CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo + suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair? { + val r = sendCmd(rh, CC.ApiGetChat(type, id, scope, 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}") - if (pagination is ChatPagination.Around && r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.ChatItemNotFound) { + 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)) @@ -912,7 +1038,7 @@ object ChatController { suspend fun apiCreateChatTag(rh: Long?, tag: ChatTagData): List? { val r = sendCmd(rh, CC.ApiCreateChatTag(tag)) - if (r is CR.ChatTags) return r.userTags + 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 @@ -920,7 +1046,7 @@ object ChatController { 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 CR.TagsUpdated) return r.userTags to r.chatTags + 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 @@ -932,16 +1058,16 @@ object ChatController { 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) + suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, live: Boolean = false, ttl: Int? = null, composedMessages: List): List? { + val cmd = CC.ApiSendMessages(type, id, scope, live, ttl, composedMessages) return processSendMessageCmd(rh, cmd) } private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): List? { val r = sendCmd(rh, cmd) return when { - r is CR.NewChatItems -> r.chatItems - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.LargeMsg && cmd is CC.ApiSendMessages -> { + 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), @@ -953,7 +1079,7 @@ object ChatController { ) null } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.LargeMsg && cmd is CC.ApiForwardChatItems -> { + 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) @@ -968,60 +1094,48 @@ object ChatController { } } } - suspend fun apiCreateChatItems(rh: Long?, noteFolderId: Long, composedMessages: List): List? { + + 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.NewChatItems -> r.chatItems - else -> { - apiErrorAlert("apiCreateChatItems", 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)) - return when (r) { - is CR.NewChatItems -> r.chatItems - else -> { - apiErrorAlert("apiReportMessage", generalGetString(MR.strings.error_creating_report), r) - null - } - } + 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 - } - } + suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long): ChatItemInfo? { + val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, scope, 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) + suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, itemIds: List, ttl: Int?): List? { + val cmd = CC.ApiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl) return processSendMessageCmd(rh, cmd)?.map { it.chatItem } } - suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List): CR.ForwardPlan? { - return when (val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds))) { - is CR.ForwardPlan -> r - else -> { - apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r) - null - } - } + suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List): CR.ForwardPlan? { + val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds)) + if (r is API.Result && r.res is CR.ForwardPlan) return r.res + apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r) + return null } - suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mc: MsgContent, live: Boolean = false): AChatItem? { - val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, mc, live)) + suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? { + val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, scope, itemId, updatedMessage, live)) when { - r is CR.ChatItemUpdated -> return r.chatItem - r is CR.ChatItemNotChanged -> return r.chatItem - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.LargeMsg -> { + 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) { @@ -1038,9 +1152,9 @@ object ChatController { 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 + suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? { + val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, scope, itemId, add, reaction)) + 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 } @@ -1048,143 +1162,120 @@ object ChatController { 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 CR.ReactionMembers) return r.memberReactions + 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 CR.ChatItemsDeleted) return r.chatItemDeletions + suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List, mode: CIDeleteMode): List? { + val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, scope, 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 apiDeleteMemberChatItems(rh: Long?, groupId: Long, itemIds: List): List? { val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, itemIds)) - if (r is CR.ChatItemsDeleted) return r.chatItemDeletions + 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 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 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()) - - return when (r) { - is CR.ServerOperatorConditions -> r.conditions - else -> { - Log.e(TAG, "getServerOperators bad response: ${r.responseType} ${r.details}") - null - } - } + 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)) - return when (r) { - is CR.ServerOperatorConditions -> r.conditions - else -> { - Log.e(TAG, "setServerOperators bad response: ${r.responseType} ${r.details}") - null - } - } + 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)) - return when (r) { - is CR.UserServers -> r.userServers - else -> { - Log.e(TAG, "getUserServers bad response: ${r.responseType} ${r.details}") - null - } - } + 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)) - return when (r) { - is CR.CmdOk -> true - else -> { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.failed_to_save_servers), - "${r.responseType}: ${r.details}" - ) - Log.e(TAG, "setUserServers bad response: ${r.responseType} ${r.details}") - false - } - } + 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)) - return when (r) { - is CR.UserServersValidation -> r.serverErrors - else -> { - Log.e(TAG, "validateServers bad response: ${r.responseType} ${r.details}") - null - } - } + 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()) - return when (r) { - is CR.UsageConditions -> Triple(r.usageConditions, r.conditionsText, r.acceptedConditions) - else -> { - Log.e(TAG, "getUsageConditions bad response: ${r.responseType} ${r.details}") - null - } - } + 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)) - return when (r) { - is CR.CmdOk -> true - else -> { - Log.e(TAG, "setConditionsNotified bad response: ${r.responseType} ${r.details}") - false - } - } + 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)) - return when (r) { - is CR.ServerOperatorConditions -> r.conditions - else -> { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.error_accepting_operator_conditions), - "${r.responseType}: ${r.details}" - ) - Log.e(TAG, "acceptConditions bad response: ${r.responseType} ${r.details}") - null - } - } + 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 if (r.chatItemTTL != null) { - 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 } @@ -1195,37 +1286,32 @@ object ChatController { 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 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 is CR.CmdOk) return + 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) - return when (r) { - is CR.CmdOk -> true - else -> { - 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}" - ) - } - false - } + 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)) } @@ -1233,13 +1319,9 @@ object ChatController { 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 = @@ -1250,230 +1332,274 @@ 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 CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo) - apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) + val r = sendCmdWithRetry(rh, CC.APIContactQueueInfo(contactId)) + if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo + if (r != null) 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 CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo) - apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) + val r = sendCmdWithRetry(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) + if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo + if (r != null) 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 r = sendCmdWithRetry(rh, CC.APIAddContact(userId, incognito = incognito)) + return when { + r is API.Result && r.res is CR.Invitation -> (r.res.connLinkInvitation to r.res.connection) to null + r == null -> null 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)) - - return when (r) { - is CR.ConnectionIncognitoUpdated -> r.toConnection - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiSetConnectionIncognito", generalGetString(MR.strings.error_sending_message), r) - } - null - } + if (r is API.Result && r.res is CR.ConnectionIncognitoUpdated) return r.res.toConnection + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiSetConnectionIncognito", generalGetString(MR.strings.error_sending_message), r) } - } - - suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? { - val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId)) - - return when (r) { - is CR.ConnectionUserChanged -> r.toConnection - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r) - } - null - } - } - } - - suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? { - val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } - val r = sendCmd(rh, CC.APIConnectPlan(userId, connReq)) - if (r is CR.CRConnectionPlan) return r.connectionPlan - Log.e(TAG, "apiConnectPlan bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiConnect(rh: Long?, incognito: Boolean, connReq: String): PendingContactConnection? { + suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? { + val r = sendCmdWithRetry(rh, CC.ApiChangeConnectionUser(connId, userId)) + if (r is API.Result && r.res is CR.ConnectionUserChanged) return r.res.toConnection + if (r == null) return null + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r) + } + return null + } + + suspend fun apiConnectPlan(rh: Long?, connLink: String, inProgress: MutableState): Pair? { + val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } + val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink), inProgress = inProgress) + if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan + if (inProgress.value && r != null) apiConnectResponseAlert(r) + return null + } + + 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 = sendCmdWithRetry(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 -> { + r != null -> 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 CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.SMP - && r.chatError.agentError.smpErr is SMPErrorType.BLOCKED -> { + 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.chatError.agentError.smpErr.blockInfo.reason.text), + generalGetString(MR.strings.connection_error_blocked_desc).format(r.err.agentError.smpErr.blockInfo.reason.text), ) - return null } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.SMP - && r.chatError.agentError.smpErr is SMPErrorType.QUOTA -> { + 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) ) - return null } else -> { if (!(networkErrorAlert(r))) { apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r) } - return null } } } + suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? { + val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null } + val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData)) + if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat + Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData): Chat? { + val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null } + val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, groupShortLinkData)) + if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat + Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiChangePreparedContactUser(rh: Long?, contactId: Long, newUserId: Long): Contact? { + val r = sendCmd(rh, CC.APIChangePreparedContactUser(contactId, newUserId)) + if (r is API.Result && r.res is CR.ContactUserChanged) return r.res.toContact + Log.e(TAG, "apiChangePreparedContactUser bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_user), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiChangePreparedGroupUser(rh: Long?, groupId: Long, newUserId: Long): GroupInfo? { + val r = sendCmd(rh, CC.APIChangePreparedGroupUser(groupId, newUserId)) + if (r is API.Result && r.res is CR.GroupUserChanged) return r.res.toGroup + Log.e(TAG, "apiChangePreparedGroupUser bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_user), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiConnectPreparedContact(rh: Long?, contactId: Long, incognito: Boolean, msg: MsgContent?): Contact? { + val r = sendCmdWithRetry(rh, CC.APIConnectPreparedContact(contactId, incognito, msg)) + if (r is API.Result && r.res is CR.StartedConnectionToContact) return r.res.contact + if (r != null) { + Log.e(TAG, "apiConnectPreparedContact bad response: ${r.responseType} ${r.details}") + apiConnectResponseAlert(r) + } + return null + } + + suspend fun apiConnectPreparedGroup(rh: Long?, groupId: Long, incognito: Boolean, msg: MsgContent?): GroupInfo? { + val r = sendCmdWithRetry(rh, CC.APIConnectPreparedGroup(groupId, incognito, msg)) + if (r is API.Result && r.res is CR.StartedConnectionToGroup) return r.res.groupInfo + if (r != null) { + Log.e(TAG, "apiConnectPreparedGroup bad response: ${r.responseType} ${r.details}") + apiConnectResponseAlert(r) + } + return null + } + 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 - } + val r = sendCmdWithRetry(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId)) + if (r is API.Result && r.res is CR.SentInvitationToContact) return r.res.contact + if (r == null) return null + 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)) { - withChats { - removeChat(chat.remoteHostId, cInfo.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(chat.remoteHostId, cInfo.id) } } } @@ -1481,10 +1607,11 @@ object ChatController { suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean { chatModel.deletedChats.value += rh to type.type + id val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) + val res = r.result val success = when { - 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 @@ -1505,13 +1632,12 @@ object ChatController { val type = ChatType.Direct chatModel.deletedChats.value += rh to type.type + id val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) - val contact = when { - r is CR.ContactDeleted -> r.contact - else -> { - val titleId = MR.strings.error_deleting_contact - apiErrorAlert("apiDeleteChat", generalGetString(titleId), r) - null - } + 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 @@ -1521,11 +1647,11 @@ object ChatController { withBGApi { val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId) if (updatedChatInfo != null) { - withChats { - clearChat(chat.remoteHostId, updatedChatInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.clearChat(chat.remoteHostId, updatedChatInfo) } - withChats(MsgContentTag.Report) { - clearChat(chat.remoteHostId, updatedChatInfo) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.clearChat(chat.remoteHostId, updatedChatInfo) } ntfManager.cancelNotificationsForChat(chat.chatInfo.id) close?.invoke() @@ -1535,7 +1661,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 } @@ -1543,9 +1669,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}") @@ -1554,73 +1680,77 @@ 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 CR.GroupAliasUpdated) return r.toGroup + 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 apiSetUserUIThemes(rh: Long?, userId: Long, themes: ThemeModeOverrides?): Boolean { val r = sendCmd(rh, CC.ApiSetUserUIThemes(userId, themes)) - if (r is CR.CmdOk) return true + 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 is CR.CmdOk) return true + 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?): String? { + suspend fun apiCreateUserAddress(rh: Long?): 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 = sendCmdWithRetry(rh, CC.ApiCreateMyAddress(userId)) + if (r is API.Result && r.res is CR.UserContactLinkCreated) return r.res.connLinkContact + if (r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.NOTICE) { + val e = r.err.agentError + showClientNoticeAlert(e.server, e.preset, e.expiresAt) + return null } + if (r == null) return null + 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) + val r = sendCmdWithRetry(rh, CC.ApiDeleteMyAddress(userId)) + if (r is API.Result && r.res is CR.UserContactLinkDeleted) return r.res.user.updateRemoteHostId(rh) + if (r == null) return null Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}") return null } @@ -1628,9 +1758,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 } @@ -1638,126 +1768,142 @@ object ChatController { return null } - 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 + suspend fun apiAddMyAddressShortLink(rh: Long?): UserContactLinkRec? { + val userId = kotlin.runCatching { currentUserId("apiAddMyAddressShortLink") }.getOrElse { return null } + val r = sendCmdWithRetry(rh, CC.ApiAddMyAddressShortLink(userId)) + if (r is API.Result && r.res is CR.UserContactLink) return r.res.contactLink + if (r == null) return null + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiAddMyAddressShortLink", generalGetString(MR.strings.error_creating_address), r) + } + return null + } + + suspend fun apiSetUserAddressSettings(rh: Long?, settings: AddressSettings): UserContactLinkRec? { + val userId = kotlin.runCatching { currentUserId("apiSetUserAddressSettings") }.getOrElse { return null } + val r = sendCmdWithRetry(rh, CC.ApiSetAddressSettings(userId, settings)) + 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 } + if (r == null) return null Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}") return null } suspend fun apiAcceptContactRequest(rh: Long?, incognito: Boolean, contactReqId: Long): Contact? { - val r = sendCmd(rh, CC.ApiAcceptContact(incognito, contactReqId)) + val r = sendCmdWithRetry(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) ) null } - else -> { + r != null -> { if (!(networkErrorAlert(r))) { apiErrorAlert("apiAcceptContactRequest", generalGetString(MR.strings.error_accepting_contact_request), r) } null } + else -> null } } - suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Boolean { + suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Contact? { 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 r.res.contact_ Log.e(TAG, "apiRejectContactRequest bad response: ${r.responseType} ${r.details}") - return false + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiRejectContactRequest", generalGetString(MR.strings.error_rejecting_contact_request), r) + } + return null } 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 - } - - suspend fun apiGetNetworkStatuses(rh: Long?): List? { - val r = sendCmd(rh, CC.ApiGetNetworkStatuses()) - if (r is CR.NetworkStatuses) return r.networkStatuses - Log.e(TAG, "apiGetNetworkStatuses bad response: ${r.responseType} ${r.details}") - return null + return r.result is CR.CmdOk } suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean { - val r = sendCmd(rh, CC.ApiChatRead(type, id)) - if (r is CR.CmdOk) return true + val r = sendCmd(rh, CC.ApiChatRead(type, id, scope = null)) + 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 is CR.CmdOk) return true + suspend fun apiSupportChatRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope): Pair? { + val r = sendCmd(rh, CC.ApiChatRead(type, id, scope)) + if (r is API.Result && r.res is CR.MemberSupportChatRead) return r.res.groupInfo to r.res.member + apiErrorAlert("apiSupportChatRead", generalGetString(MR.strings.error_marking_member_support_chat_read), r) + return null + } + + suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List): ChatInfo? { + val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, scope, itemIds)) + if (r is API.Result && r.res is CR.ItemsReadForChat) return r.res.chatInfo Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}") - return false + return null } 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() @@ -1766,8 +1912,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() @@ -1776,8 +1922,8 @@ 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 @@ -1787,7 +1933,7 @@ object ChatController { suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List, userApprovedRelays: Boolean = false, auto: Boolean = false) { val fileIdsToApprove = mutableListOf() val srvsToApprove = mutableSetOf() - val otherFileErrs = mutableListOf() + val otherFileErrs = mutableListOf() for (fileId in fileIds) { val r = sendCmd( @@ -1798,10 +1944,10 @@ object ChatController { inline = null ) ) - if (r is CR.RcvFileAccepted) { - chatItemSimpleUpdate(rhId, user, r.chatItem) + if (r is API.Result && r.res is CR.RcvFileAccepted) { + chatItemSimpleUpdate(rhId, user, r.res.chatItem) } else { - val maybeChatError = chatError(r) + val maybeChatError = apiChatErrorType(r) if (maybeChatError is ChatErrorType.FileNotApproved) { fileIdsToApprove.add(maybeChatError.fileId) srvsToApprove.addAll(maybeChatError.unknownServers.map { serverHostname(it) }) @@ -1829,21 +1975,19 @@ object ChatController { } ) } else if (otherFileErrs.size == 1) { // If there is a single other error, we differentiate on it - when (val errCR = otherFileErrs.first()) { - 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) - ) - } - else -> { - val maybeChatError = chatError(errCR) - if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { - Log.d(TAG, "receiveFiles ignoring FileCancelled or FileAlreadyReceiving error") - } else { - apiErrorAlert("receiveFiles", generalGetString(MR.strings.error_receiving_file), errCR) - } + 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) + ) + } else { + val maybeChatError = apiChatErrorType(errCR) + if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { + Log.d(TAG, "receiveFiles ignoring FileCancelled or FileAlreadyReceiving error") + } else { + apiErrorAlert("receiveFiles", generalGetString(MR.strings.error_receiving_file), errCR) } } } else if (otherFileErrs.size > 1) { // If there are multiple other errors, we show general alert @@ -1859,7 +2003,7 @@ object ChatController { private fun showFilesToApproveAlert( srvsToApprove: Set, - otherFileErrs: List, + otherFileErrs: List, approveFiles: (() -> Unit) ) { val srvsToApproveStr = srvsToApprove.sorted().joinToString(separator = ", ") @@ -1922,9 +2066,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 @@ -1935,34 +2079,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 -> - withChats { - updateGroup(rh, r.groupInfo) + val r = sendCmdWithRetry(rh, CC.ApiJoinGroup(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") } } } - is CR.ChatCmdError -> { - val e = r.chatError - suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { withChats { 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)) @@ -1973,62 +2115,73 @@ object ChatController { apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) } } - else -> apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) + r != null -> apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) } } - 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 apiAcceptMember(rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole): Pair? { + val r = sendCmd(rh, CC.ApiAcceptMember(groupId, groupMemberId, memberRole)) + if (r is API.Result && r.res is CR.MemberAccepted) return r.res.groupInfo to r.res.member + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiAcceptMember", generalGetString(MR.strings.error_accepting_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 apiDeleteMemberSupportChat(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { + val r = sendCmd(rh, CC.ApiDeleteMemberSupportChat(groupId, groupMemberId)) + if (r is API.Result && r.res is CR.MemberSupportChatDeleted) return r.res.groupInfo to r.res.member + apiErrorAlert("apiDeleteMemberSupportChat", generalGetString(MR.strings.error_deleting_member_support_chat), r) + return null + } - 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 apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean): Pair>? { + val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages)) + if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.groupInfo to r.res.members + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r) } + return null + } + + 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 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 -> { @@ -2042,82 +2195,91 @@ 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): GroupLink? { + val r = sendCmdWithRetry(rh, CC.APICreateGroupLink(groupId, memberRole)) + if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink + if (r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.NOTICE) { + val e = r.err.agentError + showClientNoticeAlert(e.server, e.preset, e.expiresAt) + return null } + if (r == null) return null + 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): GroupLink? { + val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole)) + if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink + 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 = sendCmdWithRetry(rh, CC.APIDeleteGroupLink(groupId)) + if (r is API.Result && r.res is CR.GroupLinkDeleted) return true + if (r == null) return false + 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): GroupLink? { + val r = sendCmd(rh, CC.APIGetGroupLink(groupId)) + if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink + Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiAddGroupShortLink(rh: Long?, groupId: Long): GroupLink? { + val r = sendCmdWithRetry(rh, CC.ApiAddGroupShortLink(groupId)) + if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink + if (r == null) return null + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiAddGroupShortLink", generalGetString(MR.strings.error_creating_link_for_group), r) } + 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 apiAcceptMemberContact(rh: Long?, contactId: Long): Contact? { + val r = sendCmdWithRetry(rh, CC.APIAcceptMemberContact(contactId)) + if (r is API.Result && r.res is CR.MemberContactAccepted) return r.res.contact + if (r != null) { + Log.e(TAG, "apiAcceptMemberContact bad response: ${r.responseType} ${r.details}") + apiConnectResponseAlert(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) { - withChats { - updateContact(rh, toContact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rh, toContact) } } } @@ -2126,7 +2288,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 } @@ -2139,14 +2301,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 } @@ -2168,45 +2330,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 } @@ -2217,72 +2383,74 @@ 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(if (e.agentError.brokerErr.networkError is NetworkError.UnknownCAError) MR.strings.network_error_unknown_ca else 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.HOST -> { + 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(r.chatError.agentError.brokerAddress)) + String.format(generalGetString(MR.strings.network_error_broker_host_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.TRANSPORT - && r.chatError.agentError.brokerErr.transportErr is SMPTransportError.Version -> { + 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(r.chatError.agentError.brokerAddress)) + String.format(generalGetString(MR.strings.network_error_broker_version_desc), serverHostname(e.agentError.brokerAddress)) ) true } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.SMP - && r.chatError.agentError.smpErr is SMPErrorType.PROXY -> - smpProxyErrorAlert(r.chatError.agentError.smpErr.proxyErr, r.chatError.agentError.serverAddress) - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.PROXY - && r.chatError.agentError.proxyErr is ProxyClientError.ProxyProtocolError - && r.chatError.agentError.proxyErr.protocolErr is SMPErrorType.PROXY -> + 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( - r.chatError.agentError.proxyErr.protocolErr.proxyErr, - r.chatError.agentError.proxyServer, - r.chatError.agentError.relayServer + e.agentError.proxyErr.protocolErr.proxyErr, + e.agentError.proxyServer, + e.agentError.relayServer ) else -> false } @@ -2302,7 +2470,10 @@ object ChatController { && 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)) + String.format( + generalGetString(if (pe.brokerErr.networkError is NetworkError.UnknownCAError) MR.strings.smp_proxy_error_unknown_ca else MR.strings.smp_proxy_error_connecting), + serverHostname(srvAddr) + ) ) true } @@ -2341,7 +2512,8 @@ object ChatController { && 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)) + if (pe.brokerErr.networkError is NetworkError.UnknownCAError) String.format(generalGetString(MR.strings.proxy_destination_error_unknown_ca), serverHostname(relayServer)) + else String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) ) true } @@ -2373,157 +2545,138 @@ object ChatController { } } - private fun apiErrorAlert(method: String, title: String, r: CR) { + 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) { - withChats { - updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) } } } is CR.ContactConnected -> { if (active(r.user) && r.contact.directOrUsed) { - withChats { - updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) val conn = r.contact.activeConn if (conn != null) { chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - removeChat(rhId, conn.id) + chatModel.chatsContext.removeChat(rhId, conn.id) + } + if (r.contact.id == chatModel.chatId.value && conn != null) { + chatModel.chatAgentConnId.value = conn.agentConnId + chatModel.chatSubStatus.value = SubscriptionStatus.Active } } } if (r.contact.directOrUsed) { ntfManager.notifyContactConnected(r.user, r.contact) } - chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) } is CR.ContactConnecting -> { if (active(r.user) && r.contact.directOrUsed) { - withChats { - updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) val conn = r.contact.activeConn if (conn != null) { chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - removeChat(rhId, conn.id) + chatModel.chatsContext.removeChat(rhId, conn.id) } } } } is CR.ContactSndReady -> { if (active(r.user) && r.contact.directOrUsed) { - withChats { - updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) val conn = r.contact.activeConn if (conn != null) { chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - removeChat(rhId, conn.id) + 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)) { - withChats { - if (hasChat(rhId, contactRequest.id)) { - updateChatInfo(rhId, cInfo) + withContext(Dispatchers.Main) { + if (r.chat_ != null) { // means contact request was created with contact, so we need to add/update contact chat + if (chatModel.chatsContext.hasChat(rhId, r.chat_.id)) { + chatModel.chatsContext.updateChatInfo(rhId, r.chat_.chatInfo) + } else { + chatModel.chatsContext.addChat(r.chat_.copy(remoteHostId = rhId)) + } } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + val cInfo = ChatInfo.ContactRequest(contactRequest) + 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) + ntfManager.notifyContactRequestReceived(r.user, ChatInfo.ContactRequest(contactRequest)) } is CR.ContactUpdated -> { if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.toContact.id)) { val cInfo = ChatInfo.Direct(r.toContact) - withChats { - updateChatInfo(rhId, cInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatInfo(rhId, cInfo) } } } is CR.GroupMemberUpdated -> { if (active(r.user)) { - withChats { - upsertGroupMember(rhId, r.groupInfo, r.toMember) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.toMember) } - withReportsChatsIfOpen { - 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.chatsContext.hasChat(rhId, r.mergedContact.id)) { - if (chatModel.chatId.value == r.mergedContact.id) { - chatModel.chatId.value = r.intoContact.id - } - withChats { - removeChat(rhId, r.mergedContact.id) + is CR.SubscriptionStatusEvt -> { + val chatAgentConnId = chatModel.chatAgentConnId.value + if (chatAgentConnId != null && r.connections.contains(chatAgentConnId)) { + withContext(Dispatchers.Main) { + chatModel.chatSubStatus.value = r.subscriptionStatus } } } - // ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI, - // They have to be used here for remote desktop to process these status updates. - is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected()) - is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected()) - is CR.ContactSubSummary -> { - for (sub in r.contactSubscriptions) { - if (active(r.user)) { - withChats { - updateContact(rhId, sub.contact) - } - } - val err = sub.contactError - if (err == null) { - chatModel.setContactNetworkStatus(sub.contact, NetworkStatus.Connected()) - } else { - processContactSubError(sub.contact, sub.contactError) + is CR.ChatInfoUpdated -> + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatInfo(rhId, r.chatInfo) } } - } - is CR.NetworkStatusResp -> { - for (cId in r.connections) { - chatModel.networkStatuses[cId] = r.networkStatus - } - } - is CR.NetworkStatuses -> { - for (s in r.networkStatuses) { - chatModel.networkStatuses[s.agentConnId] = s.networkStatus - } - } is CR.NewChatItems -> withBGApi { r.chatItems.forEach { chatItem -> val cInfo = chatItem.chatInfo val cItem = chatItem.chatItem if (active(r.user)) { - withChats { - addChatItem(rhId, cInfo, cItem) + withContext(Dispatchers.Main) { + chatModel.chatsContext.addChatItem(rhId, cInfo, cItem) if (cItem.isActiveReport) { - increaseGroupReportsCounter(rhId, cInfo.id) + chatModel.chatsContext.increaseGroupReportsCounter(rhId, cInfo.id) } + chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem) } - withReportsChatsIfOpen { - if (cItem.isReport) { - addChatItem(rhId, cInfo, cItem) - } - } - } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { - withChats { - increaseUnreadCounter(rhId, r.user) + } else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.increaseUnreadCounter(rhId, r.user) } } val file = cItem.file @@ -2544,13 +2697,11 @@ object ChatController { val cInfo = chatItem.chatInfo val cItem = chatItem.chatItem if (!cItem.isDeletedContent && active(r.user)) { - withChats { - updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertChatItem(rhId, cInfo, cItem) } - withReportsChatsIfOpen { - if (cItem.isReport) { - updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) - } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, cItem) } } } @@ -2558,22 +2709,20 @@ object ChatController { chatItemUpdateNotify(rhId, r.user, r.chatItem) is CR.ChatItemReaction -> { if (active(r.user)) { - withChats { - updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) } - withReportsChatsIfOpen { - if (r.reaction.chatReaction.chatItem.isReport) { - updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) - } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) } } } is CR.ChatItemsDeleted -> { if (!active(r.user)) { r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> - if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled) { - withChats { - decreaseUnreadCounter(rhId, r.user) + if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled(deletedChatItem.chatItem)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.decreaseUnreadCounter(rhId, r.user) } } } @@ -2596,79 +2745,38 @@ object ChatController { generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) ) } - withChats { + withContext(Dispatchers.Main) { if (toChatItem == null) { - removeChatItem(rhId, cInfo, cItem) + chatModel.chatsContext.removeChatItem(rhId, cInfo, cItem) } else { - upsertChatItem(rhId, cInfo, toChatItem.chatItem) + chatModel.chatsContext.upsertChatItem(rhId, cInfo, toChatItem.chatItem) + } + if (cItem.isActiveReport) { + chatModel.chatsContext.decreaseGroupReportsCounter(rhId, cInfo.id) } } - withReportsChatsIfOpen { - if (cItem.isReport) { - if (toChatItem == null) { - removeChatItem(rhId, cInfo, cItem) - } else { - upsertChatItem(rhId, cInfo, toChatItem.chatItem) - } + withContext(Dispatchers.Main) { + if (toChatItem == null) { + chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem) + } else { + chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem) } } } + r.chatItemDeletions.lastOrNull()?.deletedChatItem?.chatInfo?.let { updatedChatInfo -> + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatInfo(rhId, updatedChatInfo) + } + } } is CR.GroupChatItemsDeleted -> { - if (!active(r.user)) { - val users = chatController.listUsers(rhId) - chatModel.users.clear() - chatModel.users.addAll(users) - return - } - val cInfo = ChatInfo.Group(r.groupInfo) - withChats { - r.chatItemIDs.forEach { itemId -> - decreaseGroupReportsCounter(rhId, cInfo.id) - val cItem = 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 = 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()) - } - upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted))) - } - } - withReportsChatsIfOpen { - r.chatItemIDs.forEach { itemId -> - val cItem = 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()) - } - upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted))) - } - } + groupChatItemsDeleted(rhId, r) } is CR.ReceivedGroupInvitation -> { if (active(r.user)) { - withChats { + withContext(Dispatchers.Main) { // update so that repeat group invitations are not duplicated - updateGroup(rhId, r.groupInfo) + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) } // TODO NtfManager.shared.notifyGroupInvitation } @@ -2676,135 +2784,173 @@ object ChatController { is CR.UserAcceptedGroupSent -> { if (!active(r.user)) return - withChats { - updateGroup(rhId, r.groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) val conn = r.hostContact?.activeConn if (conn != null) { chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") - removeChat(rhId, conn.id) + chatModel.chatsContext.removeChat(rhId, conn.id) } } } is CR.GroupLinkConnecting -> { if (!active(r.user)) return - withChats { - updateGroup(rhId, r.groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) val hostConn = r.hostMember.activeConn if (hostConn != null) { chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") - removeChat(rhId, hostConn.id) + chatModel.chatsContext.removeChat(rhId, hostConn.id) } } } is CR.BusinessLinkConnecting -> { if (!active(r.user)) return - withChats { - updateGroup(rhId, r.groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) } if (chatModel.chatId.value == r.fromContact.id) { openGroupChat(rhId, r.groupInfo.groupId) } - withChats { - removeChat(rhId, r.fromContact.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, r.fromContact.id) } } is CR.JoinedGroupMemberConnecting -> if (active(r.user)) { - withChats { - upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + } + } + is CR.MemberAcceptedByOther -> + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) } } is CR.DeletedMemberUser -> // TODO update user member if (active(r.user)) { - withChats { - 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)) { - withChats { - upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + if (r.withMessages) { + chatModel.chatsContext.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo) + } } - withReportsChatsIfOpen { - upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + 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)) { - withChats { - upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) } - withReportsChatsIfOpen { - 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)) { - withChats { - upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) } - withReportsChatsIfOpen { - 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)) { - withChats { - upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + r.members.forEach { member -> + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, member) + } } - withReportsChatsIfOpen { - upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + r.members.forEach { member -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, member) + } } } is CR.MemberBlockedForAll -> if (active(r.user)) { - withChats { - upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) } - withReportsChatsIfOpen { - 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)) { - withChats { - updateGroup(rhId, r.groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) } } is CR.UserJoinedGroup -> if (active(r.user)) { - withChats { - updateGroup(rhId, r.groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + } + if ( + chatModel.chatId.value == r.groupInfo.id + && !r.groupInfo.membership.memberPending + && ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT) + && chatModel.secondaryChatsContext.value?.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext + ) { + CoroutineScope(Dispatchers.Default).launch { + delay(1000L) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value = null + } + ModalManager.end.closeModals() + } } } is CR.JoinedGroupMember -> if (active(r.user)) { - withChats { - upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) } } is CR.ConnectedToGroupMember -> { if (active(r.user)) { - withChats { - 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()) - } } is CR.GroupUpdated -> if (active(r.user)) { - withChats { - updateGroup(rhId, r.toGroup) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.toGroup) } } is CR.NewMemberContactReceivedInv -> if (active(r.user)) { - withChats { - updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) } } is CR.RcvFileStart -> @@ -2905,26 +3051,26 @@ object ChatController { } is CR.ContactSwitch -> if (active(r.user)) { - withChats { - updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) } } is CR.GroupMemberSwitch -> if (active(r.user)) { - withChats { - updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) } } is CR.ContactRatchetSync -> if (active(r.user)) { - withChats { - updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) } } is CR.GroupMemberRatchetSync -> if (active(r.user)) { - withChats { - updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) } } is CR.RemoteHostSessionCode -> { @@ -2933,12 +3079,13 @@ object ChatController { 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)) { - withChats { - updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) } } } @@ -3062,20 +3209,21 @@ object ChatController { } is CR.ContactPQEnabled -> if (active(r.user)) { - withChats { - 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}") } } @@ -3104,12 +3252,6 @@ object ChatController { m.users.clear() m.users.addAll(users) getUserChatData(null) - val statuses = apiGetNetworkStatuses(null) - if (statuses != null) { - chatModel.networkStatuses.clear() - val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap() - chatModel.networkStatuses.putAll(ss) - } } private fun activeUser(rhId: Long?, user: UserLike): Boolean = @@ -3127,8 +3269,8 @@ object ChatController { suspend fun leaveGroup(rh: Long?, groupId: Long) { val groupInfo = apiLeaveGroup(rh, groupId) if (groupInfo != null) { - withChats { - updateGroup(rh, groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rh, groupInfo) } } } @@ -3137,10 +3279,65 @@ object ChatController { if (activeUser(rh, user)) { val cInfo = aChatItem.chatInfo val cItem = aChatItem.chatItem - withChats { upsertChatItem(rh, cInfo, cItem) } - withReportsChatsIfOpen { - if (cItem.isReport) { - upsertChatItem(rh, cInfo, cItem) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) + } + withContext(Dispatchers.Main) { + 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, groupChatScope = null) // TODO [knocking] get scope from items? + 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))) } } } @@ -3153,8 +3350,12 @@ object ChatController { if (!activeUser(rh, user)) { notify() } else { - val createdChat = withChats { upsertChatItem(rh, cInfo, cItem) } - withReportsChatsIfOpen { if (cItem.content.msgContent is MsgContent.MCReport) { upsertChatItem(rh, cInfo, cItem) } } + val createdChat = withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) + } if (createdChat) { notify() } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { @@ -3163,27 +3364,6 @@ object ChatController { } } - private fun updateContactsStatus(contactRefs: List, status: NetworkStatus) { - for (c in contactRefs) { - chatModel.networkStatuses[c.agentConnId] = status - } - } - - private fun processContactSubError(contact: Contact, chatError: ChatError) { - val e = chatError - val err: String = - if (e is ChatError.ChatErrorAgent) { - val a = e.agentError - when { - a is AgentErrorType.BROKER && a.brokerErr is BrokerErrorType.NETWORK -> "network" - a is AgentErrorType.SMP && a.smpErr is SMPErrorType.AUTH -> "contact deleted" - else -> e.string - } - } - else e.string - chatModel.setContactNetworkStatus(contact, NetworkStatus.Error(err)) - } - suspend fun switchUIRemoteHost(rhId: Long?) = showProgressIfNeeded { // TODO lock the switch so that two switches can't run concurrently? chatModel.chatId.value = null @@ -3199,23 +3379,17 @@ object ChatController { chatModel.users.addAll(users) chatModel.currentUser.value = user if (user == null) { - withChats { - chatItems.clearAndNotify() - chats.clear() - popChatCollector.clear() + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() + chatModel.chatsContext.chats.clear() + chatModel.chatsContext.popChatCollector.clear() } - withReportsChatsIfOpen { - chatItems.clearAndNotify() - chats.clear() - 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) { - chatModel.networkStatuses.clear() - val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap() - chatModel.networkStatuses.putAll(ss) - } getUserChatData(rhId) } @@ -3244,13 +3418,20 @@ object ChatController { } 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 = SMPProxyMode.valueOf(appPrefs.networkSMPProxyMode.get()!!) - val smpProxyFallback = SMPProxyFallback.valueOf(appPrefs.networkSMPProxyFallback.get()!!) - val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() - val tcpTimeout = appPrefs.networkTCPTimeout.get() + val smpProxyMode = appPrefs.networkSMPProxyMode.get() + val smpProxyFallback = appPrefs.networkSMPProxyFallback.get() + val smpWebPortServers = appPrefs.networkSMPWebPortServers.get() + val tcpConnectTimeout = NetworkTimeout( + backgroundTimeout = appPrefs.networkTCPConnectTimeoutBackground.get(), + interactiveTimeout = appPrefs.networkTCPConnectTimeoutInteractive.get() + ) + val tcpTimeout = NetworkTimeout( + backgroundTimeout = appPrefs.networkTCPTimeoutBackground.get(), + interactiveTimeout = appPrefs.networkTCPTimeoutInteractive.get() + ) val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get() val rcvConcurrency = appPrefs.networkRcvConcurrency.get() val smpPingInterval = appPrefs.networkSMPPingInterval.get() @@ -3271,6 +3452,7 @@ object ChatController { sessionMode = sessionMode, smpProxyMode = smpProxyMode, smpProxyFallback = smpProxyFallback, + smpWebPortServers = smpWebPortServers, tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpTimeoutPerKb = tcpTimeoutPerKb, @@ -3286,13 +3468,16 @@ object ChatController { * */ 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.name) - appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback.name) - appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) - appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) + appPrefs.networkSMPProxyMode.set(cfg.smpProxyMode) + appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback) + appPrefs.networkSMPWebPortServers.set(cfg.smpWebPortServers) + appPrefs.networkTCPConnectTimeoutBackground.set(cfg.tcpConnectTimeout.backgroundTimeout) + appPrefs.networkTCPConnectTimeoutInteractive.set(cfg.tcpConnectTimeout.interactiveTimeout) + appPrefs.networkTCPTimeoutBackground.set(cfg.tcpTimeout.backgroundTimeout) + appPrefs.networkTCPTimeoutInteractive.set(cfg.tcpTimeout.interactiveTimeout) appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb) appPrefs.networkRcvConcurrency.set(cfg.rcvConcurrency) appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval) @@ -3335,6 +3520,7 @@ sealed class CC { class SetAllContactReceipts(val enable: Boolean): CC() class ApiSetUserContactReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC() class ApiSetUserGroupReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC() + class ApiSetUserAutoAcceptMemberContacts(val userId: Long, val enable: Boolean): CC() class ApiHideUser(val userId: Long, val viewPwd: String): CC() class ApiUnhideUser(val userId: Long, val viewPwd: String): CC() class ApiMuteUser(val userId: Long): CC() @@ -3355,9 +3541,9 @@ sealed class 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 contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC() - class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC() - class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List): CC() + class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC() + class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC() + class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, 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() @@ -3365,19 +3551,23 @@ sealed class 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 mc: MsgContent, val live: Boolean): CC() - class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List, val mode: CIDeleteMode): CC() + class ApiUpdateChatItem(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC() + class ApiDeleteChatItem(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemIds: List, val mode: CIDeleteMode): CC() class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List): CC() - class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): 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 scope: GroupChatScope?, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC() class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC() - class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List): CC() - class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List, val ttl: Int?): CC() + class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List): CC() + class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, 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 ApiAcceptMember(val groupId: Long, val groupMemberId: Long, val memberRole: GroupMemberRole): CC() + class ApiDeleteMemberSupportChat(val groupId: Long, val groupMemberId: 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() @@ -3385,8 +3575,10 @@ sealed class CC { class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC() + class ApiAddGroupShortLink(val groupId: Long): CC() class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC() class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC() + class APIAcceptMemberContact(val contactId: Long): CC() class APITestProtoServer(val userId: Long, val server: String): CC() class ApiGetServerOperators(): CC() class ApiSetServerOperators(val operators: List): CC() @@ -3423,8 +3615,14 @@ sealed class CC { class APIAddContact(val userId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() - class APIConnectPlan(val userId: Long, val connReq: String): CC() - class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() + class APIConnectPlan(val userId: Long, val connLink: String): CC() + class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC() + class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val groupShortLinkData: GroupShortLinkData): CC() + class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC() + class APIChangePreparedGroupUser(val groupId: Long, val newUserId: Long): CC() + class APIConnectPreparedContact(val contactId: Long, val incognito: Boolean, val msg: MsgContent?): CC() + class APIConnectPreparedGroup(val groupId: Long, val incognito: Boolean, val msg: MsgContent?): 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 chatDeleteMode: ChatDeleteMode): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() @@ -3439,8 +3637,9 @@ sealed class CC { class ApiCreateMyAddress(val userId: Long): CC() class ApiDeleteMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC() + class ApiAddMyAddressShortLink(val userId: Long): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() - class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC() + class ApiSetAddressSettings(val userId: Long, val addressSettings: AddressSettings): CC() class ApiGetCallInvitations: CC() class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC() class ApiRejectCall(val contact: Contact): CC() @@ -3449,11 +3648,10 @@ sealed class CC { class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC() class ApiEndCall(val contact: Contact): CC() class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC() - class 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): CC() - class ApiChatItemsRead(val type: ChatType, val id: Long, val itemIds: List): CC() + class ApiChatRead(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC() + class ApiChatItemsRead(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemIds: List): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() @@ -3500,6 +3698,7 @@ sealed class CC { val mrs = userMsgReceiptSettings "/_set receipts groups $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}" } + is ApiSetUserAutoAcceptMemberContacts -> "/_set accept member contacts $userId ${onOff(enable)}" is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}" is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}" is ApiMuteUser -> "/_mute user $userId" @@ -3525,16 +3724,16 @@ sealed class CC { } else { " content=${contentTag.name.lowercase()}" } - "/_get chat ${chatRef(type, id)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") + "/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") } - is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId" + is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId" is ApiSendMessages -> { val msgs = json.encodeToString(composedMessages) val ttlStr = if (ttl != null) "$ttl" else "default" - "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs" + "/_send ${chatRef(type, id, scope)} live=${onOff(live)} ttl=${ttlStr} json $msgs" } is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}" - is ApiSetChatTags -> "/_tags ${chatRef(type, id)} ${tagIds.joinToString(",")}" + is ApiSetChatTags -> "/_tags ${chatRef(type, id, scope = null)} ${tagIds.joinToString(",")}" is ApiDeleteChatTag -> "/_delete tag $tagId" is ApiUpdateChatTag -> "/_update tag $tagId ${json.encodeToString(tagData)}" is ApiReorderChatTags -> "/_reorder tags ${tagIds.joinToString(",")}" @@ -3543,24 +3742,28 @@ sealed class CC { "/_create *$noteFolderId json $msgs" } is ApiReportMessage -> "/_report #$groupId $chatItemId reason=${json.encodeToString(reportReason).trim('"')} $reportText" - is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}" - is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}" + is ApiUpdateChatItem -> "/_update item ${chatRef(type, id, scope)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}" + is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id, scope)} ${itemIds.joinToString(",")} ${mode.deleteMode}" is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}" - is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}" + is ApiArchiveReceivedReports -> "/_archive reports #$groupId" + is ApiDeleteReceivedReports -> "/_delete reports #$groupId ${itemIds.joinToString(",")} ${mode.deleteMode}" + is ApiChatItemReaction -> "/_reaction ${chatRef(type, id, scope)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}" is ApiGetReactionMembers -> "/_reaction members $userId #$groupId $itemId ${json.encodeToString(reaction)}" is ApiForwardChatItems -> { val ttlStr = if (ttl != null) "$ttl" else "default" - "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}" + "/_forward ${chatRef(toChatType, toChatId, toScope)} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}" } is ApiPlanForwardChatItems -> { - "/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}" + "/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${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 ApiAcceptMember -> "/_accept member #$groupId $groupMemberId ${memberRole.memberRole}" + is ApiDeleteMemberSupportChat -> "/_delete member chat #$groupId $groupMemberId" + 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)}" @@ -3568,8 +3771,10 @@ sealed class CC { is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIDeleteGroupLink -> "/_delete link #$groupId" is APIGetGroupLink -> "/_get link #$groupId" + is ApiAddGroupShortLink -> "/_short link #$groupId" is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId" is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}" + is APIAcceptMemberContact -> "/_accept member contact @$contactId" is APITestProtoServer -> "/_server test $userId $server" is ApiGetServerOperators -> "/_operators" is ApiSetServerOperators -> "/_operators ${json.encodeToString(operators)}" @@ -3581,13 +3786,13 @@ sealed class CC { 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 APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id, scope = null)} ${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 APISetChatSettings -> "/_settings ${chatRef(type, id, scope = null)} ${json.encodeToString(chatSettings)}" is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}" is APIContactInfo -> "/_info @$contactId" is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId" @@ -3606,11 +3811,17 @@ sealed class CC { is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" - is APIConnectPlan -> "/_connect plan $userId $connReq" - is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" + is APIConnectPlan -> "/_connect plan $userId $connLink" + is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}" + is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(groupShortLinkData)}" + is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId" + is APIChangePreparedGroupUser -> "/_set group user #$groupId $newUserId" + is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)}${maybeContent(msg)}" + is APIConnectPreparedGroup -> "/_connect group #$groupId incognito=${onOff(incognito)}${maybeContent(msg)}" + is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}" is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId" - is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}" - is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" + is ApiDeleteChat -> "/_delete ${chatRef(type, id, scope = null)} ${chatDeleteMode.cmdString}" + is ApiClearChat -> "/_clear chat ${chatRef(type, id, scope = null)}" is ApiListContacts -> "/_contacts $userId" is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}" is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}" @@ -3622,8 +3833,9 @@ sealed class CC { is ApiCreateMyAddress -> "/_address $userId" is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiShowMyAddress -> "/_show_address $userId" + is ApiAddMyAddressShortLink -> "/_short_link_address $userId" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" - is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}" + is ApiSetAddressSettings -> "/_address_settings $userId ${json.encodeToString(addressSettings)}" is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId" is ApiRejectContact -> "/_reject $contactReqId" is ApiGetCallInvitations -> "/_call get" @@ -3634,14 +3846,13 @@ sealed class CC { is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}" 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)}" - is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}" - is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" + is ApiChatRead -> "/_read chat ${chatRef(type, id, scope)}" + is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id, scope)} ${itemIds.joinToString(",")}" + is ApiChatUnread -> "/_unread chat ${chatRef(type, id, scope = null)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId" + - (" approved_relays=${onOff(userApprovedRelays)}") + - (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" @@ -3680,6 +3891,7 @@ sealed class CC { is SetAllContactReceipts -> "setAllContactReceipts" is ApiSetUserContactReceipts -> "apiSetUserContactReceipts" is ApiSetUserGroupReceipts -> "apiSetUserGroupReceipts" + is ApiSetUserAutoAcceptMemberContacts -> "apiSetUserAutoAcceptMemberContacts" is ApiHideUser -> "apiHideUser" is ApiUnhideUser -> "apiUnhideUser" is ApiMuteUser -> "apiMuteUser" @@ -3712,6 +3924,8 @@ sealed class CC { is ApiUpdateChatItem -> "apiUpdateChatItem" is ApiDeleteChatItem -> "apiDeleteChatItem" is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem" + is ApiArchiveReceivedReports -> "apiArchiveReceivedReports" + is ApiDeleteReceivedReports -> "apiDeleteReceivedReports" is ApiChatItemReaction -> "apiChatItemReaction" is ApiGetReactionMembers -> "apiGetReactionMembers" is ApiForwardChatItems -> "apiForwardChatItems" @@ -3719,9 +3933,11 @@ sealed class CC { is ApiNewGroup -> "apiNewGroup" is ApiAddMember -> "apiAddMember" is ApiJoinGroup -> "apiJoinGroup" - is ApiMemberRole -> "apiMemberRole" - is ApiBlockMemberForAll -> "apiBlockMemberForAll" - is ApiRemoveMember -> "apiRemoveMember" + is ApiAcceptMember -> "apiAcceptMember" + is ApiDeleteMemberSupportChat -> "apiDeleteMemberSupportChat" + is ApiMembersRole -> "apiMembersRole" + is ApiBlockMembersForAll -> "apiBlockMembersForAll" + is ApiRemoveMembers -> "apiRemoveMembers" is ApiLeaveGroup -> "apiLeaveGroup" is ApiListMembers -> "apiListMembers" is ApiUpdateGroupProfile -> "apiUpdateGroupProfile" @@ -3729,8 +3945,10 @@ sealed class CC { is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole" is APIDeleteGroupLink -> "apiDeleteGroupLink" is APIGetGroupLink -> "apiGetGroupLink" + is ApiAddGroupShortLink -> "apiAddGroupShortLink" is APICreateMemberContact -> "apiCreateMemberContact" is APISendMemberContactInvitation -> "apiSendMemberContactInvitation" + is APIAcceptMemberContact -> "apiAcceptMemberContact" is APITestProtoServer -> "testProtoServer" is ApiGetServerOperators -> "apiGetServerOperators" is ApiSetServerOperators -> "apiSetServerOperators" @@ -3768,6 +3986,12 @@ sealed class CC { is ApiSetConnectionIncognito -> "apiSetConnectionIncognito" is ApiChangeConnectionUser -> "apiChangeConnectionUser" is APIConnectPlan -> "apiConnectPlan" + is APIPrepareContact -> "apiPrepareContact" + is APIPrepareGroup -> "apiPrepareGroup" + is APIChangePreparedContactUser -> "apiChangePreparedContactUser" + is APIChangePreparedGroupUser -> "apiChangePreparedGroupUser" + is APIConnectPreparedContact -> "apiConnectPreparedContact" + is APIConnectPreparedGroup -> "apiConnectPreparedGroup" is APIConnect -> "apiConnect" is ApiConnectContactViaAddress -> "apiConnectContactViaAddress" is ApiDeleteChat -> "apiDeleteChat" @@ -3783,8 +4007,9 @@ sealed class CC { is ApiCreateMyAddress -> "apiCreateMyAddress" is ApiDeleteMyAddress -> "apiDeleteMyAddress" is ApiShowMyAddress -> "apiShowMyAddress" + is ApiAddMyAddressShortLink -> "apiAddMyAddressShortLink" is ApiSetProfileAddress -> "apiSetProfileAddress" - is ApiAddressAutoAccept -> "apiAddressAutoAccept" + is ApiSetAddressSettings -> "apiSetAddressSettings" is ApiAcceptContact -> "apiAcceptContact" is ApiRejectContact -> "apiRejectContact" is ApiGetCallInvitations -> "apiGetCallInvitations" @@ -3795,7 +4020,6 @@ sealed class CC { is ApiSendCallExtraInfo -> "apiSendCallExtraInfo" is ApiEndCall -> "apiEndCall" is ApiCallStatus -> "apiCallStatus" - is ApiGetNetworkStatuses -> "apiGetNetworkStatuses" is ApiChatRead -> "apiChatRead" is ApiChatItemsRead -> "apiChatItemsRead" is ApiChatUnread -> "apiChatUnread" @@ -3854,8 +4078,22 @@ sealed class CC { private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd) + private fun maybeContent(mc: MsgContent?): String { + return when { + mc is MsgContent.MCText && mc.text.isEmpty() -> "" + mc != null -> " " + mc.cmdString + else -> "" + } + } + companion object { - fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" + fun chatRef(chatType: ChatType, id: Long, scope: GroupChatScope?) = when (scope) { + null -> "${chatType.type}${id}" + is GroupChatScope.MemberSupport -> when (scope.groupMemberId_) { + null -> "${chatType.type}${id}(_support)" + else -> "${chatType.type}${id}(_support:${scope.groupMemberId_})" + } + } } } @@ -3890,7 +4128,13 @@ sealed class ChatPagination { } @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) @@ -4324,9 +4568,10 @@ data class ProtocolTestFailure( err + " " + generalGetString(MR.strings.error_smp_test_server_auth) testError is AgentErrorType.XFTP && testError.xftpErr is XFTPErrorType.AUTH -> err + " " + generalGetString(MR.strings.error_xftp_test_server_auth) - testError is AgentErrorType.BROKER && testError.brokerErr is BrokerErrorType.NETWORK -> + testError is AgentErrorType.BROKER && testError.brokerErr is BrokerErrorType.NETWORK && testError.brokerErr.networkError is NetworkError.UnknownCAError -> err + " " + generalGetString(MR.strings.error_smp_test_certificate) - else -> err + else -> + err + " " + String.format(generalGetString(MR.strings.error_with_info), testError.toString()) } } } @@ -4377,18 +4622,31 @@ data class ParsedServerAddress ( var parseError: String ) +fun parseSanitizeUri(s: String, safe: Boolean): ParsedUri? { + val parsed = chatParseUri(s, if (safe) 1 else 0) + return runCatching { json.decodeFromString(ParsedUri.serializer(), parsed) } + .onFailure { Log.d(TAG, "parseSanitizeUri decode error: $it") } + .getOrNull() +} + +@Serializable +data class ParsedUri(val uriInfo: UriInfo?, val parseError: String) + +@Serializable +data class UriInfo(val scheme: String, val sanitized: String?) + @Serializable data class NetCfg( val socksProxy: String?, - val socksMode: SocksMode = SocksMode.Always, - val hostMode: HostMode = HostMode.OnionViaSocks, + val socksMode: SocksMode = SocksMode.default, + val hostMode: HostMode = HostMode.default, val requiredHostMode: Boolean = false, val sessionMode: TransportSessionMode = TransportSessionMode.default, - val smpProxyMode: SMPProxyMode = SMPProxyMode.Always, - val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.AllowProtected, - val smpWebPort: Boolean = false, - val tcpConnectTimeout: Long, // microseconds - val tcpTimeout: Long, // microseconds + val smpProxyMode: SMPProxyMode = SMPProxyMode.default, + val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.default, + val smpWebPortServers: SMPWebPortServers = SMPWebPortServers.default, + val tcpConnectTimeout: NetworkTimeout, + val tcpTimeout: NetworkTimeout, val tcpTimeoutPerKb: Long, // microseconds val rcvConcurrency: Int, // pool size val tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults, @@ -4407,8 +4665,8 @@ data class NetCfg( val defaults: NetCfg = NetCfg( socksProxy = null, - tcpConnectTimeout = 25_000_000, - tcpTimeout = 15_000_000, + tcpConnectTimeout = NetworkTimeout(backgroundTimeout = 45_000_000, interactiveTimeout = 15_000_000), + tcpTimeout = NetworkTimeout(backgroundTimeout = 30_000_000, interactiveTimeout = 10_000_000), tcpTimeoutPerKb = 10_000, rcvConcurrency = 12, smpPingInterval = 1200_000_000 @@ -4417,8 +4675,8 @@ data class NetCfg( val proxyDefaults: NetCfg = NetCfg( socksProxy = ":9050", - tcpConnectTimeout = 35_000_000, - tcpTimeout = 20_000_000, + tcpConnectTimeout = NetworkTimeout(backgroundTimeout = 60_000_000, interactiveTimeout = 30_000_000), + tcpTimeout = NetworkTimeout(backgroundTimeout = 40_000_000, interactiveTimeout = 20_000_000), tcpTimeoutPerKb = 15_000, rcvConcurrency = 8, smpPingInterval = 1200_000_000 @@ -4442,6 +4700,12 @@ data class NetCfg( } } +@Serializable +data class NetworkTimeout( + val backgroundTimeout: Long, // microseconds + val interactiveTimeout: Long // microseconds +) + @Serializable data class NetworkProxy( val username: String = "", @@ -4484,12 +4748,20 @@ 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 @@ -4498,6 +4770,10 @@ enum class SMPProxyMode { @SerialName("unknown") Unknown, @SerialName("unprotected") Unprotected, @SerialName("never") Never; + + companion object { + val default = Always + } } @Serializable @@ -4505,6 +4781,27 @@ 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 @@ -4547,7 +4844,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 @@ -4559,14 +4886,18 @@ data class FullChatPreferences( val fullDelete: SimpleChatPreference, val reactions: SimpleChatPreference, val voice: SimpleChatPreference, + val files: SimpleChatPreference, val calls: SimpleChatPreference, + val commands: List ) { fun toPreferences(): ChatPreferences = ChatPreferences( timedMessages = timedMessages, fullDelete = fullDelete, reactions = reactions, voice = voice, - calls = calls + files = files, + calls = calls, + commands = commands, ) companion object { @@ -4575,7 +4906,9 @@ data class FullChatPreferences( fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO), reactions = SimpleChatPreference(allow = FeatureAllowed.YES), voice = SimpleChatPreference(allow = FeatureAllowed.YES), + files = SimpleChatPreference(allow = FeatureAllowed.YES), calls = SimpleChatPreference(allow = FeatureAllowed.YES), + commands = listOf(), ) } } @@ -4586,7 +4919,9 @@ data class ChatPreferences( val fullDelete: SimpleChatPreference?, val reactions: SimpleChatPreference?, val voice: SimpleChatPreference?, + val files: SimpleChatPreference?, val calls: SimpleChatPreference?, + val commands: List?, ) { fun setAllowed(feature: ChatFeature, allowed: FeatureAllowed = FeatureAllowed.YES, param: Int? = null): ChatPreferences = when (feature) { @@ -4594,6 +4929,7 @@ data class ChatPreferences( ChatFeature.FullDelete -> this.copy(fullDelete = SimpleChatPreference(allow = allowed)) ChatFeature.Reactions -> this.copy(reactions = SimpleChatPreference(allow = allowed)) ChatFeature.Voice -> this.copy(voice = SimpleChatPreference(allow = allowed)) + ChatFeature.Files -> this.copy(files = SimpleChatPreference(allow = allowed)) ChatFeature.Calls -> this.copy(calls = SimpleChatPreference(allow = allowed)) } @@ -4603,7 +4939,9 @@ data class ChatPreferences( fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO), reactions = SimpleChatPreference(allow = FeatureAllowed.YES), voice = SimpleChatPreference(allow = FeatureAllowed.YES), + files = SimpleChatPreference(allow = FeatureAllowed.YES), calls = SimpleChatPreference(allow = FeatureAllowed.YES), + commands = null, ) } } @@ -4625,9 +4963,18 @@ data class TimedMessagesPreference( companion object { val ttlValues: List get() = listOf(600, 3600, 86400, 7 * 86400, 30 * 86400, 3 * 30 * 86400, null) + + val profileLevelTTLValues: List + get() = listOf(7 * 86400, 30 * 86400, 3 * 30 * 86400, null) } } +@Serializable +sealed class ChatBotCommand { + @Serializable @SerialName("command") class Command(val keyword: String, val label: String, val params: String?): ChatBotCommand() + @Serializable @SerialName("menu") class Menu(val label: String, val commands: List): ChatBotCommand() +} + @Serializable data class PresentedServersSummary( val statsStartedAt: Instant, @@ -4878,14 +5225,18 @@ data class ContactUserPreferences( val fullDelete: ContactUserPreference, val reactions: ContactUserPreference, val voice: ContactUserPreference, + val files: ContactUserPreference, val calls: ContactUserPreference, + val commands: List?, ) { fun toPreferences(): ChatPreferences = ChatPreferences( timedMessages = timedMessages.userPreference.pref, fullDelete = fullDelete.userPreference.pref, reactions = reactions.userPreference.pref, voice = voice.userPreference.pref, - calls = calls.userPreference.pref + files = files.userPreference.pref, + calls = calls.userPreference.pref, + commands = commands, ) companion object { @@ -4910,11 +5261,17 @@ data class ContactUserPreferences( userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) ), + files = ContactUserPreference( + enabled = FeatureEnabled(forUser = true, forContact = true), + userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), + contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) + ), calls = ContactUserPreference( enabled = FeatureEnabled(forUser = true, forContact = true), userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) ), + commands = null, ) } } @@ -5004,6 +5361,7 @@ enum class ChatFeature: Feature { @SerialName("fullDelete") FullDelete, @SerialName("reactions") Reactions, @SerialName("voice") Voice, + @SerialName("files") Files, @SerialName("calls") Calls; val asymmetric: Boolean get() = when (this) { @@ -5023,6 +5381,7 @@ enum class ChatFeature: Feature { FullDelete -> generalGetString(MR.strings.full_deletion) Reactions -> generalGetString(MR.strings.message_reactions) Voice -> generalGetString(MR.strings.voice_messages) + Files -> generalGetString(MR.strings.files_and_media) Calls -> generalGetString(MR.strings.audio_video_calls) } @@ -5032,6 +5391,7 @@ enum class ChatFeature: Feature { FullDelete -> painterResource(MR.images.ic_delete_forever) Reactions -> painterResource(MR.images.ic_add_reaction) Voice -> painterResource(MR.images.ic_keyboard_voice) + Files -> painterResource(MR.images.ic_draft) Calls -> painterResource(MR.images.ic_call) } @@ -5041,6 +5401,7 @@ enum class ChatFeature: Feature { FullDelete -> painterResource(MR.images.ic_delete_forever_filled) Reactions -> painterResource(MR.images.ic_add_reaction_filled) Voice -> painterResource(MR.images.ic_keyboard_voice_filled) + Files -> painterResource(MR.images.ic_draft_filled) Calls -> painterResource(MR.images.ic_call_filled) } @@ -5061,11 +5422,16 @@ enum class ChatFeature: Feature { FeatureAllowed.YES -> generalGetString(MR.strings.allow_message_reactions_only_if) FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_message_reactions) } - Voice -> when (allowed) { + Voice -> when (allowed) { FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_send_voice_messages) FeatureAllowed.YES -> generalGetString(MR.strings.allow_voice_messages_only_if) FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_sending_voice_messages) } + Files -> when (allowed) { + FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_send_files_and_media) + FeatureAllowed.YES -> generalGetString(MR.strings.allow_files_and_media_only_if) + FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_sending_files_and_media) + } Calls -> when (allowed) { FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_call) FeatureAllowed.YES -> generalGetString(MR.strings.allow_calls_only_if) @@ -5099,6 +5465,12 @@ enum class ChatFeature: Feature { enabled.forContact -> generalGetString(MR.strings.only_your_contact_can_send_voice) else -> generalGetString(MR.strings.voice_prohibited_in_this_chat) } + Files -> when { + enabled.forUser && enabled.forContact -> generalGetString(MR.strings.both_you_and_your_contact_can_send_files) + enabled.forUser -> generalGetString(MR.strings.only_you_can_send_files) + enabled.forContact -> generalGetString(MR.strings.only_your_contact_can_send_files) + else -> generalGetString(MR.strings.files_prohibited_in_this_chat) + } Calls -> when { enabled.forUser && enabled.forContact -> generalGetString(MR.strings.both_you_and_your_contact_can_make_calls) enabled.forUser -> generalGetString(MR.strings.only_you_can_make_calls) @@ -5117,6 +5489,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) { @@ -5133,6 +5506,7 @@ enum class GroupFeature: Feature { Voice -> true Files -> true SimplexLinks -> true + Reports -> false History -> false } @@ -5145,6 +5519,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) } @@ -5157,6 +5532,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) } @@ -5169,6 +5545,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) } @@ -5203,6 +5580,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) @@ -5238,6 +5619,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) @@ -5280,7 +5665,9 @@ data class ContactFeaturesAllowed( val fullDelete: ContactFeatureAllowed, val reactions: ContactFeatureAllowed, val voice: ContactFeatureAllowed, + val files: ContactFeatureAllowed, val calls: ContactFeatureAllowed, + val commands: List?, ) { companion object { val sampleData = ContactFeaturesAllowed( @@ -5289,7 +5676,9 @@ data class ContactFeaturesAllowed( fullDelete = ContactFeatureAllowed.UserDefault(FeatureAllowed.NO), reactions = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), voice = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), + files = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), calls = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), + commands = null, ) } } @@ -5303,7 +5692,9 @@ fun contactUserPrefsToFeaturesAllowed(contactUserPreferences: ContactUserPrefere fullDelete = contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete), reactions = contactUserPrefToFeatureAllowed(contactUserPreferences.reactions), voice = contactUserPrefToFeatureAllowed(contactUserPreferences.voice), + files = contactUserPrefToFeatureAllowed(contactUserPreferences.files), calls = contactUserPrefToFeatureAllowed(contactUserPreferences.calls), + commands = contactUserPreferences.commands, ) } @@ -5323,7 +5714,9 @@ fun contactFeaturesAllowedToPrefs(contactFeaturesAllowed: ContactFeaturesAllowed fullDelete = contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete), reactions = contactFeatureAllowedToPref(contactFeaturesAllowed.reactions), voice = contactFeatureAllowedToPref(contactFeaturesAllowed.voice), + files = contactFeatureAllowedToPref(contactFeaturesAllowed.files), calls = contactFeatureAllowedToPref(contactFeaturesAllowed.calls), + commands = contactFeaturesAllowed.commands, ) fun contactFeatureAllowedToPref(contactFeatureAllowed: ContactFeatureAllowed): SimpleChatPreference? = @@ -5357,7 +5750,9 @@ data class FullGroupPreferences( val voice: RoleGroupPreference, val files: RoleGroupPreference, val simplexLinks: RoleGroupPreference, + val reports: GroupPreference, val history: GroupPreference, + val commands: List, ) { fun toGroupPreferences(): GroupPreferences = GroupPreferences( @@ -5368,7 +5763,9 @@ data class FullGroupPreferences( voice = voice, files = files, simplexLinks = simplexLinks, - history = history + reports = reports, + history = history, + commands = commands, ) companion object { @@ -5380,7 +5777,9 @@ 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), + commands = listOf() ) } } @@ -5394,7 +5793,9 @@ data class GroupPreferences( val voice: RoleGroupPreference? = null, val files: RoleGroupPreference? = null, val simplexLinks: RoleGroupPreference? = null, + val reports: GroupPreference? = null, val history: GroupPreference? = null, + val commands: List? = null ) { companion object { val sampleData = GroupPreferences( @@ -5405,7 +5806,9 @@ 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), + commands = null, ) } } @@ -5546,56 +5949,122 @@ val yaml = Yaml(configuration = YamlConfiguration( 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) } } @@ -5650,22 +6119,26 @@ 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("tagsUpdated") class TagsUpdated(val user: UserRef, val userTags: List, val chatTags: List): CR() - @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): 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("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 connectionPlan: ConnectionPlan): CR() + @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val connectionPlan: ConnectionPlan): CR() + @Serializable @SerialName("newPreparedChat") class NewPreparedChat(val user: UserRef, val chat: Chat): CR() + @Serializable @SerialName("contactUserChanged") class ContactUserChanged(val user: UserRef, val fromContact: Contact, val newUser: UserRef, val toContact: Contact): CR() + @Serializable @SerialName("groupUserChanged") class GroupUserChanged(val user: UserRef, val fromGroup: GroupInfo, val newUser: UserRef, val toGroup: GroupInfo): 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("startedConnectionToContact") class StartedConnectionToContact(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("startedConnectionToGroup") class StartedConnectionToGroup(val user: UserRef, val groupInfo: GroupInfo): 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("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactDeletedByContact") class ContactDeletedByContact(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("itemsReadForChat") class ItemsReadForChat(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR() @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile, val updateSummary: UserProfileUpdateSummary): CR() @@ -5676,27 +6149,18 @@ sealed class 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("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest, val chat_: Chat?): CR() @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR() - @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR() + @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef, val contactRequest: UserContactRequest, val contact_: Contact?): CR() @Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR() @Serializable @SerialName("groupMemberUpdated") class GroupMemberUpdated(val user: UserRef, val groupInfo: GroupInfo, val fromMember: GroupMember, val toMember: GroupMember): CR() - // TODO remove below - @Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List): CR() - @Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List): CR() - @Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: UserRef, val contactSubscriptions: List): 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("subscriptionStatus") class SubscriptionStatusEvt(val subscriptionStatus: SubscriptionStatus, val connections: List): CR() + @Serializable @SerialName("chatInfoUpdated") class ChatInfoUpdated(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List): CR() @Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List): CR() @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() @@ -5712,32 +6176,34 @@ sealed class 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("businessLinkConnecting") class BusinessLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val fromContact: Contact): CR() - @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): 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("memberAccepted") class MemberAccepted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("memberSupportChatRead") class MemberSupportChatRead(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("memberSupportChatDeleted") class MemberSupportChatDeleted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("memberAcceptedByOther") class MemberAcceptedByOther(val user: UserRef, val groupInfo: GroupInfo, val acceptingMember: 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 groupLink: GroupLink): CR() + @Serializable @SerialName("groupLink") class CRGroupLink(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR() @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("memberContactAccepted") class MemberContactAccepted(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("newMemberContactReceivedInv") class NewMemberContactReceivedInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() // receiving file events @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR() @@ -5758,12 +6224,10 @@ sealed class CR { @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem_: AChatItem?, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List): CR() @Serializable @SerialName("sndStandaloneFileCreated") class SndStandaloneFileCreated(val user: UserRef, val fileTransferMeta: FileTransferMeta): CR() // returned by _upload - @Serializable @SerialName("sndFileStartXFTP") class SndFileStartXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() // not used @Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR() @Serializable @SerialName("sndFileRedirectStartXFTP") class SndFileRedirectStartXFTP(val user: UserRef, val fileTransferMeta: FileTransferMeta, val redirectMeta: FileTransferMeta): CR() @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() @Serializable @SerialName("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, val errorMessage: String): CR() @Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() // call events @@ -5797,8 +6261,6 @@ 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() @@ -5838,8 +6300,6 @@ 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" @@ -5848,12 +6308,18 @@ sealed class CR { is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated" is ConnectionUserChanged -> "ConnectionUserChanged" is CRConnectionPlan -> "connectionPlan" + is NewPreparedChat -> "newPreparedChat" + is ContactUserChanged -> "contactUserChanged" + is GroupUserChanged -> "groupUserChanged" is SentConfirmation -> "sentConfirmation" is SentInvitation -> "sentInvitation" + is StartedConnectionToContact -> "startedConnectionToContact" + is StartedConnectionToGroup -> "startedConnectionToGroup" is SentInvitationToContact -> "sentInvitationToContact" is ContactAlreadyExists -> "contactAlreadyExists" is ContactDeleted -> "contactDeleted" is ContactDeletedByContact -> "contactDeletedByContact" + is ItemsReadForChat -> "itemsReadForChat" is ChatCleared -> "chatCleared" is UserProfileNoChange -> "userProfileNoChange" is UserProfileUpdated -> "userProfileUpdated" @@ -5874,15 +6340,8 @@ sealed class CR { is ContactRequestRejected -> "contactRequestRejected" is ContactUpdated -> "contactUpdated" is GroupMemberUpdated -> "groupMemberUpdated" - is ContactsSubscribed -> "contactsSubscribed" - is ContactsDisconnected -> "contactsDisconnected" - is ContactSubSummary -> "contactSubSummary" - is NetworkStatusResp -> "networkStatus" - is NetworkStatuses -> "networkStatuses" - is GroupSubscribed -> "groupSubscribed" - is MemberSubErrors -> "memberSubErrors" - is GroupEmpty -> "groupEmpty" - is UserContactLinkSubscribed -> "userContactLinkSubscribed" + is SubscriptionStatusEvt -> "subscriptionStatus" + is ChatInfoUpdated -> "chatInfoUpdated" is NewChatItems -> "newChatItems" is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated" is ChatItemUpdated -> "chatItemUpdated" @@ -5897,32 +6356,34 @@ sealed class CR { is UserAcceptedGroupSent -> "userAcceptedGroupSent" is GroupLinkConnecting -> "groupLinkConnecting" is BusinessLinkConnecting -> "businessLinkConnecting" - is UserDeletedMember -> "userDeletedMember" + is UserDeletedMembers -> "userDeletedMembers" is LeftMemberUser -> "leftMemberUser" is GroupMembers -> "groupMembers" is ReceivedGroupInvitation -> "receivedGroupInvitation" is GroupDeletedUser -> "groupDeletedUser" is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting" + is MemberAccepted -> "memberAccepted" + is MemberSupportChatRead -> "memberSupportChatRead" + is MemberSupportChatDeleted -> "memberSupportChatDeleted" + is MemberAcceptedByOther -> "memberAcceptedByOther" 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" + is CRGroupLink -> "groupLink" is GroupLinkDeleted -> "groupLinkDeleted" is NewMemberContact -> "newMemberContact" is NewMemberContactSentInv -> "newMemberContactSentInv" + is MemberContactAccepted -> "memberContactAccepted" is NewMemberContactReceivedInv -> "newMemberContactReceivedInv" is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled" is StandaloneFileInfo -> "standaloneFileInfo" @@ -5933,7 +6394,6 @@ sealed class CR { is RcvStandaloneFileComplete -> "rcvStandaloneFileComplete" is RcvFileCancelled -> "rcvFileCancelled" is SndStandaloneFileCreated -> "sndStandaloneFileCreated" - is SndFileStartXFTP -> "sndFileStartXFTP" is RcvFileSndCancelled -> "rcvFileSndCancelled" is RcvFileProgressXFTP -> "rcvFileProgressXFTP" is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP" @@ -5946,7 +6406,6 @@ sealed class CR { is SndFileProgressXFTP -> "sndFileProgressXFTP" is SndFileCompleteXFTP -> "sndFileCompleteXFTP" is SndStandaloneFileComplete -> "sndStandaloneFileComplete" - is SndFileCancelledXFTP -> "sndFileCancelledXFTP" is SndFileError -> "sndFileError" is SndFileWarning -> "sndFileWarning" is CallInvitations -> "callInvitations" @@ -5977,8 +6436,6 @@ sealed class CR { is AgentSubsTotal -> "agentSubsTotal" is AgentServersSummary -> "agentServersSummary" is CmdOk -> "cmdOk" - is ChatCmdError -> "chatCmdError" - is ChatRespError -> "chatError" is ArchiveExported -> "archiveExported" is ArchiveImported -> "archiveImported" is AppSettingsR -> "appSettings" @@ -6016,22 +6473,26 @@ 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 TagsUpdated -> withUser(user, "userTags: ${json.encodeToString(userTags)}\nchatTags: ${json.encodeToString(chatTags)}") - is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection") + is Invitation -> withUser(user, "connLinkInvitation: ${json.encodeToString(connLinkInvitation)}\nconnection: $connection") is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" ) - is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan)) + is CRConnectionPlan -> withUser(user, "connLink: ${json.encodeToString(connLink)}\nconnectionPlan: ${json.encodeToString(connectionPlan)}") + is NewPreparedChat -> withUser(user, json.encodeToString(chat)) + is ContactUserChanged -> withUser(user, "fromContact: ${json.encodeToString(fromContact)}\nnewUserId: ${json.encodeToString(newUser.userId)}\ntoContact: ${json.encodeToString(toContact)}") + is GroupUserChanged -> withUser(user, "fromGroup: ${json.encodeToString(fromGroup)}\nnewUserId: ${json.encodeToString(newUser.userId)}\ntoGroup: ${json.encodeToString(toGroup)}") is SentConfirmation -> withUser(user, json.encodeToString(connection)) is SentInvitation -> withUser(user, json.encodeToString(connection)) + is StartedConnectionToContact -> withUser(user, json.encodeToString(contact)) + is StartedConnectionToGroup -> withUser(user, json.encodeToString(groupInfo)) is SentInvitationToContact -> withUser(user, json.encodeToString(contact)) is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) is ContactDeleted -> withUser(user, json.encodeToString(contact)) is ContactDeletedByContact -> withUser(user, json.encodeToString(contact)) + is ItemsReadForChat -> withUser(user, json.encodeToString(chatInfo)) is ChatCleared -> withUser(user, json.encodeToString(chatInfo)) is UserProfileNoChange -> withUser(user, noDetails()) is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile)) @@ -6040,27 +6501,20 @@ sealed class CR { 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 UserContactLink -> withUser(user, json.encodeToString(contactLink)) + is UserContactLinkUpdated -> withUser(user, json.encodeToString(contactLink)) + 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 ReceivedContactRequest -> withUser(user, "contactRequest: ${json.encodeToString(contactRequest)}\nchat_: ${json.encodeToString(chat_)}") is AcceptingContactRequest -> withUser(user, json.encodeToString(contact)) - is ContactRequestRejected -> withUser(user, noDetails()) + is ContactRequestRejected -> withUser(user, "contactRequest: ${json.encodeToString(contactRequest)}\ncontact_: ${json.encodeToString(contact_)}") is ContactUpdated -> withUser(user, json.encodeToString(toContact)) is GroupMemberUpdated -> withUser(user, "groupInfo: $groupInfo\nfromMember: $fromMember\ntoMember: $toMember") - is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" - is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" - 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 SubscriptionStatusEvt -> "subscriptionStatus $subscriptionStatus\nconnections: $connections" + is ChatInfoUpdated -> withUser(user, json.encodeToString(chatInfo)) 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)) @@ -6075,32 +6529,34 @@ sealed class CR { is UserAcceptedGroupSent -> json.encodeToString(groupInfo) is GroupLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember") is BusinessLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nfromContact: $fromContact") - is UserDeletedMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + 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 MemberAccepted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is MemberSupportChatRead -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is MemberSupportChatDeleted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is MemberAcceptedByOther -> withUser(user, "groupInfo: $groupInfo\nacceptingMember: $acceptingMember\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\ngroupLink: $groupLink") + is CRGroupLink -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink") is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") + is MemberContactAccepted -> withUser(user, "contact: $contact") is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is RcvFileAcceptedSndCancelled -> withUser(user, noDetails()) is StandaloneFileInfo -> json.encodeToString(fileMeta) @@ -6116,7 +6572,6 @@ sealed class CR { is RcvFileWarning -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") is SndFileCancelled -> json.encodeToString(chatItem_) is SndStandaloneFileCreated -> noDetails() - is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem)) is SndFileComplete -> withUser(user, json.encodeToString(chatItem)) is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem_)) is SndFileStart -> withUser(user, json.encodeToString(chatItem)) @@ -6124,7 +6579,6 @@ sealed class CR { is SndFileRedirectStartXFTP -> withUser(user, json.encodeToString(redirectMeta)) 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, "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)}" @@ -6172,8 +6626,6 @@ sealed class CR { "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) @@ -6186,13 +6638,9 @@ 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 { @@ -6207,16 +6655,28 @@ sealed class ChatDeleteMode { } } +@Serializable +data class CreatedConnLink(val connFullLink: String, val connShortLink: String?) { + fun simplexChatUri(short: Boolean): String = + if (short) connShortLink ?: simplexChatLink(connFullLink) + else simplexChatLink(connFullLink) +} + +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 sealed class InvitationLinkPlan { - @Serializable @SerialName("ok") object Ok: InvitationLinkPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): InvitationLinkPlan() @Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan() @Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan() @Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan() @@ -6224,7 +6684,7 @@ sealed class InvitationLinkPlan { @Serializable sealed class ContactAddressPlan { - @Serializable @SerialName("ok") object Ok: ContactAddressPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): ContactAddressPlan() @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan() @@ -6234,7 +6694,7 @@ sealed class ContactAddressPlan { @Serializable sealed class GroupLinkPlan { - @Serializable @SerialName("ok") object Ok: GroupLinkPlan() + @Serializable @SerialName("ok") class Ok(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan() @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() @@ -6254,7 +6714,7 @@ abstract class TerminalItem { 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 } @@ -6262,11 +6722,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) } } @@ -6276,7 +6736,8 @@ class ConnectionStats( val rcvQueuesInfo: List, val sndQueuesInfo: List, val ratchetSyncState: RatchetSyncState, - val ratchetSyncSupported: Boolean + val ratchetSyncSupported: Boolean, + var subStatus: SubscriptionStatus? ) { val ratchetSyncAllowed: Boolean get() = ratchetSyncSupported && listOf(RatchetSyncState.Allowed, RatchetSyncState.Required).contains(ratchetSyncState) @@ -6291,8 +6752,10 @@ class ConnectionStats( @Serializable class RcvQueueInfo( val rcvServer: String, + var status: QueueStatus, val rcvSwitchStatus: RcvSwitchStatus?, - var canAbortSwitch: Boolean + var canAbortSwitch: Boolean, + var subStatus: SubscriptionStatus ) @Serializable @@ -6306,6 +6769,7 @@ enum class RcvSwitchStatus { @Serializable class SndQueueInfo( val sndServer: String, + var status: QueueStatus, val sndSwitchStatus: SndSwitchStatus? ) @@ -6344,24 +6808,112 @@ enum class RatchetSyncState { } @Serializable -class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) { - val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}" +enum class QueueStatus { + @SerialName("new") New, + @SerialName("confirmed") Confirmed, + @SerialName("secured") Secured, + @SerialName("active") Active, + @SerialName("disabled") Disabled } @Serializable -class AutoAccept(val businessAddress: Boolean, val acceptIncognito: Boolean, val autoReply: MsgContent?) { - companion object { - fun cmdString(autoAccept: AutoAccept?): String { - if (autoAccept == null) return "off" - 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 +sealed class SubscriptionStatus { + @Serializable @SerialName("active") object Active: SubscriptionStatus() + @Serializable @SerialName("pending") object Pending: SubscriptionStatus() + @Serializable @SerialName("removed") class Removed(val subError: String): SubscriptionStatus() + @Serializable @SerialName("noSub") object NoSub: SubscriptionStatus() + + val statusString: String get() = + when (this) { + is Active -> generalGetString(MR.strings.server_connected) + is Pending -> generalGetString(MR.strings.server_connecting) + is Removed -> generalGetString(MR.strings.server_error) + is NoSub -> generalGetString(MR.strings.server_no_sub) } + + val statusExplanation: String get() = + when (this) { + is Active -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact) + is Pending -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages) + is Removed -> String.format(generalGetString(MR.strings.error_connecting_to_server_to_receive_messages), subError) + is NoSub -> generalGetString(MR.strings.not_connected_to_server_to_receive_messages_no_sub) + } +} + +interface SimplexAddress { + val connLinkContact: CreatedConnLink + val shortLinkDataSet: Boolean + val shortLinkLargeDataSet: Boolean + + val shouldBeUpgraded: Boolean get() = connLinkContact.connShortLink == null || !shortLinkDataSet || !shortLinkLargeDataSet +} + +@Serializable +data class UserContactLinkRec( + override val connLinkContact: CreatedConnLink, + override val shortLinkDataSet: Boolean, + override val shortLinkLargeDataSet: Boolean, + val addressSettings: AddressSettings +): SimplexAddress + +@Serializable +data class AddressSettings( + val businessAddress: Boolean, + val autoAccept: AutoAccept?, + val autoReply: MsgContent? +) + +@Serializable +data class AutoAccept(val acceptIncognito: Boolean) + +@Serializable +data class GroupLink( + val userContactLinkId: Long, + override val connLinkContact: CreatedConnLink, + override val shortLinkDataSet: Boolean, + override val shortLinkLargeDataSet: Boolean, + val groupLinkId: String, + val acceptMemberRole: GroupMemberRole +): SimplexAddress { + companion object { + val nullableStateSaver: Saver> = Saver( + save = { groupLink -> + if (groupLink == null) return@Saver null + + val conn = groupLink.connLinkContact + val connData = conn.connFullLink to conn.connShortLink + + listOf( + groupLink.userContactLinkId, + connData, + groupLink.shortLinkDataSet, + groupLink.shortLinkLargeDataSet, + groupLink.groupLinkId, + groupLink.acceptMemberRole.name + ) + }, + restore = { saved -> + val list = saved as? List<*> ?: return@Saver null + + val userContactLinkId = list.getOrNull(0) as? Long ?: return@Saver null + val connPair = list.getOrNull(1) as? Pair<*, *> ?: return@Saver null + val connFullLink = connPair.first as? String ?: return@Saver null + val connShortLink = connPair.second as? String + val shortLinkDataSet = list.getOrNull(2) as? Boolean ?: return@Saver null + val shortLinkLargeDataSet = list.getOrNull(3) as? Boolean ?: return@Saver null + val groupLinkId = list.getOrNull(4) as? String ?: return@Saver null + val roleName = list.getOrNull(5) as? String ?: return@Saver null + + GroupLink( + userContactLinkId = userContactLinkId, + connLinkContact = CreatedConnLink(connFullLink, connShortLink), + shortLinkDataSet = shortLinkDataSet, + shortLinkLargeDataSet = shortLinkLargeDataSet, + groupLinkId = groupLinkId, + acceptMemberRole = GroupMemberRole.valueOf(roleName) + ) + } + ) } } @@ -6410,6 +6962,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 @@ -6437,7 +6999,9 @@ sealed class ChatErrorType { is ChatStoreChanged -> "chatStoreChanged" is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" + is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" + is ConnReqMessageProhibited -> "connReqMessageProhibited" is ContactNotReady -> "contactNotReady" is ContactNotActive -> "contactNotActive" is ContactDisabled -> "contactDisabled" @@ -6453,6 +7017,7 @@ sealed class ChatErrorType { is GroupMemberNotActive -> "groupMemberNotActive" is GroupMemberUserRemoved -> "groupMemberUserRemoved" is GroupMemberNotFound -> "groupMemberNotFound" + is GroupHostMemberNotFound -> "groupHostMemberNotFound" is GroupCantResendInvitation -> "groupCantResendInvitation" is GroupInternal -> "groupInternal" is FileNotFound -> "fileNotFound" @@ -6461,7 +7026,6 @@ sealed class ChatErrorType { is FileCancelled -> "fileCancelled" is FileCancel -> "fileCancel" is FileAlreadyExists -> "fileAlreadyExists" - is FileRead -> "fileRead" is FileWrite -> "fileWrite $message" is FileSend -> "fileSend" is FileRcvChunk -> "fileRcvChunk" @@ -6515,7 +7079,9 @@ 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("connReqMessageProhibited") object ConnReqMessageProhibited: ChatErrorType() @Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType() @Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType() @Serializable @SerialName("contactDisabled") class ContactDisabled(val contact: Contact): ChatErrorType() @@ -6531,6 +7097,7 @@ sealed class ChatErrorType { @Serializable @SerialName("groupMemberNotActive") object GroupMemberNotActive: ChatErrorType() @Serializable @SerialName("groupMemberUserRemoved") object GroupMemberUserRemoved: ChatErrorType() @Serializable @SerialName("groupMemberNotFound") object GroupMemberNotFound: ChatErrorType() + @Serializable @SerialName("groupHostMemberNotFound") class GroupHostMemberNotFound(val groupId: Long): 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() @@ -6539,7 +7106,6 @@ sealed class ChatErrorType { @Serializable @SerialName("fileCancelled") class FileCancelled(val message: String): ChatErrorType() @Serializable @SerialName("fileCancel") class FileCancel(val fileId: Long, val message: String): ChatErrorType() @Serializable @SerialName("fileAlreadyExists") class FileAlreadyExists(val filePath: String): ChatErrorType() - @Serializable @SerialName("fileRead") class FileRead(val filePath: String, val message: String): ChatErrorType() @Serializable @SerialName("fileWrite") class FileWrite(val filePath: String, val message: String): ChatErrorType() @Serializable @SerialName("fileSend") class FileSend(val fileId: Long, val agentError: String): ChatErrorType() @Serializable @SerialName("fileRcvChunk") class FileRcvChunk(val message: String): ChatErrorType() @@ -6577,63 +7143,81 @@ 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 InvalidContactRequestEntity -> "invalidContactRequestEntity $contactRequestId" + is InvalidBusinessChatContactRequest -> "invalidBusinessChatContactRequest" + 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() @@ -6651,6 +7235,8 @@ sealed class StoreError { @Serializable @SerialName("userContactLinkNotFound") object UserContactLinkNotFound: StoreError() @Serializable @SerialName("contactRequestNotFound") class ContactRequestNotFound(val contactRequestId: Long): StoreError() @Serializable @SerialName("contactRequestNotFoundByName") class ContactRequestNotFoundByName(val contactName: String): StoreError() + @Serializable @SerialName("invalidContactRequestEntity") class InvalidContactRequestEntity(val contactRequestId: Long): StoreError() + @Serializable @SerialName("invalidBusinessChatContactRequest") object InvalidBusinessChatContactRequest: StoreError() @Serializable @SerialName("groupNotFound") class GroupNotFound(val groupId: Long): StoreError() @Serializable @SerialName("groupNotFoundByName") class GroupNotFoundByName(val groupName: String): StoreError() @Serializable @SerialName("groupMemberNameNotFound") class GroupMemberNameNotFound(val groupId: Long, val groupMemberName: String): StoreError() @@ -6661,6 +7247,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() @@ -6668,24 +7257,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() @@ -6693,7 +7285,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("largeMsg") object LargeMsg: 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 @@ -6722,7 +7324,7 @@ sealed class SQLiteError { sealed class AgentErrorType { val string: String get() = when (this) { is CMD -> "CMD ${cmdErr.string} $errContext" - is CONN -> "CONN ${connErr.string}" + is CONN -> "CONN ${connErr.string} $errContext" is SMP -> "SMP ${smpErr.string}" // is NTF -> "NTF ${ntfErr.string}" is XFTP -> "XFTP ${xftpErr.string}" @@ -6730,12 +7332,13 @@ sealed class AgentErrorType { is RCP -> "RCP ${rcpErr.string}" is BROKER -> "BROKER ${brokerErr.string}" is AGENT -> "AGENT ${agentErr.string}" + is NOTICE -> "NOTICE $server $expiresAt" is INTERNAL -> "INTERNAL $internalErr" is CRITICAL -> "CRITICAL $offerRestart $criticalErr" is INACTIVE -> "INACTIVE" } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType() - @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() + @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType, val errContext: String): 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() @@ -6743,6 +7346,7 @@ sealed class AgentErrorType { @Serializable @SerialName("RCP") class RCP(val rcpErr: RCErrorType): AgentErrorType() @Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType() @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() + @Serializable @SerialName("NOTICE") class NOTICE(val server: String, val preset: Boolean, val expiresAt: Instant?): AgentErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() @Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType() @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() @@ -6792,7 +7396,7 @@ sealed class BrokerErrorType { } @Serializable @SerialName("RESPONSE") class RESPONSE(val smpErr: String): BrokerErrorType() @Serializable @SerialName("UNEXPECTED") object UNEXPECTED: BrokerErrorType() - @Serializable @SerialName("NETWORK") object NETWORK: BrokerErrorType() + @Serializable @SerialName("NETWORK") class NETWORK(val networkError: NetworkError): BrokerErrorType() @Serializable @SerialName("HOST") object HOST: BrokerErrorType() @Serializable @SerialName("TRANSPORT") class TRANSPORT(val transportErr: SMPTransportError): BrokerErrorType() @Serializable @SerialName("TIMEOUT") object TIMEOUT: BrokerErrorType() @@ -6879,6 +7483,16 @@ sealed class ProtocolCommandError { @Serializable @SerialName("NO_QUEUE") object NO_QUEUE: ProtocolCommandError() } +@Serializable +sealed class NetworkError { + @Serializable @SerialName("connectError") class ConnectError(val connectError: String): NetworkError() + @Serializable @SerialName("tLSError") class TLSError(val tlsError: String): NetworkError() + @Serializable @SerialName("unknownCAError") object UnknownCAError: NetworkError() + @Serializable @SerialName("failedError") object FailedError: NetworkError() + @Serializable @SerialName("timeoutError") object TimeoutError: NetworkError() + @Serializable @SerialName("subscribeError") class SubscribeError(val subscribeError: String): NetworkError() +} + @Serializable sealed class SMPTransportError { val string: String get() = when (this) { @@ -7460,3 +8074,34 @@ enum class MsgType { @SerialName("quota") QUOTA } + +fun showClientNoticeAlert(server: String, preset: Boolean, expiresAt: Instant?) { + var message = "Server: $server.\nConditions of use violation notice received from ${if (preset) "preset" else "this"} server.\nNo ID shared, see How it works." + if (expiresAt != null) { + val tz = TimeZone.currentSystemDefault() + val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) + message += "\n\nNew addresses can be created after ${expiresAt.toLocalDateTime(tz).toJavaLocalDateTime().format(formatter)}." + } + AlertManager.shared.showAlertDialogButtonsColumn(title = "Not allowed", text = AnnotatedString(message)) { + val uriHandler = LocalUriHandler.current + Column { + SectionItemView({ AlertManager.shared.hideAlert() }) { + Text(generalGetString(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + if (preset) { + SectionItemView({ + AlertManager.shared.hideAlert() + uriHandler.openUriCatching(defaultConditionsLink) + }) { + Text(generalGetString(MR.strings.operator_conditions_of_use), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + SectionItemView({ + AlertManager.shared.hideAlert() + uriHandler.openUriCatching(contentModerationPostLink) + }) { + Text(generalGetString(MR.strings.how_it_works), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } +} 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 60a65eaac6..7a96bd99d2 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 @@ -28,9 +28,11 @@ val appVersionInfo: Pair = if (appPlatform == AppPlatform.ANDROID) 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() + if (size > capacity) removeFirstOrNull() return super.add(element) } } 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 a2061674ec..41c45b0dcb 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 @@ -22,12 +22,13 @@ external fun pipeStdOutToSocket(socketName: String) : Int typealias ChatCtrl = Long external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array external fun chatCloseStore(ctrl: ChatCtrl): String -external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String -external fun chatSendRemoteCmd(ctrl: ChatCtrl, rhId: Int, msg: String): String +external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String +external fun chatSendRemoteCmdRetry(ctrl: ChatCtrl, rhId: Int, msg: String, retryNum: Int): String external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String +external fun chatParseUri(str: String, safe: Int): String external fun chatPasswordHash(pwd: String, salt: String): String external fun chatValidName(name: String): String external fun chatJsonLength(str: String): Int @@ -57,6 +58,7 @@ fun initChatControllerOnStart() { } suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred = { CompletableDeferred(true) }) { + Log.d(TAG, "initChatController") try { if (chatModel.ctrlInitInProgress.value) return chatModel.ctrlInitInProgress.value = true @@ -65,7 +67,11 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } 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) } @@ -77,7 +83,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,7 +95,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat val ctrl = if (res is DBMigrationResult.OK) { migrated[1] as Long } else null - chatController.ctrl = ctrl + chatController.setChatCtrl(ctrl) chatModel.chatDbEncrypted.value = dbKey != "" chatModel.chatDbStatus.value = res if (res != DBMigrationResult.OK) { @@ -199,7 +209,7 @@ fun chatInitControllerRemovingDatabases() { }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } val ctrl = migrated[1] as Long - chatController.ctrl = ctrl + chatController.setChatCtrl(ctrl) // We need only controller, not databases File(dbPath + "_chat.db").delete() File(dbPath + "_agent.db").delete() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt index cfb3457599..d122530abd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt @@ -1,7 +1,15 @@ package chat.simplex.common.platform +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.layout.ContentScale import boofcv.struct.image.GrayU8 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.io.InputStream import java.net.URI @@ -17,10 +25,30 @@ expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOut expect fun GrayU8.toImageBitmap(): ImageBitmap expect fun ImageBitmap.hasAlpha(): Boolean -expect fun ImageBitmap.addLogo(): ImageBitmap +expect fun ImageBitmap.addLogo(size: Float): ImageBitmap expect fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap expect fun isImage(uri: URI): Boolean expect fun isAnimImage(uri: URI, drawable: Any?): Boolean expect fun loadImageBitmap(inputStream: InputStream): ImageBitmap + +@Composable +fun Base64AsyncImage( + base64ImageString: String, + contentDescription: String?, + contentScale: ContentScale, + modifier: Modifier = Modifier +) { + val imageBitmap by produceState(initialValue = null, base64ImageString) { + value = withContext(Dispatchers.IO) { base64ToBitmap(base64ImageString) } + } + imageBitmap?.let { + Image( + bitmap = it, + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier + ) + } +} 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 51d26f8ff2..d906ef7baf 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 @@ -10,8 +10,7 @@ import chat.simplex.res.MR import kotlinx.coroutines.delay enum class NotificationAction { - ACCEPT_CONTACT_REQUEST, - ACCEPT_CONTACT_REQUEST_INCOGNITO + ACCEPT_CONTACT_REQUEST } lateinit var ntfManager: NtfManager @@ -31,15 +30,14 @@ abstract class NtfManager { msgText = generalGetString(MR.strings.notification_new_contact_request), image = cInfo.image, listOf( - NotificationAction.ACCEPT_CONTACT_REQUEST to { acceptContactRequestAction(user.userId, incognito = false, cInfo.id) }, - NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO to { acceptContactRequestAction(user.userId, incognito = true, cInfo.id) } + NotificationAction.ACCEPT_CONTACT_REQUEST to { acceptContactRequestAction(user.userId, incognito = false, cInfo.id) } ) ) fun notifyMessageReceived(rhId: Long?, user: UserLike, cInfo: ChatInfo, cItem: ChatItem) { if ( cItem.showNotification && - cInfo.ntfsEnabled && + cInfo.ntfsEnabled(cItem) && ( allowedToShowNotification() || chatModel.chatId.value != cInfo.id || @@ -51,14 +49,9 @@ abstract class NtfManager { fun acceptContactRequestAction(userId: Long?, incognito: Boolean, chatId: ChatId) { val isCurrentUser = ChatModel.currentUser.value?.userId == userId - val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) { - (ChatModel.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return - } else { - null - } val apiId = chatId.replace("<@", "").toLongOrNull() ?: return // TODO include remote host in notification - acceptContactRequest(null, incognito, apiId, cInfo, isCurrentUser, ChatModel) + acceptContactRequest(null, incognito, apiId, isCurrentUser, ChatModel) cancelNotificationsForChat(chatId) } @@ -73,7 +66,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) + if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(secondaryChatsCtx = null, rhId = null, cInfo) } } @@ -134,7 +127,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/PlatformTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt index 1dff386684..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,14 +12,15 @@ import java.net.URI expect fun PlatformTextField( composeState: MutableState, sendMsgEnabled: Boolean, + disabledText: String?, sendMsgButtonDisabled: Boolean, textStyle: MutableState, showDeleteTextButton: MutableState, - userIsObserver: Boolean, placeholder: String, showVoiceButton: Boolean, - onMessageChange: (String) -> Unit, + 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/ScrollableColumn.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt index e6d4514875..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 @@ -45,6 +45,8 @@ expect fun LazyColumnWithScrollBarNoAppBar( additionalBarOffset: State? = null, additionalTopBar: State = remember { mutableStateOf(false) }, chatBottomBar: State = remember { mutableStateOf(true) }, + maxHeight: State? = null, + containerAlignment: Alignment = Alignment.TopStart, content: LazyListScope.() -> Unit ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt new file mode 100644 index 0000000000..0644b5dd30 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt @@ -0,0 +1,3 @@ +package chat.simplex.common.platform + +expect fun getWakeLock(timeout: Long): (() -> Unit) 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 01e19ea478..df9af7fbf6 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 @@ -804,7 +804,8 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { LocalAppColors provides rememberedAppColors, LocalAppWallpaper provides rememberedWallpaper, LocalDensity provides density, - content = content) + content = content + ) } ) } 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 67fae65897..1bfe88ecf7 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 @@ -10,8 +10,10 @@ 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 @@ -45,16 +47,16 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState Unit) { val scope = rememberCoroutineScope() @@ -46,6 +52,7 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { .padding(top = 20.dp) ) { val displayName = rememberSaveable { mutableStateOf("") } + val shortDescr = rememberSaveable { mutableStateOf("") } val focusRequester = remember { FocusRequester() } ColumnWithScrollBar { @@ -66,16 +73,34 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { } } ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester) + + Spacer(Modifier.height(DEFAULT_PADDING)) + + Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(MR.strings.short_descr), + fontSize = 16.sp + ) + Spacer(Modifier.height(20.dp)) + if (!bioFitsLimit(shortDescr.value)) { + IconButton( + onClick = { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.bio_too_large)) }, + Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } + ProfileNameField(shortDescr, "", isValid = { bioFitsLimit(it) }) } SettingsActionItem( painterResource(MR.images.ic_check), stringResource(MR.strings.create_another_profile_button), - disabled = !canCreateProfile(displayName.value), + disabled = !canCreateProfile(displayName.value) || !bioFitsLimit(shortDescr.value), textColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary, click = { if (chatModel.localUserCreated.value == true) { - createProfileInProfiles(chatModel, displayName.value, close) + createProfileInProfiles(chatModel, displayName.value, shortDescr.value, close) } else { createProfileInNoProfileSetup(displayName.value, close) } @@ -162,7 +187,7 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { withBGApi { - val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withBGApi + val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null, null)) ?: return@withBGApi if (!chatModel.connectedToRemote()) { chatModel.localUserCreated.value = true } @@ -173,11 +198,11 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { } } -fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { +fun createProfileInProfiles(chatModel: ChatModel, displayName: String, shortDescr: String, close: () -> Unit) { withBGApi { val rhId = chatModel.remoteHostId() val user = chatModel.controller.apiCreateActiveUser( - rhId, Profile(displayName.trim(), "", null) + rhId, Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, null) ) ?: return@withBGApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { @@ -196,7 +221,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) { withBGApi { chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser( - null, Profile(displayName.trim(), "", null) + null, Profile(displayName.trim(), "", null, null) ) ?: return@withBGApi chatModel.localUserCreated.value = true val onboardingStage = chatModel.controller.appPrefs.onboardingStage 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 ae020ea3a1..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,13 +10,13 @@ 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.ChatItemsStatusesUpdated && - msg.chatItems.any { + 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 } ) { 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 7f5ed7b2a5..061ea71016 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 @@ -19,7 +19,6 @@ 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.graphics.painter.Painter import androidx.compose.ui.platform.* @@ -37,17 +36,16 @@ 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.model.ChatModel.withChats 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.chat.group.ProgressIndicator +import chat.simplex.common.views.chat.group.ChatTTLOption +import chat.simplex.common.views.chat.item.MarkdownText 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 @@ -57,6 +55,7 @@ import java.io.File @Composable fun ChatInfoView( + chatsCtx: ChatModel.ChatsContext, chatModel: ChatModel, contact: Contact, connectionStats: ConnectionStats?, @@ -73,9 +72,6 @@ fun ChatInfoView( val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && currentUser != null) { - val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) { - mutableStateOf(chatModel.contactNetworkStatus(contact)) - } 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) } @@ -99,10 +95,9 @@ fun ChatInfoView( val previousChatTTL = chatItemTTL.value chatItemTTL.value = it - setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) + setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) }, connStats = connStats, - contactNetworkStatus.value, customUserProfile, localAlias, connectionCode, @@ -126,8 +121,8 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - withChats { - updateContactConnectionStats(chatRh, contact, cStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats) } } close.invoke() @@ -140,8 +135,8 @@ fun ChatInfoView( val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - withChats { - updateContactConnectionStats(chatRh, contact, cStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats) } } } @@ -171,8 +166,8 @@ fun ChatInfoView( verify = { code -> chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r -> val (verified, existingCode) = r - withChats { - updateContact( + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact( chatRh, ct.copy( activeConn = ct.activeConn?.copy( @@ -200,8 +195,8 @@ suspend fun syncContactConnection(rhId: Long?, contact: Contact, connectionStats val cStats = chatModel.controller.apiSyncContactRatchet(rhId, contact.contactId, force = force) connectionStats.value = cStats if (cStats != null) { - withChats { - updateContactConnectionStats(rhId, contact, cStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(rhId, contact, cStats) } } } @@ -475,14 +470,14 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDe val chatRh = chat.remoteHostId val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode) if (ct != null) { - withChats { + withContext(Dispatchers.Main) { when (chatDeleteMode) { is ChatDeleteMode.Full -> - removeChat(chatRh, chatInfo.id) + chatModel.chatsContext.removeChat(chatRh, chatInfo.id) is ChatDeleteMode.Entity -> - updateContact(chatRh, ct) + chatModel.chatsContext.updateContact(chatRh, ct) is ChatDeleteMode.Messages -> - clearChat(chatRh, ChatInfo.Direct(ct)) + chatModel.chatsContext.clearChat(chatRh, ChatInfo.Direct(ct)) } } if (chatModel.chatId.value == chatInfo.id) { @@ -525,7 +520,6 @@ fun ChatInfoLayout( chatItemTTL: MutableState, setChatItemTTL: (ChatItemTTL?) -> Unit, connStats: MutableState, - contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, localAlias: String, connectionCode: String?, @@ -618,7 +612,10 @@ fun ChatInfoLayout( } SectionDividerSpaced(maxBottomPadding = false) - ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems) + SectionView { + ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems) + SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) + } SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) val conn = contact.activeConn @@ -641,13 +638,16 @@ fun ChatInfoLayout( if (contact.ready && contact.active) { SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) { - SectionItemView({ - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.network_status), - contactNetworkStatus.statusExplanation - ) - }) { - NetworkStatusRow(contactNetworkStatus) + val chatSubStatus = chatModel.chatSubStatus.value + if (chatSubStatus != null) { + SectionItemView({ + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.network_status), + chatSubStatus.statusExplanation + ) + }) { + SubStatusRow(chatSubStatus) + } } if (cStats != null) { SwitchAddressButton( @@ -705,15 +705,16 @@ fun ChatInfoLayout( @Composable fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { Column( - Modifier.padding(horizontal = 8.dp), + Modifier.padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally ) { ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) + val displayName = contact.profile.displayName.trim() val text = buildAnnotatedString { if (contact.verified) { appendInlineContent(id = "shieldIcon") } - append(contact.profile.displayName) + append(displayName) } val inlineContent: Map = mapOf( "shieldIcon" to InlineTextContent( @@ -723,10 +724,11 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { } ) val clipboard = LocalClipboardManager.current - val copyNameToClipboard = { - clipboard.setText(AnnotatedString(contact.profile.displayName)) + val copyNameToClipboard = fun (name: String) { + clipboard.setText(AnnotatedString(name)) showToast(generalGetString(MR.strings.copied)) } + val copyDisplayName = { copyNameToClipboard(displayName) } Text( text, inlineContent = inlineContent, @@ -734,18 +736,39 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { textAlign = TextAlign.Center, maxLines = 3, overflow = TextOverflow.Ellipsis, - modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) + modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) + ) + ChatInfoDescription(cInfo, displayName, copyNameToClipboard) + } +} + +@Composable +fun ChatInfoDescription(c: NamedChat, displayName: String, copyNameToClipboard: (String) -> Unit) { + val fullName = c.fullName.trim() + if (fullName != "" && fullName != displayName && fullName != c.displayName.trim()) { + val copyFullName = { copyNameToClipboard(fullName) } + Text( + fullName, style = MaterialTheme.typography.h2, + color = MaterialTheme.colors.onBackground, + textAlign = TextAlign.Center, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF).combinedClickable(onClick = copyFullName, onLongClick = copyFullName).onRightClick(copyFullName) + ) + } + val descr = c.shortDescr?.trim() + if (descr != null && descr != "") { + MarkdownText( + descr, + parseToMarkdown(descr), + toggleSecrets = true, + style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), + maxLines = 4, + overflow = TextOverflow.Ellipsis, + uriHandler = LocalUriHandler.current, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF), + linkMode = chatModel.simplexLinkMode.value ) - if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) { - Text( - cInfo.fullName, style = MaterialTheme.typography.h2, - color = MaterialTheme.colors.onBackground, - textAlign = TextAlign.Center, - maxLines = 4, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) - ) - } } } @@ -837,17 +860,18 @@ fun MuteButton( chat: Chat, contact: Contact ) { - val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + 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 = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), - title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), + icon = painterResource(nextNtfMode.icon), + title = stringResource(nextNtfMode.text(false)), disabled = disabled, disabledLook = disabled, onClick = { - toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled.value, chatModel, ntfsEnabled) + toggleNotifications(chat.remoteHostId, chat.chatInfo, nextNtfMode, chatModel, enableNtfs) } ) } @@ -932,7 +956,7 @@ fun CallButton( } } } } - contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } } + contact.sendMsgToConnect -> { { showCantCallContactSendMessageAlert() } } !contact.active -> { { showCantCallContactDeletedAlert() } } !contact.ready -> { { showCantCallContactConnectingAlert() } } needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } } @@ -1037,7 +1061,7 @@ fun InfoViewActionButton( } @Composable -private fun NetworkStatusRow(networkStatus: NetworkStatus) { +fun SubStatusRow(subStatus: SubscriptionStatus) { Row( Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween, @@ -1060,25 +1084,24 @@ private fun NetworkStatusRow(networkStatus: NetworkStatus) { horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - networkStatus.statusString, + subStatus.statusString, color = MaterialTheme.colors.secondary ) - ServerImage(networkStatus) + ServerImage(subStatus) } } } @Composable -private fun ServerImage(networkStatus: NetworkStatus) { +private fun ServerImage(subStatus: SubscriptionStatus) { Box(Modifier.size(18.dp)) { - when (networkStatus) { - is NetworkStatus.Connected -> + when (subStatus) { + is SubscriptionStatus.Active -> Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = Color.Green) - is NetworkStatus.Disconnected -> + is SubscriptionStatus.Pending -> Icon(painterResource(MR.images.ic_pending_filled), stringResource(MR.strings.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary) - is NetworkStatus.Error -> + is SubscriptionStatus.Removed, SubscriptionStatus.NoSub -> Icon(painterResource(MR.images.ic_error_filled), stringResource(MR.strings.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary) - else -> Icon(painterResource(MR.images.ic_circle), stringResource(MR.strings.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary) } } } @@ -1269,11 +1292,11 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c wallpaperFilesToDelete.forEach(::removeWallpaperFile) if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { - withChats { + withContext(Dispatchers.Main) { if (chat.chatInfo is ChatInfo.Direct) { - updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) + chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) } else if (chat.chatInfo is ChatInfo.Group) { - updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) } } } @@ -1282,8 +1305,8 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { val chatRh = chat.remoteHostId chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { - withChats { - updateContact(chatRh, it) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(chatRh, it) } } } @@ -1333,6 +1356,7 @@ fun queueInfoText(info: Pair): String { } fun setChatTTLAlert( + chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, selectedChatTTL: MutableState, @@ -1352,7 +1376,7 @@ fun setChatTTLAlert( } 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(rhId, chatInfo, selectedChatTTL, progressIndicator, previousChatTTL) }, + onConfirm = { setChatTTL(chatsCtx, rhId, chatInfo, selectedChatTTL, progressIndicator, previousChatTTL) }, onDismiss = { selectedChatTTL.value = previousChatTTL }, onDismissRequest = { selectedChatTTL.value = previousChatTTL }, destructive = true, @@ -1360,6 +1384,7 @@ fun setChatTTLAlert( } private fun setChatTTL( + chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, chatTTL: MutableState, @@ -1370,34 +1395,35 @@ private fun setChatTTL( withBGApi { try { chatModel.controller.setChatTTL(rhId, chatInfo.chatType, chatInfo.apiId, chatTTL.value) - afterSetChatTTL(rhId, chatInfo, progressIndicator) + afterSetChatTTL(chatsCtx, rhId, chatInfo, progressIndicator) } catch (e: Exception) { chatTTL.value = previousChatTTL - afterSetChatTTL(rhId, chatInfo, progressIndicator) + afterSetChatTTL(chatsCtx, rhId, chatInfo, progressIndicator) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_message_deletion), e.stackTraceToString()) } } } -private suspend fun afterSetChatTTL(rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState) { +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 + val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, scope = null, contentTag = 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 - withChats { - val oldChat = getChat(chat.id) + withContext(Dispatchers.Main) { + val oldChat = chatModel.chatsContext.getChat(chat.id) if (oldChat != null) { - replaceChat(oldChat.remoteHostId, oldChat.id, oldChat.copy(chatItems = emptyList())) + 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, - contentTag = null, - pagination = pagination + pagination = pagination, + openAroundItemId = null ) } catch (e: Exception) { Log.e(TAG, "apiGetChat error: ${e.stackTraceToString()}") @@ -1426,7 +1452,6 @@ fun PreviewChatInfoLayout() { connectionCode = "123", developerTools = false, connStats = remember { mutableStateOf(null) }, - contactNetworkStatus = NetworkStatus.Connected(), onLocalAliasChanged = {}, customUserProfile = null, openPreferences = {}, 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 e62235bd7c..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 @@ -42,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 = 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, @@ -80,7 +82,12 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools .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)) { @@ -202,7 +209,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools SectionItemView( click = { withBGApi { - openChat(chatRh, forwardedFromItem.chatInfo) + openChat(secondaryChatsCtx = null, chatRh, forwardedFromItem.chatInfo) ModalManager.end.closeModals() } }, 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 index 3613ceaa7c..ed40150cb1 100644 --- 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 @@ -2,70 +2,74 @@ package chat.simplex.common.views.chat import androidx.compose.runtime.snapshots.SnapshotStateList import chat.simplex.common.model.* -import chat.simplex.common.model.ChatModel.withChats 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, - contentTag: MsgContentTag?, + itemId: Long ): ChatItem? = coroutineScope { - val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null + val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null chat.chatItems.firstOrNull() } suspend fun apiLoadMessages( + chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatType: ChatType, apiId: Long, - contentTag: MsgContentTag?, pagination: ChatPagination, search: String = "", + openAroundItemId: Long? = null, visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } ) = coroutineScope { - val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentTag, pagination, search) ?: return@coroutineScope + val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), 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 - if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last) + /** 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(chat, navInfo, contentTag, pagination, visibleItemIndexesNonReversed) + processLoadedChat(chatsCtx, chat, navInfo, pagination, openAroundItemId, visibleItemIndexesNonReversed) } suspend fun processLoadedChat( + chatsCtx: ChatModel.ChatsContext, chat: Chat, navInfo: NavigationInfo, - contentTag: MsgContentTag?, pagination: ChatPagination, + openAroundItemId: Long?, visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } ) { - val chatState = chatModel.chatStateForContent(contentTag) + val chatState = chatsCtx.chatState val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState - val oldItems = chatModel.chatItemsForContent(contentTag).value + 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 (contentTag == null) { + if (chatsCtx.secondaryContextFilter == null) { // update main chats, not content tagged - withChats { - if (getChat(chat.id) == null) { - addChat(chat) + withContext(Dispatchers.Main) { + val oldChat = chatModel.chatsContext.getChat(chat.id) + if (oldChat == null) { + chatModel.chatsContext.addChat(chat) } else { - updateChatInfo(chat.remoteHostId, chat.chatInfo) - updateChatStats(chat.remoteHostId, chat.id, chat.chatStats) + 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)) } } } - withChats(contentTag) { - chatItemStatuses.clear() - chatItems.replaceAll(chat.chatItems) - chatModel.chatId.value = chat.chatInfo.id + withContext(Dispatchers.Main) { + chatsCtx.chatItems.replaceAll(chat.chatItems) + chatModel.chatId.value = chat.id splits.value = newSplits if (chat.chatItems.isNotEmpty()) { unreadAfterItemId.value = chat.chatItems.last().id @@ -87,8 +91,8 @@ suspend fun processLoadedChat( ) val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0) newItems.addAll(insertAt, chat.chatItems) - withChats(contentTag) { - chatItems.replaceAll(newItems) + withContext(Dispatchers.Main) { + chatsCtx.chatItems.replaceAll(newItems) splits.value = newSplits chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems) } @@ -106,8 +110,8 @@ suspend fun processLoadedChat( val indexToAdd = min(indexInCurrentItems + 1, newItems.size) val indexToAddIsLast = indexToAdd == newItems.size newItems.addAll(indexToAdd, chat.chatItems) - withChats(contentTag) { - chatItems.replaceAll(newItems) + 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 @@ -117,27 +121,43 @@ suspend fun processLoadedChat( } } is ChatPagination.Around -> { - newItems.addAll(oldItems) - val newSplits = removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed) - // currently, items will always be added on top, which is index 0 - newItems.addAll(0, chat.chatItems) - withChats(contentTag) { - chatItems.replaceAll(newItems) - splits.value = listOf(chat.chatItems.last().id) + newSplits + 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 - // no need to set it, count will be wrong - // unreadAfterNewestLoaded.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) - withChats(contentTag) { - chatItems.replaceAll(newItems) + withContext(Dispatchers.Main) { + chatsCtx.chatItems.replaceAll(newItems) + chatState.splits.value = newSplits unreadAfterNewestLoaded.value = 0 } } @@ -223,7 +243,15 @@ private fun removeDuplicatesAndModifySplitsOnAfterPagination( val indexInSplitRanges = splits.value.indexOf(paginationChatItemId) // Currently, it should always load from split range val loadingFromSplitRange = indexInSplitRanges != -1 - val splitsToMerge = if (loadingFromSplitRange && indexInSplitRanges + 1 <= splits.value.size) ArrayList(splits.value.subList(indexInSplitRanges + 1, splits.value.size)) else ArrayList() + 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) { @@ -242,8 +270,8 @@ private fun removeDuplicatesAndModifySplitsOnAfterPagination( } var newSplits: List = emptyList() if (firstItemIdBelowAllSplits != null) { - // no splits anymore, all were merged with bottom items - newSplits = emptyList() + // no splits below anymore, all were merged with bottom items + newSplits = topSplits } else { if (splitsToRemove.isNotEmpty()) { val new = ArrayList(splits.value) @@ -297,7 +325,8 @@ private fun removeDuplicatesAndUpperSplits( 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() + // note: removeLast() produce NoSuchMethodError on Android but removeLastOrNull() works + idsToTrim.removeLastOrNull() } val allItemsToDelete = idsToTrim.flatten() if (allItemsToDelete.isNotEmpty()) { @@ -306,6 +335,31 @@ private fun removeDuplicatesAndUpperSplits( 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 @@ -326,3 +380,141 @@ 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 index d318cf05fd..d98c041478 100644 --- 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 @@ -237,24 +237,8 @@ data class ActiveChatState ( unreadAfter.value = 0 unreadAfterNewestLoaded.value = 0 } -} -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 -} - -fun recalculateChatStatePositions(chatState: ActiveChatState) = object: ChatItemsChangesListener { - override fun read(itemIds: Set?, newItems: List) { - val (_, unreadAfterItemId, _, unreadTotal, unreadAfter) = chatState + fun itemsRead(itemIds: Set?, newItems: List) { if (itemIds == null) { // special case when the whole chat became read unreadTotal.value = 0 @@ -287,14 +271,15 @@ fun recalculateChatStatePositions(chatState: ActiveChatState) = object: ChatItem unreadTotal.value = newUnreadTotal unreadAfter.value = newUnreadAfter } - override fun added(item: Pair, index: Int) { + + fun itemAdded(item: Pair) { if (item.second) { - chatState.unreadAfter.value++ - chatState.unreadTotal.value++ + unreadAfter.value++ + unreadTotal.value++ } } - override fun removed(itemIds: List>, newItems: List) { - val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter) = chatState + + fun itemsRemoved(itemIds: List>, newItems: List) { val newSplits = ArrayList() for (split in splits.value) { val index = itemIds.indexOfFirst { it.first == split } @@ -343,7 +328,19 @@ fun recalculateChatStatePositions(chatState: ActiveChatState) = object: ChatItem totalAfter.value -= itemIds.size } } - override fun cleared() { chatState.clear() } +} + +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 */ 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 7713a2399f..26daee363f 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 @@ -13,6 +13,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* 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 @@ -31,8 +32,6 @@ 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.model.ChatModel.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.group.* @@ -43,7 +42,9 @@ import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.common.platform.AudioPlayer import chat.simplex.common.views.newchat.ContactConnectionInfoView +import chat.simplex.common.views.newchat.alertProfileImageSize import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.datetime.* @@ -54,35 +55,79 @@ import kotlin.math.* @Stable data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?) +@Composable +fun ConnectInProgressView(s: String) { + Surface(color = MaterialTheme.colors.background) { + Divider() + Row( + Modifier + .height(60.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Row( + Modifier + .padding(start = 10.dp) + .fillMaxWidth() + .weight(1F), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ComposeProgressIndicator() + Text(s) + } + + IconButton(onClick = { connectProgressManager.cancelConnectProgress() }) { + Icon( + painterResource(MR.images.ic_close), + contentDescription = stringResource(MR.strings.cancel_verb), + tint = MaterialTheme.colors.primary, + modifier = Modifier.padding(10.dp) + ) + } + } + } +} + @Composable // 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, - reportsView: Boolean, scrollToItemId: MutableState = remember { mutableStateOf(null) }, onComposed: suspend (chatId: String) -> Unit ) { val showSearch = rememberSaveable { mutableStateOf(false) } + val chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } // They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..." val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } - val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } } - val activeChatStats = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats } } + val chatRh = remoteHostId.value + val activeChat = remember { derivedStateOf { + var chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } + val chatInfo = chat?.chatInfo + if ( + chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext + && chat != null + && chatInfo is ChatInfo.Group + ) { + val scopeInfo = chatsCtx.secondaryContextFilter.groupScopeInfo + chat = chat.copy(chatInfo = chatInfo.copy(groupChatScope = scopeInfo)) + } + chat + } } val user = chatModel.currentUser.value - val chatInfo = activeChatInfo.value - if (chatInfo == null || user == null) { + val chatInfo = activeChat.value?.chatInfo + if (chat == null || chatInfo == null || user == null) { LaunchedEffect(Unit) { chatModel.chatId.value = null + chatModel.chatAgentConnId.value = null + chatModel.chatSubStatus.value = null ModalManager.end.closeModals() } } else { - val groupReports = remember { derivedStateOf { - val reportsCount = if (activeChatInfo.value is ChatInfo.Group) activeChatStats.value?.reportsCount ?: 0 else 0 - GroupReports(reportsCount, reportsView) } - } - val reversedChatItems = remember { derivedStateOf { chatModel.chatItemsForContent(groupReports.value.contentTag).value.asReversed() } } val searchText = rememberSaveable { mutableStateOf("") } - val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() + val useLinkPreviews = true val composeState = rememberSaveable(saver = ComposeState.saver()) { val draft = chatModel.draft.value val sharedContent = chatModel.sharedContent.value @@ -98,6 +143,14 @@ fun ChatView( val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() val selectedChatItems = rememberSaveable { mutableStateOf(null as Set?) } + val showCommandsMenu = rememberSaveable { mutableStateOf(false) } + if (appPlatform.isAndroid) { + DisposableEffect(Unit) { + onDispose { + connectProgressManager.cancelConnectProgress() + } + } + } 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 @@ -106,28 +159,63 @@ fun ChatView( .distinctUntilChanged() .filterNotNull() .collect { chatId -> - if (!groupReports.value.reportsView) { + if (appPlatform.isAndroid) { + connectProgressManager.cancelConnectProgress() + } + if (chatsCtx.secondaryContextFilter == null) { markUnreadChatAsRead(chatId) + chatModel.groupMembers.value = emptyList() + chatModel.groupMembersIndexes.value = emptyMap() + chatModel.membersLoaded.value = false } showSearch.value = false searchText.value = "" selectedChatItems.value = null + if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) { + 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, chat.chatInfo.contact, contactStats) + chatModel.chatAgentConnId.value = chat.chatInfo.contact.activeConn.agentConnId + chatModel.chatSubStatus.value = contactStats.subStatus + } + } + } + } else { + withContext(Dispatchers.Main) { + chatModel.chatAgentConnId.value = null + chatModel.chatSubStatus.value = null + } + } } } } + if (chatsCtx.secondaryContextFilter == null && chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberPending) { + LaunchedEffect(Unit) { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) + val supportChatInfo = ChatInfo.Group(chatInfo.groupInfo, groupChatScope = scopeInfo) + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + } 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.chatsForContent(if (reportsView) MsgContentTag.Report else null).value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0 + chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0 } } val clipboard = LocalClipboardManager.current CompositionLocalProvider( LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false), - LocalContentTag provides groupReports.value.contentTag ) { when (chatInfo) { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { @@ -135,49 +223,54 @@ fun ChatView( 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 -> - if (searchText.value == value) return@onSearchValueChanged - val c = chatModel.getChat(chatInfo.id) ?: return@onSearchValueChanged - if (chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged + 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.secondaryContextFilter == null + val c = chatModel.getChat(chatInfo.id) + if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged withBGApi { - apiFindMessages(c, value, groupReports.value.toContentTag()) + apiFindMessages(chatsCtx, c, value) searchText.value = value } } ChatLayout( + chatsCtx = chatsCtx, remoteHostId = remoteHostId, - chatInfo = activeChatInfo, - reversedChatItems = reversedChatItems, + chat = activeChat, unreadCount, composeState, - composeView = { + 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 - ) { + val connectInProgressText = connectProgressManager.showConnectProgress + if (appPlatform.isAndroid && connectInProgressText != null) { + ConnectInProgressView(connectInProgressText) + } + val connectingText = connectingText(chatInfo) + if (connectingText != null) { Text( - generalGetString(MR.strings.contact_connection_pending), + connectingText, 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() } } + rhId = remoteHostId.value, chatModel, chatsCtx, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, showCommandsMenu, attachmentOption, + showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }, + focusRequester = focusRequester ) } } else { - SelectedItemsBottomToolbar( - reversedChatItems = reversedChatItems, + SelectedItemsButtonsToolbar( + chatsCtx = chatsCtx, selectedChatItems = selectedChatItems, chatInfo = chatInfo, deleteItems = { canDeleteForAll -> @@ -200,6 +293,7 @@ fun ChatView( ) } }, + archiveItems = { archiveItems(chatRh, chatInfo, selectedChatItems) }, moderateItems = { if (chatInfo is ChatInfo.Group) { val itemIds = selectedChatItems.value @@ -222,6 +316,7 @@ fun ChatView( rh = chatRh, fromChatType = chatInfo.chatType, fromChatId = chatInfo.apiId, + fromScope = chatInfo.groupChatScope(), chatItemIds = chatItemIds ) @@ -238,7 +333,6 @@ fun ChatView( ) } }, - groupReports, scrollToItemId, attachmentOption, attachmentBottomSheetState, @@ -252,6 +346,7 @@ fun ChatView( chatModel.chatId.value = null chatModel.groupMembers.value = emptyList() chatModel.groupMembersIndexes.value = emptyMap() + chatModel.membersLoaded.value = false }, info = { if (ModalManager.end.hasModalsOpen()) { @@ -264,7 +359,7 @@ fun ChatView( // 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 + var preloadedLink: GroupLink? = null if (chatInfo is ChatInfo.Direct) { preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second @@ -274,48 +369,92 @@ fun ChatView( } if (!isActive) return@launch - ModalManager.end.showModalCloseable(true) { close -> - 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 + 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 { activeChat }.value?.chatInfo + if (chatInfo is ChatInfo.Direct) { + var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } + var code: String? by remember { mutableStateOf(preloadedCode) } + KeyChangeEffect(chatInfo.id) { + 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: GroupLink? 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, selectedItems, appBar, scrollToItemId, { + link = it + preloadedLink = it + }, close, { showSearch.value = true }) + } else { + LaunchedEffect(Unit) { + close() + } } - ChatInfoView(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(chatModel, chatRh, chatInfo.id, link?.first, link?.second, scrollToItemId, { - link = it - preloadedLink = it - }, close, { showSearch.value = true }) - } else { LaunchedEffect(Unit) { - close() + snapshotFlow { activeChat.value?.id } + .drop(1) + .collect { + appBar.value = null + selectedItems.value = null + } } } } } }, - showGroupReports = { - val info = activeChatInfo.value ?: return@ChatLayout + showReports = { + val cInfo = activeChat.value?.chatInfo ?: return@ChatLayout if (ModalManager.end.hasModalsOpen()) { ModalManager.end.closeModals() return@ChatLayout } hideKeyboard(view) scope.launch { - showGroupReportsView(staleChatId, scrollToItemId, info) + showGroupReportsView(staleChatId, scrollToItemId, cInfo) + } + }, + showSupportChats = { + val cInfo = activeChat.value?.chatInfo ?: return@ChatLayout + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout + } + hideKeyboard(view) + scope.launch { + if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ModalManager.end.showCustomModal { close -> + MemberSupportView( + chatRh, + chat, + cInfo.groupInfo, + scrollToItemId, + close + ) + } + } else if (cInfo is ChatInfo.Group) { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) + val supportChatInfo = ChatInfo.Group(cInfo.groupInfo, groupChatScope = scopeInfo) + scope.launch { + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId = scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + } } }, showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> @@ -333,12 +472,12 @@ fun ChatView( setGroupMembers(chatRh, groupInfo, chatModel) if (!isActive) return@launch - if (!groupReports.value.reportsView) { + if (chatsCtx.secondaryContextFilter == 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) + GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close, close) } } } @@ -347,12 +486,12 @@ fun ChatView( val c = chatModel.getChat(chatId) if (chatModel.chatId.value != chatId) return@ChatLayout if (c != null) { - apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, groupReports.value.toContentTag(), pagination, searchText.value, visibleItemIndexes) + apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, searchText.value, null, visibleItemIndexes) } }, deleteMessage = { itemId, mode -> withBGApi { - val toDeleteItem = reversedChatItems.value.lastOrNull { it.id == itemId } + val toDeleteItem = reversedChatItemsStatic(chatsCtx).lastOrNull { it.id == itemId } val toModerate = toDeleteItem?.memberToModerate(chatInfo) val groupInfo = toModerate?.first val groupMember = toModerate?.second @@ -369,6 +508,7 @@ fun ChatView( chatRh, type = chatInfo.chatType, id = chatInfo.apiId, + scope = chatInfo.groupChatScope(), itemIds = listOf(itemId), mode = mode ) @@ -377,30 +517,30 @@ fun ChatView( if (deleted != null) { deletedChatItem = deleted.deletedChatItem.chatItem toChatItem = deleted.toChatItem?.chatItem - withChats { + withContext(Dispatchers.Main) { if (toChatItem != null) { - upsertChatItem(chatRh, chatInfo, toChatItem) + chatModel.chatsContext.upsertChatItem(chatRh, chatInfo, toChatItem) } else { - removeChatItem(chatRh, chatInfo, deletedChatItem) + chatModel.chatsContext.removeChatItem(chatRh, chatInfo, deletedChatItem) } val deletedItem = deleted.deletedChatItem.chatItem if (deletedItem.isActiveReport) { - decreaseGroupReportsCounter(chatRh, chatInfo.id) + chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id) } + chatModel.chatsContext.updateChatInfo(chatRh, deleted.deletedChatItem.chatInfo) } - withReportsChatsIfOpen { - if (deletedChatItem.isReport) { - if (toChatItem != null) { - upsertChatItem(chatRh, chatInfo, toChatItem) - } else { - removeChatItem(chatRh, chatInfo, deletedChatItem) - } + withContext(Dispatchers.Main) { + if (toChatItem != null) { + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem) } } } } }, deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) }, + archiveReports = { itemIds, forAll -> archiveReports(chatRh, chatInfo, itemIds, forAll) }, receiveFile = { fileId -> withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) } }, @@ -450,8 +590,8 @@ fun ChatView( if (r != null) { val contactStats = r.first if (contactStats != null) - withChats { - updateContactConnectionStats(chatRh, contact, contactStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, contactStats) } } } @@ -462,8 +602,8 @@ fun ChatView( if (r != null) { val memStats = r.second if (memStats != null) { - withChats { - updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) } } } @@ -473,8 +613,8 @@ fun ChatView( withBGApi { val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) if (cStats != null) { - withChats { - updateContactConnectionStats(chatRh, contact, cStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats) } } } @@ -483,8 +623,8 @@ fun ChatView( withBGApi { val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) if (r != null) { - withChats { - updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) } } } @@ -501,25 +641,24 @@ fun ChatView( rh = chatRh, type = cInfo.chatType, id = cInfo.apiId, + scope = cInfo.groupChatScope(), itemId = cItem.id, add = add, reaction = reaction ) if (updatedCI != null) { - withChats { - updateChatItem(cInfo, updatedCI) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatItem(cInfo, updatedCI) } - withReportsChatsIfOpen { - if (cItem.isReport) { - updateChatItem(cInfo, updatedCI) - } + withContext(Dispatchers.Main) { + 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) + val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cInfo.groupChatScope(), cItem.id) if (ciInfo != null) { if (chatInfo is ChatInfo.Group) { setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) @@ -531,7 +670,7 @@ fun ChatView( groupMembersJob.cancel() groupMembersJob = scope.launch(Dispatchers.Default) { var initialCiInfo = loadChatItemInfo() ?: return@launch - if (!ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) { + if (!ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) { ModalManager.end.closeModals() } ModalManager.end.showModalCloseable(endButtons = { @@ -540,14 +679,14 @@ fun ChatView( } }) { close -> var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) } - ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) + ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo) LaunchedEffect(cItem.id) { withContext(Dispatchers.Default) { - for (apiResp in controller.messagesChannel) { - val msg = apiResp.resp - if (apiResp.remoteHostId == chatRh && - msg is CR.ChatItemsStatusesUpdated && - msg.chatItems.any { it.chatItem.id == cItem.id } + 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 @@ -565,31 +704,29 @@ fun ChatView( openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, markItemsRead = { itemsIds -> withBGApi { - withChats { - // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace - withContext(Dispatchers.Main) { - markChatItemsRead(chatRh, chatInfo.id, itemsIds) - } + withContext(Dispatchers.Main) { + chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id, itemsIds) ntfManager.cancelNotificationsForChat(chatInfo.id) - chatModel.controller.apiChatItemsRead( + val updatedChatInfo = chatModel.controller.apiChatItemsRead( chatRh, chatInfo.chatType, chatInfo.apiId, + chatInfo.groupChatScope(), itemsIds ) + if (updatedChatInfo != null) { + chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo) + } } - withReportsChatsIfOpen { - markChatItemsRead(chatRh, chatInfo.id, itemsIds) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.markChatItemsRead(chatRh, chatInfo.id, itemsIds) } } }, markChatRead = { withBGApi { - withChats { - // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace - withContext(Dispatchers.Main) { - markChatItemsRead(chatRh, chatInfo.id) - } + withContext(Dispatchers.Main) { + chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id) ntfManager.cancelNotificationsForChat(chatInfo.id) chatModel.controller.apiChatRead( chatRh, @@ -597,30 +734,35 @@ fun ChatView( chatInfo.apiId ) } - withReportsChatsIfOpen { - markChatItemsRead(chatRh, chatInfo.id) + 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 + showSearch = showSearch, + showCommandsMenu = showCommandsMenu ) } } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } ModalView(close, showClose = appPlatform.isAndroid, content = { - ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) + ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connLinkInv, chatInfo.contactConnection, false, close) }) LaunchedEffect(chatInfo.id) { onComposed(chatInfo.id) ModalManager.end.closeModals() - withChats { - chatItems.clearAndNotify() + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() } } } @@ -632,8 +774,8 @@ fun ChatView( LaunchedEffect(chatInfo.id) { onComposed(chatInfo.id) ModalManager.end.closeModals() - withChats { - chatItems.clearAndNotify() + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() } } } @@ -643,6 +785,35 @@ fun ChatView( } } +private fun connectingText(chatInfo: ChatInfo): String? { + return when (chatInfo) { + is ChatInfo.Direct -> + if ( + !chatInfo.contact.sndReady + && chatInfo.contact.active + && !chatInfo.contact.sendMsgToConnect + && !chatInfo.contact.nextAcceptContactRequest + ) { + if ((chatInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con && !chatInfo.contact.isBot) || chatInfo.contact.contactGroupMemberId != null) { + generalGetString(MR.strings.contact_should_accept) + } else { + generalGetString(MR.strings.contact_connection_pending) + } + } else { + null + } + + is ChatInfo.Group -> + when (chatInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemUnknown -> if (chatInfo.groupInfo.preparedGroup?.connLinkStartedConnection == true) generalGetString(MR.strings.group_connection_pending) else null + GroupMemberStatus.MemAccepted -> generalGetString(MR.strings.group_connection_pending) + else -> null + } + + else -> null + } +} + fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) { withBGApi { if (chatInfo is ChatInfo.Direct) { @@ -658,13 +829,12 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) @Composable fun ChatLayout( + chatsCtx: ChatModel.ChatsContext, remoteHostId: State, - chatInfo: State, - reversedChatItems: State>, + chat: State, unreadCount: State, composeState: MutableState, - composeView: (@Composable () -> Unit), - groupReports: State, + composeView: (@Composable (FocusRequester?) -> Unit), scrollToItemId: MutableState, attachmentOption: MutableState, attachmentBottomSheetState: ModalBottomSheetState, @@ -674,11 +844,13 @@ fun ChatLayout( selectedChatItems: MutableState?>, back: () -> Unit, info: () -> Unit, - showGroupReports: () -> Unit, + showReports: () -> Unit, + showSupportChats: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> 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, @@ -700,20 +872,23 @@ fun ChatLayout( openGroupLink: (GroupInfo) -> Unit, markItemsRead: (List) -> Unit, markChatRead: () -> Unit, - changeNtfsState: (Boolean, currentValue: MutableState) -> Unit, + changeNtfsState: (MsgFilter, currentValue: MutableState) -> Unit, onSearchValueChanged: (String) -> Unit, + closeSearch: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean, - showSearch: MutableState + showSearch: MutableState, + showCommandsMenu: MutableState ) { + val chatInfo = remember { derivedStateOf { chat.value?.chatInfo } } val scope = rememberCoroutineScope() val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } Box( Modifier .fillMaxWidth() .desktopOnExternalDrag( - enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.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 = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } }, onText = { @@ -735,29 +910,51 @@ fun ChatLayout( 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, !groupReports.value.reportsView)) { + Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.secondaryContextFilter == null)) { val remoteHostId = remember { remoteHostId }.value - val chatInfo = remember { chatInfo }.value + val chat = remember { chat }.value + val chatInfo = chat?.chatInfo val oneHandUI = remember { appPrefs.oneHandUI.state } val chatBottomBar = remember { appPrefs.chatBottomBar.state } + val composeViewFocusRequester = remember { if (appPlatform.isDesktop) FocusRequester() else null } AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { - if (chatInfo != null) { - Box(Modifier.fillMaxSize()) { + if (chat != null) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { // disables scrolling to top of chat item on click inside the bubble CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec { override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f }) { ChatItemsList( - remoteHostId, chatInfo, reversedChatItems, unreadCount, composeState, composeViewHeight, searchValue, - useLinkPreviews, linkMode, groupReports, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, + chatsCtx, remoteHostId, chat, unreadCount, composeState, composeViewHeight, searchValue, + useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, archiveReports, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy, + 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( + chatsCtx = chatsCtx, + rhId = remoteHostId, + composeState = composeState, + composeViewFocusRequester = composeViewFocusRequester, + chatInfo = chatInfo, + ) + } + } + if (chatInfo != null && chatInfo.menuCommands.isNotEmpty()) { + Column(Modifier.align(Alignment.BottomStart).padding(bottom = composeViewHeight.value)) { + CommandsMenuView(chatsCtx, chat, composeState, showCommandsMenu) + } + } } } - if (groupReports.value.reportsView) { + if (chatsCtx.contentTag == MsgContentTag.Report) { Column( Modifier .layoutId(CHAT_COMPOSE_LAYOUT_ID) @@ -767,8 +964,8 @@ fun ChatLayout( ) { AnimatedVisibility(selectedChatItems.value != null) { if (chatInfo != null) { - SelectedItemsBottomToolbar( - reversedChatItems = reversedChatItems, + SelectedItemsButtonsToolbar( + chatsCtx = chatsCtx, selectedChatItems = selectedChatItems, chatInfo = chatInfo, deleteItems = { _ -> @@ -782,6 +979,7 @@ fun ChatLayout( }) } }, + archiveItems = { archiveItems(remoteHostId, chatInfo, selectedChatItems) }, moderateItems = {}, forwardItems = {} ) @@ -801,45 +999,83 @@ fun ChatLayout( .navigationBarsPadding() .then(if (oneHandUI.value && chatBottomBar.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier) ) { - composeView() + composeView(composeViewFocusRequester) } } } + val reportsCount = reportsCount(chatInfo?.id) + val supportUnreadCount = supportUnreadCount(chatInfo?.id) if (oneHandUI.value && chatBottomBar.value) { - if (groupReports.value.showBar) { - ReportedCountToolbar(groupReports, withStatusBar = true, showGroupReports) + if ( + chatInfo is ChatInfo.Group + && chatsCtx.secondaryContextFilter == null + && (reportsCount > 0 || supportUnreadCount > 0) + ) { + SupportChatsCountToolbar(chatInfo, reportsCount, supportUnreadCount, withStatusBar = true, showReports, showSupportChats) } else { StatusBarBackground() } } else { NavigationBarBackground(true, oneHandUI.value, noAlpha = true) } - if (groupReports.value.reportsView) { - if (oneHandUI.value) { - StatusBarBackground() - } - Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { - Box { - if (selectedChatItems.value == null) { - GroupReportsAppBar(groupReports, { ModalManager.end.closeModal() }, onSearchValueChanged) - } else { - SelectedItemsTopToolbar(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(chatInfo, groupReports, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + when (chatsCtx.secondaryContextFilter) { + is SecondaryContextFilter.GroupChatScopeContext -> { + when (chatsCtx.secondaryContextFilter.groupScopeInfo) { + is GroupChatScopeInfo.MemberSupport -> { + if (oneHandUI.value) { + StatusBarBackground() + } + Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + Box { + if (selectedChatItems.value == null) { + if (chat != null) { + MemberSupportChatAppBar(chatsCtx, remoteHostId, chat, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, scrollToItemId, { ModalManager.end.closeModal() }, onSearchValueChanged) + } + } else { + SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value) + } + } } - } else { - SelectedItemsTopToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value) } } - if (groupReports.value.showBar && (!oneHandUI.value || !chatBottomBar.value)) { - ReportedCountToolbar(groupReports, withStatusBar = false, showGroupReports) + } + is SecondaryContextFilter.MsgContentTagContext -> { + when (chatsCtx.secondaryContextFilter.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 -> TODO() + } + } + null -> { + 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 + && (reportsCount > 0 || supportUnreadCount > 0) + && (!oneHandUI.value || !chatBottomBar.value) + ) { + SupportChatsCountToolbar(chatInfo, reportsCount, supportUnreadCount, withStatusBar = false, showReports, showSupportChats) + } } } } @@ -850,15 +1086,15 @@ fun ChatLayout( @Composable fun BoxScope.ChatInfoToolbar( + chatsCtx: ChatModel.ChatsContext, chatInfo: ChatInfo, - groupReports: State, 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 ) { @@ -873,7 +1109,7 @@ fun BoxScope.ChatInfoToolbar( showSearch.value = false } } - if (appPlatform.isAndroid && !groupReports.value.reportsView) { + if (appPlatform.isAndroid && chatsCtx.secondaryContextFilter == null) { BackHandler(onBack = onBackClicked) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() @@ -977,18 +1213,20 @@ fun BoxScope.ChatInfoToolbar( } } - if ((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) { - val ntfsEnabled = remember { mutableStateOf(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) } } ) @@ -1064,38 +1302,125 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo ) } } + val chatSubStatus = chatModel.chatSubStatus.value + if ( + cInfo is ChatInfo.Direct && + cInfo.contact.ready && + cInfo.contact.active && + chatSubStatus != null && + chatSubStatus != SubscriptionStatus.Active + ) { + Box( + Modifier.padding(start = 10.dp) + ) { + SubStatusView(chatSubStatus) + } + } } } @Composable -private fun ReportedCountToolbar( - groupReports: State, +fun SubStatusView(status: SubscriptionStatus) { + when (status) { + SubscriptionStatus.Active -> + Box {} + SubscriptionStatus.Pending -> + SubProgressView() + is SubscriptionStatus.Removed, SubscriptionStatus.NoSub -> + Icon( + painterResource(MR.images.ic_error), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(19.sp.toDp()) + ) + } +} + +@Composable +private fun SubProgressView() { + CircularProgressIndicator( + Modifier + .size(15.sp.toDp()), + color = MaterialTheme.colors.secondary, + strokeWidth = 1.5.dp + ) +} + +@Composable +private fun SupportChatsCountToolbar( + chatInfo: ChatInfo, + reportsCount: Int, + supportUnreadCount: Int, withStatusBar: Boolean, - showGroupReports: () -> Unit + showReports: () -> Unit, + showSupportChats: () -> 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 + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, ) { - Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error) - Spacer(Modifier.width(4.dp)) - val reports = groupReports.value.reportsCount - Text( - if (reports == 1) { - stringResource(MR.strings.group_reports_active_one) - } else { - stringResource(MR.strings.group_reports_active).format(reports) - }, - style = MaterialTheme.typography.button - ) + if ( + chatInfo is ChatInfo.Group + && chatInfo.groupInfo.canModerate + && reportsCount > 0 + ) { + Row( + Modifier + .fillMaxWidth() + .weight(1F) + .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding) + .background(MaterialTheme.colors.background) + .clickable(onClick = showReports) + .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 + ) + } + } + + if (supportUnreadCount > 0) { + Row( + Modifier + .fillMaxWidth() + .weight(1F) + .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding) + .background(MaterialTheme.colors.background) + .clickable(onClick = showSupportChats) + .padding(top = statusBarPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.primary) + Spacer(Modifier.width(4.dp)) + Text( + if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) { + if (appPlatform.isAndroid) + stringResource(MR.strings.group_new_support_chats_short).format(supportUnreadCount) + else if (supportUnreadCount == 1) + stringResource(MR.strings.group_new_support_chat_one) + else + stringResource(MR.strings.group_new_support_chats).format(supportUnreadCount) + } else { + stringResource(MR.strings.group_new_support_messages).format(supportUnreadCount) + }, + style = MaterialTheme.typography.button + ) + } + } } Divider(Modifier.align(Alignment.BottomStart)) } @@ -1106,21 +1431,20 @@ 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 [GroupReports] are open and user opens [ChatItemInfoView], for example, and goes back */ +/** 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, - reversedChatItems: State>, + chat: Chat, unreadCount: State, composeState: MutableState, composeViewHeight: State, searchValue: State, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, - groupReports: State, scrollToItemId: MutableState, selectedChatItems: MutableState?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, @@ -1128,6 +1452,7 @@ fun BoxScope.ChatItemsList( 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, @@ -1145,33 +1470,81 @@ fun BoxScope.ChatItemsList( showItemDetails: (ChatInfo, ChatItem) -> Unit, markItemsRead: (List) -> Unit, markChatRead: () -> Unit, + closeSearch: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean ) { + val chatInfo = chat.chatInfo + val loadingTopItems = remember { mutableStateOf(false) } + val loadingBottomItems = remember { mutableStateOf(false) } + // just for changing local var here based on request + 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 + } + } val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } } + val searchValueIsNotBlank = remember { derivedStateOf { searchValue.value.isNotBlank() } } val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf()) } - val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatStateForContent(groupReports.value.contentTag)) } } - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() }) + // not using reversedChatItems inside to prevent possible derivedState bug in Compose when one derived state access can cause crash asking another derived state + val mergedItems = remember { + derivedStateOf { + MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState) + } + } + val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } } + val reportsCount = reportsCount(chatInfo.id) + val supportUnreadCount = supportUnreadCount(chatInfo.id) + val topPaddingToContent = topPaddingToContent( + chatView = chatsCtx.secondaryContextFilter == null, + additionalTopBar = chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportUnreadCount > 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 * 2).roundToPx() } + with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() } ) - val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, saver = LazyListState.Saver) { - val index = mergedItems.value.items.indexOfLast { it.hasUnread() } + 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) { + } else if (index <= 0 || !searchValueIsEmpty.value) { LazyListState(0, 0) } else { LazyListState(index + 1, -maxHeightForList.value) } }) - SaveReportsStateOnDispose(groupReports, listState) + SaveReportsStateOnDispose(chatsCtx, listState) val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } val loadingMoreItems = remember { mutableStateOf(false) } val animatedScrollingInProgress = remember { mutableStateOf(false) } @@ -1180,350 +1553,505 @@ fun BoxScope.ChatItemsList( if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT) ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect) } - if (!loadingMoreItems.value) { - PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), reversedChatItems, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> - if (loadingMoreItems.value) return@PreloadItems false + 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 { - loadingMoreItems.value = true loadMessages(chatId, pagination) { visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value) } } finally { loadingMoreItems.value = false } - true } + true } - val remoteHostIdUpdated = rememberUpdatedState(remoteHostId) val chatInfoUpdated = rememberUpdatedState(chatInfo) - val highlightedItems = remember { mutableStateOf(setOf()) } 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 (groupReports.value.reportsView) return@remember { scrollToItemId.value = it } scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } - val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem, groupReports.value.contentTag) } - if (!groupReports.value.reportsView) { - LaunchedEffect(Unit) { snapshotFlow { scrollToItemId.value }.filterNotNull().collect { - if (appPlatform.isAndroid) { - ModalManager.end.closeModals() + val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(chatsCtx, remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem, scrollToItemId) } + if (chatsCtx.secondaryContextFilter == null) { + LaunchedEffect(Unit) { + snapshotFlow { scrollToItemId.value }.filterNotNull().collect { + if (appPlatform.isAndroid) { + ModalManager.end.closeModals() + } + scrollToItem(it) + scrollToItemId.value = null } - scrollToItem(it) - scrollToItemId.value = null } } } - LoadLastItems(loadingMoreItems, remoteHostId, chatInfo, groupReports) SmallScrollOnNewMessage(listState, reversedChatItems) val finishedInitialComposition = remember { mutableStateOf(false) } NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) DisposableEffectOnGone( - always = { - chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(groupReports.value.contentTag)), groupReports.value.contentTag) - }, whenGone = { VideoPlayerHolder.releaseAll() - chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(groupReports.value.contentTag)), groupReports.value.contentTag) } ) - @Composable - fun ChatViewListItem( - itemAtZeroIndexInWholeList: Boolean, - range: State, - showAvatar: Boolean, - cItem: ChatItem, - itemSeparation: ItemSeparation, - previousItemSeparationLargeGap: Boolean, - revealed: State, - reveal: (Boolean) -> Unit + @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 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 provider = { - providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed -> + val provider = { + providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed -> + itemScope.launch { + listState.value.scrollToItem( + min(reversedChatItems.value.lastIndex, indexInReversed + 1), + -maxHeight.value + ) + } + } + } + + @Composable + 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) + }) { + 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, chat, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToItemId = scrollToItemId, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + } + } + + @Composable + fun ChatItemView(cItem: ChatItem, range: State, itemSeparation: ItemSeparation, previousItemSeparationLargeGap: Boolean) { + val dismissState = rememberDismissState(initialValue = DismissValue.Default) { + if (it == DismissValue.DismissedToStart) { itemScope.launch { - listState.value.scrollToItem( - min(reversedChatItems.value.lastIndex, indexInReversed + 1), - -maxHeight.value - ) - } - } - } - - @Composable - 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) - }) { - val highlighted = remember { derivedStateOf { highlightedItems.value.contains(cItem.id) } } - LaunchedEffect(Unit) { - snapshotFlow { highlighted.value } - .distinctUntilChanged() - .filter { it } - .collect { - delay(500) - highlightedItems.value = setOf() - } - } - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, 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: 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)) - } + 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)) } } } - false } - val swipeableModifier = SwipeToDismissModifier( - state = dismissState, - directions = setOf(DismissDirection.EndToStart), - swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, - ) - val sent = cItem.chatDir.sent + 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 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 + @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) - } + 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 + ) { + @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 - 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 - ) { - @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), + 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, - 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()) { - SelectedChatItem(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()) { - SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + + @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) + } + } } - 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) + } + if (cItem.content.showMemberName) { + DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) { + MemberNameAndRole(range) + Item() } + } else { + Item() } } } else { ChatItemBox { - AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) { - SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) } - Box( + Row( Modifier - .padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp) + .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(if (selectionVisible) Modifier else swipeableModifier) + .then(swipeableOrSelectionModifier) ) { ChatItemViewShortHand(cItem, itemSeparation, range) } } } - } else { // direct message + } else { ChatItemBox { - AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) { - SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + 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), - ) + 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 || !sent) swipeableOrSelectionModifier else Modifier) + .then(if (selectionVisible) Modifier else swipeableModifier) ) { ChatItemViewShortHand(cItem, itemSeparation, range) } } } - if (selectionVisible) { - Box(Modifier.matchParentSize().clickable { - val checked = selectedChatItems.value?.contains(cItem.id) == true - selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems) - }) + } 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) + } } } + if (selectionVisible) { + Box(Modifier.matchParentSize().clickable { + val checked = selectedChatItems.value?.contains(cItem.id) == true + selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems) + }) + } } - if (itemSeparation.date != null) { - DateSeparator(itemSeparation.date) + } + if (itemSeparation.date != null) { + DateSeparator(itemSeparation.date) + } + ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap) + } + } + + @Composable + fun ChatBannerView() { + fun chatContext(): String? { + return when (chatInfo) { + is ChatInfo.Direct -> { + val contact = chatInfo.contact + val preparedLinkType = contact.preparedContact?.uiConnLinkType + if (contact.nextConnectPrepared && preparedLinkType != null) { + when (preparedLinkType) { + ConnectionMode.Inv -> generalGetString(MR.strings.chat_banner_connect_to_chat) + ConnectionMode.Con -> generalGetString(if (contact.isBot) MR.strings.chat_banner_connect_to_use_bot else MR.strings.chat_banner_send_request_to_connect) + } + } else if (contact.nextAcceptContactRequest) { + generalGetString(MR.strings.chat_banner_accept_contact_request) + } else if (contact.isBot) { + generalGetString(MR.strings.chat_banner_bot) + } else { + generalGetString(MR.strings.chat_banner_your_contact) + } } - ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap) + + is ChatInfo.Group -> { + val groupInfo = chatInfo.groupInfo + when (groupInfo.businessChat?.chatType) { + null -> { + if (groupInfo.nextConnectPrepared) { + generalGetString(MR.strings.chat_banner_join_group) + } else { + when (groupInfo.membership.memberStatus) { + GroupMemberStatus.MemInvited -> generalGetString(MR.strings.chat_banner_join_group) + GroupMemberStatus.MemCreator -> generalGetString(MR.strings.chat_banner_your_group) + else -> generalGetString(MR.strings.chat_banner_group) + } + } + } + + BusinessChatType.Business -> + if (groupInfo.nextConnectPrepared) { + generalGetString(MR.strings.chat_banner_connect_to_chat) + } else { + generalGetString(MR.strings.chat_banner_business_connection) + } + BusinessChatType.Customer -> + generalGetString(MR.strings.chat_banner_your_business_contact) + } + } + + else -> null } } + + Box( + Modifier + .clipChatItem() + .background(MaterialTheme.appColors.receivedMessage) + ) { + val bannerModifier = if (appPlatform.isDesktop) Modifier.width(400.dp) else Modifier.fillMaxWidth() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = bannerModifier + .padding(horizontal = DEFAULT_PADDING) + .padding(bottom = DEFAULT_PADDING) + // ChatInfoImage has its own padding somewhere, + // also not doing verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF) because of it + .padding(top = DEFAULT_PADDING_HALF) + .background(MaterialTheme.appColors.receivedMessage) + ) { + ChatInfoImage(chatInfo, size = alertProfileImageSize, iconColor = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) + Text( + chatInfo.displayName, + style = MaterialTheme.typography.h3, + color = MaterialTheme.colors.onBackground, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .widthIn(max = 240.dp) + ) + + val fullName = chatInfo.fullName.trim() + if (fullName.isNotEmpty() && fullName != chatInfo.displayName && fullName != chatInfo.displayName.trim()) { + Text( + fullName, + style = MaterialTheme.typography.h4, + color = MaterialTheme.colors.onBackground, + textAlign = TextAlign.Center, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .widthIn(max = 260.dp) + .padding(top = DEFAULT_PADDING_HALF) + ) + } + + val descr = chatInfo.shortDescr?.trim() + if (descr != null && descr != "") { + MarkdownText( + descr, + parseToMarkdown(descr), + toggleSecrets = true, + style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), + maxLines = 4, + overflow = TextOverflow.Ellipsis, + uriHandler = LocalUriHandler.current, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF), + linkMode = linkMode + ) + } + + val contextStr = chatContext() + if (contextStr != null) { + Text( + contextStr, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary, + modifier = Modifier.padding(top = DEFAULT_PADDING) + ) + } + } + } + } + LazyColumnWithScrollBar( Modifier.align(Alignment.BottomCenter), state = listState.value, contentPadding = PaddingValues( - top = topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar), + top = topPaddingToContent, bottom = composeViewHeight.value ), reverseLayout = true, additionalBarOffset = composeViewHeight, - additionalTopBar = remember { derivedStateOf { groupReports.value.showBar } }, + additionalTopBar = rememberUpdatedState(chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportUnreadCount > 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 { - 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 (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 } + if (item.content is CIContent.ChatBanner) { + Column { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = DEFAULT_PADDING) + .padding(bottom = 90.dp, top = DEFAULT_PADDING) + ) { + ChatBannerView() + } + + val prevItem = listItem.prevItem + if (prevItem != null) { + DateSeparator(prevItem.meta.itemTs) + } + } + } else { + val isLastItem = index == mergedItemsValue.items.lastIndex + val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null + val range = if (merged is MergedItem.Grouped) { + merged.rangeInReversed.value + } else { + null + } + val showAvatar = shouldShowAvatar(item, merged.oldest().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 (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) } - MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead) } } } - FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, groupReports, markChatRead, listState) - FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar)).align(Alignment.TopCenter), mergedItems, listState, groupReports) + 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 } @@ -1542,22 +2070,24 @@ fun BoxScope.ChatItemsList( } } -@Composable -private fun LoadLastItems(loadingMoreItems: MutableState, remoteHostId: Long?, chatInfo: ChatInfo, groupReports: State) { - LaunchedEffect(remoteHostId, chatInfo.id) { - try { - loadingMoreItems.value = true - if (chatModel.chatStateForContent(groupReports.value.contentTag).totalAfter.value <= 0) return@LaunchedEffect - delay(500) - withContext(Dispatchers.Default) { - apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, groupReports.value.toContentTag(), ChatPagination.Last(ChatPagination.INITIAL_COUNT)) - } - } finally { - loadingMoreItems.value = false - } - } +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 SmallScrollOnNewMessage(listState: State, reversedChatItems: State>) { val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() } @@ -1616,19 +2146,59 @@ private fun NotifyChatListOnFinishingComposition( @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, maxHeight: State, composeViewHeight: State, searchValue: State, - groupReports: State, markChatRead: () -> Unit, - listState: State + listState: State, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit ) { val scope = rememberCoroutineScope() - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() }) + fun scrollToBottom() { + scope.launch { + animatedScrollingInProgress.value = true + tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } + } + } + 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 @@ -1654,49 +2224,79 @@ fun BoxScope.FloatingButtons( allowToShowBottomWithArrow.value = shouldShow shouldShow && allow } } + + val requestedTopScroll = remember { mutableStateOf(false) } + val requestedBottomScroll = remember { mutableStateOf(false) } + BottomEndFloatingButton( bottomUnreadCount, showBottomButtonWithCounter, showBottomButtonWithArrow, + requestedBottomScroll, animatedScrollingInProgress, composeViewHeight, onClick = { - scope.launch { - animatedScrollingInProgress.value = true - tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } + 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 = remember { derivedStateOf { if (bottomUnreadCount.value >= 0) (unreadCount.value - bottomUnreadCount.value).coerceAtLeast(0) else 0 } } + val topUnreadCount = remember { derivedStateOf { + if (bottomUnreadCount.value >= 0) (unreadCount.value - bottomUnreadCount.value).coerceAtLeast(0) else 0 } + } val showDropDown = remember { mutableStateOf(false) } TopEndFloatingButton( - Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar)).align(Alignment.TopEnd), + Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent).align(Alignment.TopEnd), topUnreadCount, + requestedTopScroll, animatedScrollingInProgress, onClick = { - val index = mergedItems.value.items.indexOfLast { it.hasUnread() } - if (index != -1) { - // scroll to the top unread item - scope.launch { - animatedScrollingInProgress.value = true - tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) } - } + if (loadingTopItems.value) { + requestedBottomScroll.value = false + requestedTopScroll.value = true + } else { + scrollToTopUnread() } }, onLongClick = { showDropDown.value = true } ) - Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) { + 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(chatView = !groupReports.value.reportsView, groupReports.value.showBar)) + offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent) ) { ItemAction( generalGetString(MR.strings.mark_read), @@ -1711,9 +2311,11 @@ fun BoxScope.FloatingButtons( @Composable fun PreloadItems( + chatsCtx: ChatModel.ChatsContext, chatId: String, ignoreLoadingRequests: MutableSet, - reversedChatItems: State>, + loadingMoreItems: State, + resetListState: State, mergedItems: State, listState: State, remaining: Int, @@ -1724,98 +2326,83 @@ fun PreloadItems( val chatId = rememberUpdatedState(chatId) val loadItems = rememberUpdatedState(loadItems) val ignoreLoadingRequests = rememberUpdatedState(ignoreLoadingRequests) - PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, reversedChatItems, mergedItems, listState, remaining, loadItems) - PreloadItemsAfter(allowLoad, chatId, reversedChatItems, mergedItems, listState, remaining, loadItems) -} - -@Composable -private fun PreloadItemsBefore( - allowLoad: State, - chatId: State, - ignoreLoadingRequests: State>, - reversedChatItems: State>, - mergedItems: State, - listState: State, - remaining: Int, - loadItems: State Boolean>, -) { - KeyChangeEffect(allowLoad.value, chatId.value) { - snapshotFlow { listState.value.firstVisibleItemIndex } - .distinctUntilChanged() - .map { firstVisibleIndex -> - val splits = mergedItems.value.splits - val lastVisibleIndex = (listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) - var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits) - val items = reversedChatItems.value - if (splits.isEmpty() && items.isNotEmpty() && lastVisibleIndex > mergedItems.value.items.size - remaining) { - lastIndexToLoadFrom = 0 - } - if (allowLoad.value && lastIndexToLoadFrom != null) { - items.getOrNull(items.lastIndex - lastIndexToLoadFrom)?.id - } else { - null - } - } - .filterNotNull() - .filter { !ignoreLoadingRequests.value.contains(it) } - .collect { loadFromItemId -> - withBGApi { - val sizeWas = reversedChatItems.value.size - val oldestItemIdWas = reversedChatItems.value.lastOrNull()?.id - val triedToLoad = loadItems.value(chatId.value, ChatPagination.Before(loadFromItemId, ChatPagination.PRELOAD_COUNT)) - if (triedToLoad && sizeWas == reversedChatItems.value.size && oldestItemIdWas == reversedChatItems.value.lastOrNull()?.id) { - ignoreLoadingRequests.value.add(loadFromItemId) - } - } - } - } -} - -@Composable -private fun PreloadItemsAfter( - allowLoad: MutableState, - chatId: State, - reversedChatItems: State>, - mergedItems: State, - listState: State, - remaining: Int, - loadItems: State Boolean>, -) { LaunchedEffect(Unit) { snapshotFlow { chatId.value } .distinctUntilChanged() .filterNotNull() .collect { - allowLoad.value = listState.value.layoutInfo.totalItemsCount == listState.value.layoutInfo.visibleItemsInfo.size + allowLoad.value = false delay(500) allowLoad.value = true } } - LaunchedEffect(chatId.value) { - launch { + if (allowLoad.value && !loadingMoreItems.value) { + LaunchedEffect(chatId.value, resetListState.value) { snapshotFlow { listState.value.firstVisibleItemIndex } .distinctUntilChanged() - .map { firstVisibleIndex -> - val items = reversedChatItems.value - 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) { - items.getOrNull(split.indexRangeInReversed.first)?.id - } else { - null - } - } - .filterNotNull() - .collect { loadFromItemId -> - withBGApi { - loadItems.value(chatId.value, ChatPagination.After(loadFromItemId, ChatPagination.PRELOAD_COUNT)) + .collect { firstVisibleIndex -> + if (!preloadItemsBefore(chatsCtx, firstVisibleIndex, chatId, ignoreLoadingRequests, mergedItems, listState, remaining, loadItems)) { + preloadItemsAfter(chatsCtx, firstVisibleIndex, chatId, mergedItems, remaining, loadItems) } + loadLastItems(chatsCtx, chatId, listState, loadItems) } } } } +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 +} + +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 @@ -1827,6 +2414,7 @@ fun MemberImage(member: GroupMember) { private fun TopEndFloatingButton( modifier: Modifier = Modifier, unreadCount: State, + requestedTopScroll: State, animatedScrollingInProgress: State, onClick: () -> Unit, onLongClick: () -> Unit @@ -1840,11 +2428,15 @@ private fun TopEndFloatingButton( elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp), interactionSource = interactionSource, ) { - Text( - unreadCountStr(unreadCount.value), - color = MaterialTheme.colors.primary, - fontSize = 14.sp, - ) + if (requestedTopScroll.value) { + LoadingProgressIndicator() + } else { + Text( + unreadCountStr(unreadCount.value), + color = MaterialTheme.colors.primary, + fontSize = 14.sp, + ) + } } } } @@ -1861,25 +2453,40 @@ fun topPaddingToContent(chatView: Boolean, additionalTopBar: Boolean = false): D } } +@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, - groupReports: 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 topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() }) 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) + val itemTs = lastVisibleChatItem?.meta?.itemTs + if (itemTs != null && itemTs.epochSeconds > 0) { + itemTs.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone) + } else { + null + } } else { null } @@ -1957,10 +2564,10 @@ private fun FloatingDate( } @Composable -private fun SaveReportsStateOnDispose(groupReports: State, listState: State) { +private fun SaveReportsStateOnDispose(chatsCtx: ChatModel.ChatsContext, listState: State) { DisposableEffect(Unit) { onDispose { - reportsListState = if (groupReports.value.reportsView && ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) listState.value else null + reportsListState = if (chatsCtx.contentTag == MsgContentTag.Report && ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) listState.value else null } } } @@ -2038,7 +2645,7 @@ private fun DateSeparator(date: Instant) { @Composable private fun MarkItemsReadAfterDelay( - itemKey: String, + itemKey: ChatViewItemKey, itemIds: List, finishedInitialComposition: State, chatId: ChatId, @@ -2062,20 +2669,43 @@ private fun MarkItemsReadAfterDelay( } } +@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 + } +} + +@Composable +fun supportUnreadCount(staleChatId: String?): Int { + return if (staleChatId?.startsWith("#") != true) { + 0 + } else { + remember(staleChatId) { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId } } }.value?.supportUnreadCount ?: 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 - return mergedItems.value.items.getOrNull((listState.value.layoutInfo.visibleItemsInfo.lastOrNull { item -> + val visibleKey: ChatViewItemKey? = listState.value.layoutInfo.visibleItemsInfo.lastOrNull { item -> item.offset <= lastFullyVisibleOffset - }?.index ?: listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index) ?: -1)?.oldest() + }?.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( - (listState.value.layoutInfo.visibleItemsInfo.lastOrNull { item -> - item.offset <= lastFullyVisibleOffsetMinusFloatingHeight && item.size > 0 - } - ?.index + (mergedItems.value.indexInParentItems[visibleKey?.first] ?: listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index) ?: -1)?.newest()?.item } @@ -2134,23 +2764,28 @@ private fun scrollToItem( } private fun findQuotedItemFromItem( + chatsCtx: ChatModel.ChatsContext, rhId: State, chatInfo: State, scope: CoroutineScope, scrollToItem: (Long) -> Unit, - contentTag: MsgContentTag? + scrollToItemId: MutableState ): (Long) -> Unit = { itemId: Long -> scope.launch(Dispatchers.Default) { - val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId, contentTag) + val item = apiLoadSingleMessage(chatsCtx, rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId) if (item != null) { - withChats { - updateChatItem(chatInfo.value, item) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatItem(chatInfo.value, item) } - withReportsChatsIfOpen { - updateChatItem(chatInfo.value, item) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateChatItem(chatInfo.value, item) } if (item.quotedItem?.itemId != null) { - scrollToItem(item.quotedItem.itemId) + if (item.isReport && chatsCtx.secondaryContextFilter != null) { + scrollToItemId.value = item.quotedItem.itemId + } else { + scrollToItem(item.quotedItem.itemId) + } } else { showQuotedItemDoesNotExistAlert() } @@ -2177,7 +2812,7 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: ( val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId) close?.invoke() ModalManager.end.showModalCloseable(true) { - GroupLinkView(chatModel, rhId, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) + GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null) } } } @@ -2187,48 +2822,59 @@ 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, - ) { - Text( - unreadCountStr(unreadCount.value), - color = MaterialTheme.colors.primary, - fontSize = 14.sp, - ) +) { + 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, - ) { - Icon( - painter = painterResource(MR.images.ic_keyboard_arrow_down), - contentDescription = null, - tint = MaterialTheme.colors.primary - ) + 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 -> {} } - else -> {} } @Composable -private fun SelectedChatItem( +fun SelectedListItem( modifier: Modifier, - ciId: Long, - selectedChatItems: State?>, + id: Long, + selectedItems: State?>, ) { - val checked = remember { derivedStateOf { selectedChatItems.value?.contains(ciId) == true } } + 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, @@ -2245,6 +2891,20 @@ private fun SelectedChatItem( ) } +@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, @@ -2297,34 +2957,36 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List + chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo) + } } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { for (di in deleted) { - if (di.deletedChatItem.chatItem.isReport) { - val toChatItem = di.toChatItem?.chatItem - if (toChatItem != null) { - upsertChatItem(chatRh, chatInfo, toChatItem) - } else { - removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) - } + 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) } } } @@ -2334,6 +2996,54 @@ private fun deleteMessages(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 deletedItem = di.deletedChatItem.chatItem + if (deletedItem.isActiveReport) { + chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id) + } + } + deleted.lastOrNull()?.deletedChatItem?.chatInfo?.let { updatedChatInfo -> + chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo) + } + } + withContext(Dispatchers.Main) { + for (di in deleted) { + 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 @@ -2346,9 +3056,9 @@ private fun markUnreadChatAsRead(chatId: String) { false ) if (success) { - withChats { - replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) - markChatTagRead(chat) + withContext(Dispatchers.Main) { + chatModel.chatsContext.replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + chatModel.chatsContext.markChatTagRead(chat) } } } @@ -2510,7 +3220,9 @@ fun providerForGallery( } } -private fun keyForItem(item: ChatItem): String = (item.id to item.meta.createdAt.toEpochMilliseconds()).toString() +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 @@ -2528,7 +3240,7 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf private fun forwardContent(chatItemsIds: List, chatInfo: ChatInfo) { chatModel.chatId.value = null chatModel.sharedContent.value = SharedContent.Forward( - chatModel.chatItemsForContent(null).value.filter { chatItemsIds.contains(it.id) }, + chatModel.chatsContext.chatItems.value.filter { chatItemsIds.contains(it.id) }, chatInfo ) } @@ -2663,13 +3375,12 @@ fun PreviewChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( + chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), remoteHostId = remember { mutableStateOf(null) }, - chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, - reversedChatItems = remember { mutableStateOf(emptyList()) }, + chat = remember { mutableStateOf(Chat.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, - composeView = {}, - groupReports = remember { mutableStateOf(GroupReports(0, false)) }, + composeView = { _ -> }, scrollToItemId = remember { mutableStateOf(null) }, attachmentOption = remember { mutableStateOf(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), @@ -2679,11 +3390,13 @@ fun PreviewChatLayout() { selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, - showGroupReports = {}, + showReports = {}, + showSupportChats = {}, showMemberInfo = { _, _ -> }, loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, + archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, @@ -2707,10 +3420,12 @@ fun PreviewChatLayout() { markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, + closeSearch = {}, onComposed = {}, developerTools = false, showViaProxy = false, - showSearch = remember { mutableStateOf(false) } + showSearch = remember { mutableStateOf(false) }, + showCommandsMenu = remember { mutableStateOf(false) } ) } } @@ -2740,13 +3455,12 @@ fun PreviewGroupChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( + chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), remoteHostId = remember { mutableStateOf(null) }, - chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, - reversedChatItems = remember { mutableStateOf(emptyList()) }, + chat = remember { mutableStateOf(Chat.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, - composeView = {}, - groupReports = remember { mutableStateOf(GroupReports(0, false)) }, + composeView = { _ -> }, scrollToItemId = remember { mutableStateOf(null) }, attachmentOption = remember { mutableStateOf(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), @@ -2756,11 +3470,13 @@ fun PreviewGroupChatLayout() { selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, - showGroupReports = {}, + showReports = {}, + showSupportChats = {}, showMemberInfo = { _, _ -> }, loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = {}, + archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, @@ -2784,10 +3500,12 @@ fun PreviewGroupChatLayout() { markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, + closeSearch = {}, onComposed = {}, developerTools = false, showViaProxy = false, - showSearch = remember { mutableStateOf(false) } + showSearch = remember { mutableStateOf(false) }, + showCommandsMenu = remember { mutableStateOf(false) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt new file mode 100644 index 0000000000..26b1fce741 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt @@ -0,0 +1,244 @@ +package chat.simplex.common.views.chat + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.chat.group.* +import chat.simplex.common.views.chat.item.sendCommandMsg +import chat.simplex.common.views.helpers.commandMenuAnimSpec +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.launch + +private val COMMAND_MENU_ROW_SIZE = 48.dp +private val MAX_COMMAND_MENU_HEIGHT = COMMAND_MENU_ROW_SIZE * 6 - 8.dp + +@Composable +fun CommandsMenuView( + chatsCtx: ChatModel.ChatsContext, + chat: Chat, + composeState: MutableState, + showCommandsMenu: MutableState +) { + val maxHeightInPx = with(LocalDensity.current) { windowHeight().toPx() } + val offsetY = remember { Animatable(maxHeightInPx) } + val scope = rememberCoroutineScope() + + val currentCommands = remember { mutableStateOf>(emptyList()) } + val menuTreeBackPath = remember { mutableStateOf>>>(emptyList()) } + + fun filterShownCommands(commands: List, msg: CharSequence): List { + val cmds = mutableListOf() + for (cmd in commands) { + when (cmd) { + is ChatBotCommand.Command -> + if (cmd.keyword.startsWith(msg)) { + cmds.add(cmd) + } + is ChatBotCommand.Menu -> + cmds.addAll(filterShownCommands(cmd.commands, msg)) + } + } + return cmds + } + + suspend fun closeCommandsMenu() { + showCommandsMenu.value = false + currentCommands.value = emptyList() + menuTreeBackPath.value = emptyList() + if (offsetY.value != 0f) { + return + } + offsetY.animateTo( + targetValue = maxHeightInPx, + animationSpec = commandMenuAnimSpec() + ) + } + + fun messageChanged(message: String) { + val msg = message.trim() + menuTreeBackPath.value = emptyList() + if (msg == "/") { + currentCommands.value = chat.chatInfo.menuCommands + } else if (msg.startsWith("/")) { + currentCommands.value = filterShownCommands(chat.chatInfo.menuCommands, msg.drop(1)) + } else { + scope.launch { closeCommandsMenu() } + } + } + + LaunchedEffect(currentCommands.value.isNotEmpty()) { + if (currentCommands.value.isNotEmpty()) { + offsetY.animateTo( + targetValue = 0f, + animationSpec = commandMenuAnimSpec() + ) + } + } + + LaunchedEffect(composeState.value.message) { + messageChanged(composeState.value.message.text) + } + + LaunchedEffect(showCommandsMenu.value) { + if (showCommandsMenu.value) { + currentCommands.value = chat.chatInfo.menuCommands + menuTreeBackPath.value = emptyList() + } else { + closeCommandsMenu() + } + } + + @Composable + fun MenuLabelRow(prev: Pair>) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + if (menuTreeBackPath.value.isNotEmpty()) { + currentCommands.value = menuTreeBackPath.value.last().second + menuTreeBackPath.value = menuTreeBackPath.value.dropLast(1) + } + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(MR.images.ic_arrow_back_ios_new), + contentDescription = null, + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + text = prev.first, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium, + maxLines = 1, + modifier = Modifier.weight(1f), + overflow = TextOverflow.Ellipsis + ) + } + } + } + + @Composable + fun CommandRow(cmd: ChatBotCommand) { + when (cmd) { + is ChatBotCommand.Command -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + if (cmd.params != null) { + val msg = "/${cmd.keyword} ${cmd.params}" + composeState.value = ComposeState(message = ComposeMessage(msg, TextRange(msg.length)), useLinkPreviews = true) + } else { + composeState.value = ComposeState(message = ComposeMessage(), useLinkPreviews = true) + sendCommandMsg(chatsCtx, chat,"/${cmd.keyword}") + } + scope.launch { closeCommandsMenu() } + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text( + text = cmd.label, + style = MaterialTheme.typography.body1, + maxLines = 1, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + text = "/${cmd.keyword}", + style = MaterialTheme.typography.body2, + maxLines = 1, + color = MaterialTheme.colors.secondary + ) + } + } + } + is ChatBotCommand.Menu -> + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + menuTreeBackPath.value += Pair(cmd.label, currentCommands.value) + currentCommands.value = cmd.commands + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text( + text = cmd.label, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Medium, + maxLines = 1, + modifier = Modifier.weight(1f), + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Icon( + painterResource(MR.images.ic_chevron_right), + contentDescription = null, + tint = MaterialTheme.colors.secondary + ) + } + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .offset { IntOffset(0, offsetY.value.toInt()) } + .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { + scope.launch { closeCommandsMenu() } + }, + contentAlignment = Alignment.BottomStart + ) { + LazyColumnWithScrollBarNoAppBar( + Modifier + .heightIn(max = MAX_COMMAND_MENU_HEIGHT) + .background(MaterialTheme.colors.surface), + maxHeight = remember { mutableStateOf(MAX_COMMAND_MENU_HEIGHT) }, + containerAlignment = Alignment.BottomEnd + ) { + itemsIndexed(currentCommands.value, key = { i, cmd -> "$i ${cmd.hashCode()}" }) { i, command -> + if (i == 0) { + val prev = menuTreeBackPath.value.lastOrNull() + if (prev != null) { + Divider() + MenuLabelRow(prev) + } + } + Divider() + Box(Modifier.fillMaxWidth()) { CommandRow(command) } + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt new file mode 100644 index 0000000000..106c975226 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt @@ -0,0 +1,165 @@ +package chat.simplex.common.views.chat + +import SectionItemView +import TextIconSpaced +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.chatlist.acceptContactRequest +import chat.simplex.common.views.chatlist.rejectContactRequest +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay + +@Composable +fun ComposeContextContactRequestActionsView( + rhId: Long?, + contactRequestId: Long +) { + val inProgress = rememberSaveable { mutableStateOf(false) } + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + + KeyChangeEffect(chatModel.chatId.value) { + if (inProgress.value) { + inProgress.value = false + progressByTimeout = false + } + } + + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(1000) + inProgress.value + } else { + false + } + } + + Box( + Modifier.height(60.dp), + contentAlignment = Alignment.Center + ) { + Column( + Modifier + .background(MaterialTheme.colors.surface) + .alpha(if (progressByTimeout) 0.6f else 1f) + ) { + Divider() + + Row( + Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + var rejectButtonModifier = Modifier.fillMaxWidth().fillMaxHeight().weight(1F) + rejectButtonModifier = + if (inProgress.value) rejectButtonModifier + else rejectButtonModifier.clickable { showRejectRequestAlert(rhId, contactRequestId) } + Row( + rejectButtonModifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_close), + contentDescription = null, + tint = if (inProgress.value) MaterialTheme.colors.secondary else Color.Red, + ) + TextIconSpaced(false) + Text( + stringResource(MR.strings.reject_contact_button), + color = if (inProgress.value) MaterialTheme.colors.secondary else Color.Red + ) + } + var acceptButtonModifier = Modifier.fillMaxWidth().fillMaxHeight().weight(1F) + acceptButtonModifier = + if (inProgress.value) acceptButtonModifier + else + acceptButtonModifier.clickable { + if (chatModel.addressShortLinkDataSet()) { + acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgress) + } else { + showAcceptRequestAlert(rhId, contactRequestId, inProgress = inProgress) + } + } + Row( + acceptButtonModifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_check), + contentDescription = null, + tint = if (inProgress.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + ) + TextIconSpaced(false) + Text( + stringResource(MR.strings.accept_contact_button), + color = if (inProgress.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + } + + if (progressByTimeout) { + ComposeProgressIndicator() + } + } +} + +fun showRejectRequestAlert(rhId: Long?, contactRequestId: Long) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.reject_contact_request), + text = generalGetString(MR.strings.the_sender_will_not_be_notified), + confirmText = generalGetString(MR.strings.reject_contact_button), + onConfirm = { + AlertManager.shared.hideAlert() + rejectContactRequest(rhId, contactRequestId, chatModel, dismissToChatList = true) + }, + destructive = true, + hostDevice = hostDevice(rhId), + ) +} + +fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long, inProgress: MutableState) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.accept_contact_request), + buttons = { + Column { + // Accept + SectionItemView({ + AlertManager.shared.hideAlert() + acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgress) + }) { + Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + // Accept incognito + SectionItemView({ + AlertManager.shared.hideAlert() + acceptContactRequest(rhId, incognito = true, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgress) + }) { + Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + }, + hostDevice = hostDevice(rhId), + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt new file mode 100644 index 0000000000..21799ff820 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt @@ -0,0 +1,171 @@ +package chat.simplex.common.views.chat + +import TextIconSpaced +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* + +@Composable +fun ComposeContextMemberContactActionsView( + rhId: Long?, + contact: Contact, + groupDirectInv: GroupDirectInvitation +) { + val inProgress = rememberSaveable { mutableStateOf(false) } + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + + KeyChangeEffect(chatModel.chatId.value) { + if (inProgress.value) { + inProgress.value = false + progressByTimeout = false + } + } + + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(1000) + inProgress.value + } else { + false + } + } + + Box( + Modifier.height(60.dp), + contentAlignment = Alignment.Center + ) { + Column( + Modifier + .background(MaterialTheme.colors.surface) + .alpha(if (progressByTimeout) 0.6f else 1f) + ) { + Divider() + + if (groupDirectInv.memberRemoved) { + Row( + Modifier + .fillMaxSize() + .padding(horizontal = DEFAULT_PADDING_HALF), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) + ) { + Icon(painterResource(MR.images.ic_info), contentDescription = null, tint = MaterialTheme.colors.secondary) + Text(generalGetString(MR.strings.member_is_deleted_cant_accept_request), color = MaterialTheme.colors.secondary) + } + } else { + Row( + Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + var rejectButtonModifier = Modifier.fillMaxWidth().fillMaxHeight().weight(1F) + rejectButtonModifier = + if (inProgress.value) rejectButtonModifier + else rejectButtonModifier.clickable { showRejectMemberContactRequestAlert(rhId, contact) } + Row( + rejectButtonModifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_close), + contentDescription = null, + tint = if (inProgress.value) MaterialTheme.colors.secondary else Color.Red, + ) + TextIconSpaced(false) + Text( + stringResource(MR.strings.reject_contact_button), + color = if (inProgress.value) MaterialTheme.colors.secondary else Color.Red + ) + } + var acceptButtonModifier = Modifier.fillMaxWidth().fillMaxHeight().weight(1F) + acceptButtonModifier = + if (inProgress.value) acceptButtonModifier + else acceptButtonModifier.clickable { acceptMemberContact(rhId, contact.contactId, inProgress = inProgress) } + Row( + acceptButtonModifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_check), + contentDescription = null, + tint = if (inProgress.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + ) + TextIconSpaced(false) + Text( + stringResource(MR.strings.accept_contact_button), + color = if (inProgress.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + } + } + + if (progressByTimeout) { + ComposeProgressIndicator() + } + } +} + +fun showRejectMemberContactRequestAlert(rhId: Long?, contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.reject_contact_request), + text = generalGetString(MR.strings.the_sender_will_not_be_notified), + confirmText = generalGetString(MR.strings.reject_contact_button), + onConfirm = { + AlertManager.shared.hideAlert() + deleteMemberContact(rhId, contact) + }, + destructive = true, + hostDevice = hostDevice(rhId), + ) +} + +private fun deleteMemberContact(rhId: Long?, contact: Contact) { + withBGApi { + chatModel.controller.apiDeleteContact(rhId, contact.contactId, chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false)) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, contact.id) + chatModel.chatId.value = null + } + } +} + +fun acceptMemberContact( + rhId: Long?, + contactId: Long, + close: ((chat: Chat) -> Unit)? = null, + inProgress: MutableState? = null +) { + withBGApi { + inProgress?.value = true + val contact = chatModel.controller.apiAcceptMemberContact(rhId, contactId) + if (contact != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, contact) + inProgress?.value = false + } + val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) + close?.invoke(chat) + } else { + inProgress?.value = 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 deleted file mode 100644 index bc82bc593f..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt +++ /dev/null @@ -1,39 +0,0 @@ -package chat.simplex.common.views.chat - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.generalGetString -import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource - -@Composable -fun ComposeContextInvitingContactMemberView() { - val sentColor = MaterialTheme.appColors.sentMessage - Row( - Modifier - .height(60.dp) - .fillMaxWidth() - .padding(top = 8.dp) - .background(sentColor), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painterResource(MR.images.ic_chat), - stringResource(MR.strings.button_send_direct_message), - modifier = Modifier - .padding(start = 12.dp, end = 8.dp) - .height(20.dp) - .width(20.dp), - tint = MaterialTheme.colors.secondary - ) - Text(generalGetString(MR.strings.compose_send_direct_message_to_connect)) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt new file mode 100644 index 0000000000..be82a5d2d3 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt @@ -0,0 +1,126 @@ +package chat.simplex.common.views.chat + +import SectionItemView +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.chat.group.removeMember +import chat.simplex.common.views.chat.group.removeMemberDialog +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun ComposeContextPendingMemberActionsView( + rhId: Long?, + groupInfo: GroupInfo, + member: GroupMember +) { + Column( + Modifier + .height(60.dp) + .background(MaterialTheme.colors.surface) + ) { + Divider() + + Row( + Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1F) + .clickable { + rejectMemberDialog(rhId, groupInfo, member, chatModel, close = { ModalManager.end.closeModal() }) + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(MR.strings.reject_pending_member_button), color = Color.Red) + } + + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1F) + .clickable { + acceptMemberDialog(rhId, groupInfo, member, close = { ModalManager.end.closeModal() }) + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(MR.strings.accept_pending_member_button), color = MaterialTheme.colors.primary) + } + } + } +} + +fun rejectMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.reject_pending_member_alert_title), + confirmText = generalGetString(MR.strings.reject_pending_member_button), + onConfirm = { + removeMember(rhId, groupInfo, member, withMessages = false, chatModel, close) + }, + destructive = true, + ) +} + +fun acceptMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.accept_pending_member_alert_title), + text = generalGetString(MR.strings.accept_pending_member_alert_question), + buttons = { + Column { + // Accept as member + SectionItemView({ + AlertManager.shared.hideAlert() + acceptMember(rhId, groupInfo, member, GroupMemberRole.Member, close) + }) { + Text(generalGetString(MR.strings.accept_pending_member_alert_confirmation_as_member), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + // Accept as observer + SectionItemView({ + AlertManager.shared.hideAlert() + acceptMember(rhId, groupInfo, member, GroupMemberRole.Observer, close) + }) { + Text(generalGetString(MR.strings.accept_pending_member_alert_confirmation_as_observer), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +private fun acceptMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, role: GroupMemberRole, close: (() -> Unit)?) { + withBGApi { + val r = chatModel.controller.apiAcceptMember(rhId, groupInfo.groupId, member.groupMemberId, role) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second) + chatModel.chatsContext.updateGroup(rhId, r.first) + } + } + close?.invoke() + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextProfilePickerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextProfilePickerView.kt new file mode 100644 index 0000000000..ce023e83c9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextProfilePickerView.kt @@ -0,0 +1,302 @@ +package chat.simplex.common.views.chat + +import TextIconSpaced +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +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.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.IncognitoOptionImage +import chat.simplex.common.views.usersettings.IncognitoView +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +private val USER_ROW_AVATAR_SIZE = 42.dp +private val USER_ROW_VERTICAL_PADDING = 8.dp +private val USER_PICKER_ROW_SIZE = USER_ROW_AVATAR_SIZE + (USER_ROW_VERTICAL_PADDING * 2f) +private val MAX_USER_PICKER_HEIGHT = (USER_PICKER_ROW_SIZE * 4) + (USER_ROW_AVATAR_SIZE + USER_ROW_VERTICAL_PADDING - 4.dp) + +@Composable +fun ComposeContextProfilePickerView( + rhId: Long?, + chat: Chat, + currentUser: User +) { + val selectedUser = remember { mutableStateOf(currentUser) } + val incognitoDefault = chatModel.controller.appPrefs.incognito.get() + val users = chatModel.users.map { it.user }.filter { u -> u.activeUser || !u.hidden } + val listExpanded = remember { mutableStateOf(false) } + + val maxHeightInPx = with(LocalDensity.current) { windowHeight().toPx() } + val isVisible = remember { mutableStateOf(false) } + val offsetY = remember { Animatable(maxHeightInPx) } + + LaunchedEffect(isVisible.value) { + if (isVisible.value) { + offsetY.animateTo( + targetValue = 0f, + animationSpec = contextUserPickerAnimSpec() + ) + } + } + + @Composable + fun ExpandCollapseChevron() { + if (listExpanded.value) { + Icon( + painterResource( + MR.images.ic_chevron_down + ), + contentDescription = null, + Modifier.size(20.dp), + tint = MaterialTheme.colors.secondary, + ) + } else if (!chat.chatInfo.profileChangeProhibited) { + Icon( + painterResource( + MR.images.ic_chevron_up + ), + contentDescription = null, + Modifier.size(20.dp), + tint = MaterialTheme.colors.secondary, + ) + } + } + + fun changeProfile(newUser: User) { + withApi { + if (chat.chatInfo is ChatInfo.Direct) { + val updatedContact = chatModel.controller.apiChangePreparedContactUser(rhId, chat.chatInfo.contact.contactId, newUser.userId) + if (updatedContact != null) { + selectedUser.value = newUser + chatModel.controller.appPrefs.incognito.set(false) + listExpanded.value = false + chatModel.chatsContext.updateContact(rhId, updatedContact) + } + } else if (chat.chatInfo is ChatInfo.Group) { + val updatedGroup = chatModel.controller.apiChangePreparedGroupUser(rhId, chat.chatInfo.groupInfo.groupId, newUser.userId) + if (updatedGroup != null) { + selectedUser.value = newUser + chatModel.controller.appPrefs.incognito.set(false) + listExpanded.value = false + chatModel.chatsContext.updateGroup(rhId, updatedGroup) + } + } + chatModel.controller.changeActiveUser_( + rhId = newUser.remoteHostId, + toUserId = newUser.userId, + viewPwd = null, + keepingChatId = chat.id + ) + if (chatModel.currentUser.value?.userId != newUser.userId) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.switching_profile_error_title), + String.format(generalGetString(MR.strings.switching_profile_error_message), newUser.chatViewName) + ) + } + } + } + + fun showCantChangeProfileAlert() { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.context_user_picker_cant_change_profile_alert_title), + generalGetString(MR.strings.context_user_picker_cant_change_profile_alert_message) + ) + } + + @Composable + fun ProfilePickerUserOption(user: User) { + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp) + .clickable(onClick = { + if (!chat.chatInfo.profileChangeProhibited) { + if (selectedUser.value.userId == user.userId) { + if (!incognitoDefault) { + listExpanded.value = !listExpanded.value + } else { + chatModel.controller.appPrefs.incognito.set(false) + listExpanded.value = false + } + } else { + changeProfile(user) + } + } else { + showCantChangeProfileAlert() + } + }) + .padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + ProfileImage(size = USER_ROW_AVATAR_SIZE, image = user.image) + TextIconSpaced(false) + Text( + user.chatViewName, + modifier = Modifier.align(Alignment.CenterVertically), + fontWeight = if (selectedUser.value.userId == user.userId && !incognitoDefault) FontWeight.Medium else FontWeight.Normal + ) + + Spacer(Modifier.weight(1f)) + + if (selectedUser.value.userId == user.userId && !incognitoDefault) { + ExpandCollapseChevron() + } + } + } + + @Composable + fun IncognitoOption() { + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp) + .clickable(onClick = { + if (!chat.chatInfo.profileChangeProhibited) { + if (incognitoDefault) { + listExpanded.value = !listExpanded.value + } else { + chatModel.controller.appPrefs.incognito.set(true) + listExpanded.value = false + } + } else { + showCantChangeProfileAlert() + } + }) + .padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IncognitoOptionImage() + TextIconSpaced(false) + Text( + stringResource(MR.strings.incognito), + modifier = Modifier.align(Alignment.CenterVertically), + fontWeight = if (incognitoDefault) FontWeight.Medium else FontWeight.Normal + ) + Spacer(Modifier.padding(6.dp)) + Column(Modifier + .size(48.dp) + .clip(CircleShape) + .clickable( + onClick = { + if (ModalManager.end.isLastModalOpen(ModalViewId.CONTEXT_USER_PICKER_INCOGNITO)) { + ModalManager.end.closeModal() + } else { + ModalManager.end.showModal(id = ModalViewId.CONTEXT_USER_PICKER_INCOGNITO) { 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 (incognitoDefault) { + ExpandCollapseChevron() + } + } + } + + @Composable + fun ProfilePicker() { + LazyColumnWithScrollBarNoAppBar( + Modifier + .heightIn(max = MAX_USER_PICKER_HEIGHT) + .background(MaterialTheme.colors.surface), + reverseLayout = true, + maxHeight = remember { mutableStateOf(MAX_USER_PICKER_HEIGHT) }, + containerAlignment = Alignment.BottomEnd + ) { + val otherUsers = users.filter { u -> u.userId != selectedUser.value.userId }.sortedByDescending { it.activeOrder } + + if (incognitoDefault) { + item { + IncognitoOption() + Divider( + Modifier.padding( + start = DEFAULT_PADDING_HALF, + end = DEFAULT_PADDING_HALF, + ) + ) + ProfilePickerUserOption(selectedUser.value) + } + } else { + item { + ProfilePickerUserOption(selectedUser.value) + Divider( + Modifier.padding( + start = DEFAULT_PADDING_HALF, + end = DEFAULT_PADDING_HALF, + ) + ) + IncognitoOption() + } + } + + items(otherUsers, key = { it.userId }) { user -> + Divider( + Modifier.padding( + start = DEFAULT_PADDING_HALF, + end = DEFAULT_PADDING_HALF, + ) + ) + ProfilePickerUserOption(user) + } + } + } + + @Composable + fun CurrentSelection() { + Column( + Modifier + .background(MaterialTheme.colors.surface), + ) { + Text( + generalGetString(MR.strings.context_user_picker_your_profile), + Modifier.padding(horizontal = 14.dp).padding(top = 8.dp), + color = MaterialTheme.colors.secondary + ) + + if (chat.chatInfo.profileChangeProhibited) { + if (chat.chatInfo.incognito) { + IncognitoOption() + } else { + ProfilePickerUserOption(selectedUser.value) + } + } else if (incognitoDefault) { + IncognitoOption() + } else { + ProfilePickerUserOption(selectedUser.value) + } + } + } + + if (!listExpanded.value || chat.chatInfo.profileChangeProhibited) { + CurrentSelection() + } else { + ProfilePicker() + } +} 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 c413e06599..af4baad90f 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,7 +1,8 @@ -@file:UseSerializers(UriSerializer::class) +@file:UseSerializers(UriSerializer::class, ComposeMessageSerializer::class) package chat.simplex.common.views.chat import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* @@ -11,31 +12,40 @@ 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.layout.onSizeChanged +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextDecoration import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.* 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.filesToDelete -import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource 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 +import kotlin.math.min + +const val MAX_NUMBER_OF_MENTIONS = 3 @Serializable sealed class ComposePreview { @@ -63,23 +73,58 @@ 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 progressByTimeout: Boolean = false, + 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) { @@ -100,7 +145,7 @@ data class ComposeState( get() = when (contextItem) { is ComposeContextItem.ReportedItem -> { when (contextItem.reason) { - is ReportReason.Other -> message.isNotEmpty() + is ReportReason.Other -> message.text.isNotEmpty() else -> true } } @@ -112,12 +157,12 @@ data class ComposeState( is ComposePreview.MediaPreview -> true is ComposePreview.VoicePreview -> true is ComposePreview.FilePreview -> true - else -> message.isNotEmpty() || forwarding || liveMessage != null || submittingValidReport + else -> !whitespaceOnly || 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() = @@ -160,7 +205,10 @@ data class ComposeState( } val empty: Boolean - get() = message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem + get() = whitespaceOnly && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem + + val whitespaceOnly: Boolean + get() = message.text.all { it.isWhitespace() } companion object { fun saver(): Saver, *> = Saver( @@ -170,6 +218,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) @@ -194,6 +254,7 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview { 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.MCChat -> ComposePreview.NoPreview is MsgContent.MCUnknown, null -> ComposePreview.NoPreview } } @@ -223,7 +284,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( @@ -276,41 +337,48 @@ 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)) } } @Composable fun ComposeView( + rhId: Long?, chatModel: ChatModel, + chatsCtx: ChatModel.ChatsContext, chat: Chat, composeState: MutableState, + showCommandsMenu: 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 - val link = parsedMsg.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) } + fun getMessageLinks(parsedMsg: List?): Pair { + if (parsedMsg == null) return null to false val simplexLink = parsedMsg.any { ft -> ft.format is Format.SimplexLink } - return link?.text to simplexLink + for (ft in parsedMsg) { + val link = ft.linkUri + if (link != null && !cancelledLinks.contains(link) && !isSimplexLink(link)) { + return link to simplexLink + } + } + return null to simplexLink } val linkUrl = rememberSaveable { mutableStateOf(null) } // default value parsed because of draft - val hasSimplexLink = rememberSaveable { mutableStateOf(parseMessage(composeState.value.message).second) } + val hasSimplexLink = rememberSaveable { mutableStateOf(getMessageLinks(parseToMarkdown(composeState.value.message.text)).second) } val prevLinkUrl = rememberSaveable { mutableStateOf(null) } val pendingLinkUrl = rememberSaveable { mutableStateOf(null) } - val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() + val useLinkPreviews = true val saveLastDraft = chatModel.controller.appPrefs.privacySaveLastDraft.get() val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } val recState: MutableState = 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) { @@ -320,6 +388,7 @@ fun ComposeView( if (wait != null) delay(wait) val lp = getLinkPreview(url) if (lp != null && pendingLinkUrl.value == url) { + chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) // to avoid showing alert to current users, show alert in v6.5 composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp)) pendingLinkUrl.value = null } else if (pendingLinkUrl.value == url) { @@ -330,11 +399,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 = getMessageLinks(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) { @@ -369,7 +438,7 @@ fun ComposeView( fun clearState(live: Boolean = false) { if (live) { - composeState.value = composeState.value.copy(inProgress = false) + composeState.value = composeState.value.copy(inProgress = false, progressByTimeout = false) } else { composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) resetLinkPreview() @@ -403,27 +472,28 @@ 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 chatItems = if (chat.chatInfo.chatType == ChatType.Local) chatModel.controller.apiCreateChatItems( rh = chat.remoteHostId, noteFolderId = chat.chatInfo.apiId, - composedMessages = listOf(ComposedMessage(file, null, mc)) + composedMessages = listOf(ComposedMessage(file, null, mc, mentions)) ) else chatModel.controller.apiSendMessages( rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, + scope = cInfo.groupChatScope(), live = live, ttl = ttl, - composedMessages = listOf(ComposedMessage(file, quoted, mc)) + composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions)) ) if (!chatItems.isNullOrEmpty()) { chatItems.forEach { aChatItem -> - withChats { - addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + withContext(Dispatchers.Main) { + chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) } } return chatItems.first().chatItem @@ -432,31 +502,126 @@ fun ComposeView( return null } + // TODO [short links] connectCheckLinkPreview + fun checkLinkPreview(): MsgContent { + val msgText = composeState.value.message.text + return when (val composePreview = composeState.value.preview) { + is ComposePreview.CLinkPreview -> { + val parsedMsg = parseToMarkdown(msgText) + val url = getMessageLinks(parsedMsg).first + val lp = composePreview.linkPreview + if (lp != null && url == lp.uri) { + MsgContent.MCLink(msgText, preview = lp) + } else { + MsgContent.MCText(msgText) + } + } + + else -> MsgContent.MCText(msgText) + } + } + + fun sending() { + composeState.value = composeState.value.copy(inProgress = true) + } + + suspend fun sendMemberContactInvitation() { + val mc = checkLinkPreview() + sending() + val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) + if (contact != null) { + withContext(Dispatchers.Main) { + chatsCtx.updateContact(chat.remoteHostId, contact) + clearState() + } + } else { + composeState.value = composeState.value.copy(inProgress = false) + } + } + + suspend fun sendConnectPreparedContact() { + val mc = checkLinkPreview() + sending() + val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get() + val contact = chatModel.controller.apiConnectPreparedContact( + rh = chat.remoteHostId, + contactId = chat.chatInfo.apiId, + incognito = incognito, + msg = mc + ) + if (contact != null) { + withContext(Dispatchers.Main) { + chatsCtx.updateContact(chat.remoteHostId, contact) + clearState() + } + } else { + composeState.value = composeState.value.copy(inProgress = false) + } + } + + fun showSendConnectPreparedContactAlert() { + val empty = composeState.value.whitespaceOnly + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.compose_view_send_contact_request_alert_question), + text = generalGetString(MR.strings.compose_view_send_contact_request_alert_text), + confirmText = ( + if (empty) + generalGetString(MR.strings.compose_view_send_request_without_message) + else + generalGetString(MR.strings.compose_view_send_request) + ), + onConfirm = { withApi { sendConnectPreparedContact() } }, + dismissText = ( + if (empty) + generalGetString(MR.strings.compose_view_add_message) + else + generalGetString(MR.strings.cancel_verb) + ) + ) + } + + suspend fun connectPreparedGroup() { + val mc = checkLinkPreview() + sending() + val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get() + val groupInfo = chatModel.controller.apiConnectPreparedGroup( + rh = chat.remoteHostId, + groupId = chat.chatInfo.apiId, + incognito = incognito, + msg = mc + ) + if (groupInfo != null) { + withContext(Dispatchers.Main) { + chatsCtx.updateGroup(chat.remoteHostId, groupInfo) + clearState() + } + } else { + composeState.value = composeState.value.copy(inProgress = false) + } + } + suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List? { - val cInfo = chat.chatInfo val cs = composeState.value var sent: List? var lastMessageFailedToSend: ComposeState? = null - val msgText = text ?: cs.message - - fun sending() { - composeState.value = composeState.value.copy(inProgress = true) - } + val msgText = text ?: cs.message.text 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, + toScope = chat.chatInfo.groupChatScope(), fromChatType = fromChatInfo.chatType, fromChatId = fromChatInfo.apiId, + fromScope = fromChatInfo.groupChatScope(), itemIds = forwardedItem.map { it.id }, ttl = ttl ) - withChats { + withContext(Dispatchers.Main) { chatItems?.forEach { chatItem -> - addChatItem(rhId, chat.chatInfo, chatItem) + chatsCtx.addChatItem(rhId, chat.chatInfo, chatItem) } } @@ -470,22 +635,6 @@ fun ComposeView( return chatItems } - fun checkLinkPreview(): MsgContent { - return when (val composePreview = cs.preview) { - is ComposePreview.CLinkPreview -> { - val url = parseMessage(msgText).first - val lp = composePreview.linkPreview - if (lp != null && url == lp.uri) { - MsgContent.MCLink(msgText, preview = lp) - } else { - MsgContent.MCText(msgText) - } - } - - else -> MsgContent.MCText(msgText) - } - } - fun constructFailedMessage(cs: ComposeState): ComposeState { val preview = when (cs.preview) { is ComposePreview.MediaPreview -> { @@ -508,31 +657,28 @@ fun ComposeView( is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration) is MsgContent.MCFile -> MsgContent.MCFile(msgText) is MsgContent.MCReport -> MsgContent.MCReport(msgText, reason = msgContent.reason) + // TODO [short links] update chat link + is MsgContent.MCChat -> MsgContent.MCChat(msgText, chatLink = msgContent.chatLink) is MsgContent.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) { - withChats { - cItems.forEach { chatItem -> - addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem) - } - } - } - - return cItems?.map { it.chatItem } + fun showReportsInSupportChatAlert() { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.report_sent_alert_title), + text = generalGetString(MR.strings.report_sent_alert_msg_view_in_support_chat), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showReportsInSupportChatAlert.set(false) + }, + ) } - suspend fun sendMemberContactInvitation() { - val mc = checkLinkPreview() - val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) - if (contact != null) { - withChats { - updateContact(chat.remoteHostId, contact) - } - } + suspend fun sendReport(reportReason: ReportReason, chatItemId: Long): List? { + val cItems = chatModel.controller.apiReportMessage(chat.remoteHostId, chat.chatInfo.apiId, chatItemId, reportReason, msgText) + if (chatModel.controller.appPrefs.showReportsInSupportChatAlert.get()) showReportsInSupportChatAlert() + return cItems?.map { it.chatItem } } suspend fun updateMessage(ei: ChatItem, chat: Chat, live: Boolean): ChatItem? { @@ -543,12 +689,15 @@ fun ComposeView( rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, + scope = cInfo.groupChatScope(), itemId = ei.meta.itemId, - mc = updateMsgContent(oldMsgContent), + updatedMessage = UpdatedMessage(updateMsgContent(oldMsgContent), cs.memberMentions), live = live ) - if (updatedItem != null) withChats { - upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + if (updatedItem != null) { + withContext(Dispatchers.Main) { + chatsCtx.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + } } return updatedItem?.chatItem } @@ -564,18 +713,15 @@ fun ComposeView( clearCurrentDraft() } - if (chat.nextSendGrpInv) { - sendMemberContactInvitation() - sent = null - } else if (cs.contextItem is ComposeContextItem.ForwardingItems) { + if (cs.contextItem is ComposeContextItem.ForwardingItems) { sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItems, cs.contextItem.fromChatInfo, ttl = ttl) if (sent == null) { lastMessageFailedToSend = constructFailedMessage(cs) } - if (cs.message.isNotEmpty()) { + if (cs.message.text.isNotEmpty()) { sent?.mapIndexed { index, message -> if (index == sent!!.lastIndex) { - send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl) + send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl, mentions = cs.memberMentions) } else { message } @@ -686,7 +832,8 @@ fun ComposeView( } 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 ) sent = if (sendResult != null) listOf(sendResult) else null if (sent == null && index == msgs.lastIndex && cs.liveMessage == null) { @@ -719,23 +866,70 @@ 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 sanitizeMessage(parsedMsg: List): Triple, Int?> { + var pos = 0 + var updatedMsg = "" + var sanitizedPos: Int? = null + val updatedParsedMsg = parsedMsg.map { ft -> + var updated = ft + when(ft.format) { + is Format.Uri -> { + val sanitized = parseSanitizeUri(ft.text, safe = true)?.uriInfo?.sanitized + if (sanitized != null) { + updated = FormattedText(text = sanitized, format = Format.Uri()) + pos += updated.text.count() + sanitizedPos = pos + } + } + is Format.HyperLink -> { + val sanitized = parseSanitizeUri(ft.format.linkUri, safe = true)?.uriInfo?.sanitized + if (sanitized != null) { + val updatedText = if (ft.format.showText == null) sanitized else "[${ft.format.showText}]($sanitized)" + updated = FormattedText(text = updatedText, format = Format.HyperLink(showText = ft.format.showText, linkUri = sanitized)) + pos += updated.text.count() + sanitizedPos = pos + } + } + else -> + pos += ft.text.count() + } + updatedMsg += updated.text + updated + } + return Triple(updatedMsg, updatedParsedMsg, sanitizedPos) + } + + fun onMessageChange(s: ComposeMessage) { + var parsedMessage = parseToMarkdown(s.text) + if (chatModel.controller.appPrefs.privacySanitizeLinks.get() && parsedMessage != null) { + val (updatedMsg, updatedParsedMsg, sanitizedPos) = sanitizeMessage(parsedMessage) + if (sanitizedPos == null) { + composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage) + } else { + val pos = min(updatedMsg.count(), if (sanitizedPos < s.selection.start) s.selection.start else sanitizedPos) + val message = s.copy(text = updatedMsg, selection = TextRange(pos)) + composeState.value = composeState.value.copy(message = message, parsedMessage = updatedParsedMsg) + parsedMessage = updatedParsedMsg + } + } else { + composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage ?: FormattedText.plain(s.text)) + } + if (isShortEmoji(s.text)) { + textStyle.value = if (s.text.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont } else { textStyle.value = smallFont - if (composeState.value.linkPreviewAllowed) { - if (s.isNotEmpty()) { - showLinkPreview(s) + if (composeState.value.linkPreviewAllowed && chatModel.controller.appPrefs.privacyLinkPreviews.get()) { + if (s.text.isNotEmpty()) { + showLinkPreview(parsedMessage) } else { resetLinkPreview() hasSimplexLink.value = false + composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) } - } else if (s.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)) { - hasSimplexLink.value = parseMessage(s).second } else { - hasSimplexLink.value = false + resetLinkPreview() + hasSimplexLink.value = s.text.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) && getMessageLinks(parsedMessage).second + if (composeState.value.linkPreviewAllowed) composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) } } } @@ -801,7 +995,7 @@ 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.isNullOrEmpty()) { @@ -822,21 +1016,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.isNullOrEmpty()) { - composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg, sentMsg = sentMsg, sent = true)) + 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.chatItemsForContent(null).value.findLast { it.meta.editable } + val lastEditable = chatsCtx.chatItems.value.findLast { it.meta.editable } if (lastEditable != null) { composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) } @@ -904,21 +1098,311 @@ fun ComposeView( fun contextItemView() { when (val contextItem = composeState.value.contextItem) { ComposeContextItem.NoContextItem -> {} - is ComposeContextItem.QuotedItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_reply), chatType = chat.chatInfo.chatType) { + 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(listOf(contextItem.chatItem), painterResource(MR.images.ic_edit_filled), chatType = chat.chatInfo.chatType) { + is ComposeContextItem.EditingItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_edit_filled), chatInfo = chat.chatInfo) { clearState() } - is ComposeContextItem.ForwardingItems -> ContextItemView(contextItem.chatItems, painterResource(MR.images.ic_forward), showSender = false, chatType = chat.chatInfo.chatType) { + 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), chatType = chat.chatInfo.chatType, contextIconColor = Color.Red) { + 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) } } } + val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) + val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason) + val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) + + @Composable + fun CommandsButton() { + val commandsEnabled = chat.chatInfo.sendMsgEnabled && chat.chatInfo.menuCommands.isNotEmpty() + IconButton( + onClick = { showCommandsMenu.value = !showCommandsMenu.value }, + enabled = commandsEnabled + ) { + Box( + modifier = Modifier.size(28.dp).clip(CircleShape), + contentAlignment = Alignment.Center + ) { + Text("//", style = MaterialTheme.typography.h3.copy(fontStyle = FontStyle.Italic, color = if (commandsEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary)) + } + } + } + + @Composable + fun AttachmentButton() { + val isGroupAndProhibitedFiles = + chatsCtx.secondaryContextFilter == null + && 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, + 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) + ) + } + } + + @Composable + fun AttachmentAndCommandsButtons() { + val cInfo = chat.chatInfo + Row( + Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), + horizontalArrangement = Arrangement.spacedBy((-8).dp) + ) { + val msg = composeState.value.message.text.trim() + val showAttachment = cInfo !is ChatInfo.Direct || cInfo.contact.profile.peerType != ChatPeerType.Bot || cInfo.featureEnabled(ChatFeature.Files) + if (cInfo.useCommands && (!showAttachment || msg.isEmpty() || msg.startsWith("/"))) { + CommandsButton() + } + if (showAttachment) { + AttachmentButton() + } + } + } + + val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) } + LaunchedEffect(allowedVoiceByPrefs) { + if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) { + // 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) + composeState.value = cs.copy(inProgress = false, progressByTimeout = false) + } 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 + } + } + } + } + + @Composable + fun SendMsgView_( + disableSendButton: Boolean, + placeholder: String? = null, + sendToConnect: (() -> Unit)? = null + ) { + 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 && !disableSendButton, + sendToConnect = sendToConnect, + hideSendButton = chat.chatInfo.nextConnect && !nextSendGrpInv.value && composeState.value.whitespaceOnly, + nextConnect = chat.chatInfo.nextConnect, + needToAllowVoiceToContact = needToAllowVoiceToContact, + allowedVoiceByPrefs = allowedVoiceByPrefs, + allowVoiceToContact = ::allowVoiceToContact, + sendButtonColor = sendButtonColor, + timedMessageAllowed = timedMessageAllowed, + customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, + placeholder = 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, + ) + } + + @Composable + fun SendContactRequestView( + disableSendButton: Boolean, + icon: ImageResource, + sendRequest: () -> Unit + ) { + Row( + Modifier.padding(horizontal = DEFAULT_PADDING_HALF), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + Modifier.weight(1f) + ) { + SendMsgView_( + disableSendButton = disableSendButton, + placeholder = generalGetString(MR.strings.compose_view_add_message), + sendToConnect = sendRequest + ) + } + if (composeState.value.whitespaceOnly) { + SimpleButtonIconEnded( + text = stringResource(MR.strings.compose_view_connect), + icon = painterResource(icon), + style = MaterialTheme.typography.body2, + color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + disabled = composeState.value.inProgress, + click = { withApi { sendRequest() } } + ) + } + } + } + + @Composable + fun ConnectButtonView( + text: String, + icon: ImageResource, + connect: () -> Unit + ) { + var modifier = Modifier.height(60.dp).fillMaxWidth() + modifier = if (composeState.value.inProgress) modifier else modifier.clickable(onClick = { connect() }) + Box( + modifier, + contentAlignment = Alignment.Center + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(icon), + contentDescription = null, + tint = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + Text( + text, + style = MaterialTheme.typography.body2, + color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + if (composeState.value.progressByTimeout) { + Box( + Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING_HALF), + contentAlignment = Alignment.CenterEnd + ) { + ComposeProgressIndicator() + } + } + } + } + + @Composable + fun ContextSendMessageToConnect(s: String) { + Row( + Modifier + .height(60.dp) + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_chat), + contentDescription = null, + modifier = Modifier + .padding(end = 8.dp) + .height(20.dp) + .width(20.dp), + tint = MaterialTheme.colors.secondary + ) + Text(s) + } + } + // In case a user sent something, state is in progress, the user rotates a screen to different orientation. // Without clearing the state the user will be unable to send anything until re-enters ChatView LaunchedEffect(Unit) { @@ -932,7 +1416,7 @@ 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( @@ -944,22 +1428,51 @@ fun ComposeView( chatModel.sharedContent.value = null } - val userCanSend = rememberUpdatedState(chat.chatInfo.userCanSend) - val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) - val userIsObserver = rememberUpdatedState(chat.userIsObserver) - val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) + LaunchedEffect(composeState.value.inProgress) { + val newProgressByTimeout = if (composeState.value.inProgress) { + delay(1000) + composeState.value.inProgress + } else { + false + } + composeState.value = composeState.value.copy(progressByTimeout = newProgressByTimeout) + } Column { - if (nextSendGrpInv.value) { - ComposeContextInvitingContactMemberView() + val currentUser = chatModel.currentUser.value + if (chat.chatInfo.nextConnectPrepared && currentUser != null) { + ComposeContextProfilePickerView( + rhId = rhId, + chat = chat, + currentUser = currentUser + ) } + + if ( + chat.chatInfo is ChatInfo.Group + && chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext + && chatsCtx.secondaryContextFilter.groupScopeInfo is GroupChatScopeInfo.MemberSupport + && chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ != null + && chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_.memberPending + && composeState.value.contextItem == ComposeContextItem.NoContextItem + && composeState.value.preview == ComposePreview.NoPreview + ) { + ComposeContextPendingMemberActionsView( + rhId = rhId, + groupInfo = chat.chatInfo.groupInfo, + member = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ + ) + } + 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 simplexLinkProhibited = chatsCtx.secondaryContextFilter == null && hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) + val fileProhibited = chatsCtx.secondaryContextFilter == null && composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files) val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice) + val disableSendButton = simplexLinkProhibited || fileProhibited || voiceProhibited if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) { if (simplexLinkProhibited) { MsgNotAllowedView(generalGetString(MR.strings.simplex_links_not_allowed), icon = painterResource(MR.images.ic_link)) @@ -984,152 +1497,86 @@ fun ComposeView( } } } + 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) - ) - } + if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) { + if (chat.chatInfo.groupInfo.businessChat == null) { + ConnectButtonView( + text = stringResource(MR.strings.compose_view_join_group), + icon = MR.images.ic_group_filled, + connect = { withApi { connectPreparedGroup() } } + ) } else { - showChooseAttachment - } - val attachmentEnabled = - !composeState.value.attachmentDisabled - && sendMsgEnabled.value - && userCanSend.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) + SendContactRequestView( + disableSendButton = disableSendButton, + icon = MR.images.ic_work_filled, + sendRequest = { withApi { connectPreparedGroup() } } ) } - 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() + } else if (nextSendGrpInv.value) { + Column { + ContextSendMessageToConnect(generalGetString(MR.strings.compose_send_direct_message_to_connect)) + Divider() + Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { + AttachmentAndCommandsButtons() + SendMsgView_( + disableSendButton = disableSendButton, + sendToConnect = { withApi { sendMemberContactInvitation() } } + ) } } - 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 -> {} - } + } else if ( + chat.chatInfo is ChatInfo.Direct + && chat.chatInfo.contact.nextConnectPrepared + && chat.chatInfo.contact.preparedContact != null + ) { + when (chat.chatInfo.contact.preparedContact.uiConnLinkType) { + ConnectionMode.Inv -> + ConnectButtonView( + text = stringResource(MR.strings.compose_view_connect), + icon = MR.images.ic_person_add_filled, + connect = { withApi { sendConnectPreparedContact() } } + ) + ConnectionMode.Con -> + if (chat.chatInfo.contact.isBot) { + ConnectButtonView( + text = stringResource(MR.strings.compose_view_connect), + icon = MR.images.ic_bolt_filled, + connect = { withApi { sendConnectPreparedContact() } } + ) + } else { + SendContactRequestView( + disableSendButton = disableSendButton, + icon = MR.images.ic_person_add_filled, + sendRequest = { showSendConnectPreparedContactAlert() } + ) } } - - LaunchedEffect(rememberUpdatedState(chat.chatInfo.userCanSend).value) { - if (!chat.chatInfo.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 (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, - 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, - 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 + } else if ( + chat.chatInfo is ChatInfo.Direct + && chat.chatInfo.contact.nextAcceptContactRequest + && chat.chatInfo.contact.contactRequestId != null + ) { + ComposeContextContactRequestActionsView( + rhId = rhId, + contactRequestId = chat.chatInfo.contact.contactRequestId ) + } else if ( + chat.chatInfo is ChatInfo.Direct + && chat.chatInfo.contact.nextAcceptContactRequest + && chat.chatInfo.contact.groupDirectInv != null + ) { + ComposeContextMemberContactActionsView( + rhId = rhId, + contact = chat.chatInfo.contact, + groupDirectInv = chat.chatInfo.contact.groupDirectInv + ) + } else { + Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { + AttachmentAndCommandsButtons() + SendMsgView_(disableSendButton = disableSendButton) + } } } } 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 b1e9bf750e..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,16 +12,16 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.common.model.* -import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel import chat.simplex.res.MR +import kotlinx.coroutines.* @Composable fun ContactPreferencesView( @@ -41,8 +41,8 @@ fun ContactPreferencesView( val prefs = contactFeaturesAllowedToPrefs(featuresAllowed) val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs) if (toContact != null) { - withChats { - updateContact(rhId, toContact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, toContact) currentFeaturesAllowed = featuresAllowed } } 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 1657a1f0b7..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 @@ -31,7 +31,7 @@ fun ContextItemView( contextItems: List, contextIcon: Painter, showSender: Boolean = true, - chatType: ChatType, + chatInfo: ChatInfo, contextIconColor: Color = MaterialTheme.colors.secondary, cancelContextItem: () -> Unit, ) { @@ -64,6 +64,11 @@ 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 + } ) } @@ -126,7 +131,7 @@ fun ContextItemView( ContextMsgPreview(contextItem, lines = 3) } } else if (contextItems.isNotEmpty()) { - Text(String.format(generalGetString(if (chatType == ChatType.Local) MR.strings.compose_save_messages_n else MR.strings.compose_forward_messages_n), contextItems.count()), fontStyle = FontStyle.Italic) + 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) { @@ -147,7 +152,7 @@ fun PreviewContextItemView() { ContextItemView( contextItems = listOf(ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello")), contextIcon = painterResource(MR.images.ic_edit_filled), - chatType = ChatType.Direct + chatInfo = Chat.sampleData.chatInfo ) {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt index e449831ee0..03d0b99854 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -7,12 +7,12 @@ 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.model.ChatController.appPrefs import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.chatModel import chat.simplex.common.views.helpers.* @@ -21,41 +21,55 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @Composable -fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState?>, onTop: Boolean) { - val onBackClicked = { selectedChatItems.value = null } +fun BoxScope.SelectedItemsCounterToolbar(selectedItems: MutableState?>, onTop: Boolean, selectAll: (() -> Unit)? = null) { + val onBackClicked = { selectedItems.value = null } BackHandler(onBack = onBackClicked) - val count = selectedChatItems.value?.size ?: 0 - 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 = {}, - ) + 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 -fun SelectedItemsBottomToolbar( +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, - reversedChatItems: State>, 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) } @@ -63,7 +77,7 @@ fun SelectedItemsBottomToolbar( 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) }, {}) + ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(false) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) Row( Modifier .matchParentSize() @@ -78,7 +92,7 @@ fun SelectedItemsBottomToolbar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !deleteCountProhibited.value) { + IconButton({ if (canArchiveReports.value) archiveItems() else deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !deleteCountProhibited.value) { Icon( painterResource(MR.images.ic_delete), null, @@ -87,28 +101,29 @@ fun SelectedItemsBottomToolbar( ) } - IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value) { + IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value && chatsCtx.secondaryContextFilter == null) { Icon( painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), - tint = if (!moderateEnabled.value || deleteCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error + tint = if (!moderateEnabled.value || deleteCountProhibited.value || chatsCtx.secondaryContextFilter != null) MaterialTheme.colors.secondary else MaterialTheme.colors.error ) } - IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value) { + IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value && chatsCtx.secondaryContextFilter == null) { Icon( painterResource(MR.images.ic_forward), null, Modifier.size(22.dp), - tint = if (!forwardEnabled.value || forwardCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + tint = if (!forwardEnabled.value || forwardCountProhibited.value || chatsCtx.secondaryContextFilter != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } } Divider(Modifier.align(Alignment.TopStart)) } - LaunchedEffect(chatInfo, reversedChatItems.value, selectedChatItems.value) { - recheckItems(chatInfo, reversedChatItems.value.asReversed(), selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) + 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) } } @@ -117,6 +132,7 @@ private fun recheckItems(chatInfo: ChatInfo, selectedChatItems: MutableState?>, deleteEnabled: MutableState, deleteForEveryoneEnabled: MutableState, + canArchiveReports: MutableState, canModerate: MutableState, moderateEnabled: MutableState, forwardEnabled: MutableState, @@ -130,6 +146,7 @@ private fun recheckItems(chatInfo: ChatInfo, val selected = selectedChatItems.value ?: return var rDeleteEnabled = true var rDeleteForEveryoneEnabled = true + var rCanArchiveReports = true var rModerateEnabled = true var rOnlyOwnGroupItems = true var rForwardEnabled = true @@ -138,6 +155,7 @@ private fun recheckItems(chatInfo: ChatInfo, 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 @@ -147,10 +165,11 @@ private fun recheckItems(chatInfo: ChatInfo, 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.Admin + 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 e2b44478af..467f1e52af 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 @@ -12,9 +12,9 @@ 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.layout.onSizeChanged import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.* @@ -39,12 +39,13 @@ fun SendMsgView( isDirectChat: Boolean, liveMessageAlertShown: SharedPreference, sendMsgEnabled: Boolean, + userCantSendReason: Pair?, sendButtonEnabled: Boolean, - nextSendGrpInv: Boolean, + sendToConnect: (() -> Unit)? = null, + hideSendButton: Boolean = false, + nextConnect: Boolean, needToAllowVoiceToContact: Boolean, allowedVoiceByPrefs: Boolean, - userIsObserver: Boolean, - userCanSend: Boolean, sendButtonColor: Color = MaterialTheme.colors.primary, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, @@ -56,59 +57,62 @@ fun SendMsgView( 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) } - 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) { - progressByTimeout = if (composeState.value.inProgress) { - delay(500) - composeState.value.inProgress - } else { - false - } - } - val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && + val showVoiceButton = !nextConnect && 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 - val clicksOnTextFieldDisabled = !sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress + val clicksOnTextFieldDisabled = !sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || cs.inProgress PlatformTextField( composeState, sendMsgEnabled, + disabledText = userCantSendReason?.first, sendMsgButtonDisabled, textStyle, showDeleteTextButton, - userIsObserver, if (clicksOnTextFieldDisabled) "" else placeholder, showVoiceButton, onMessageChange, editPrevMessage, - onFilesPasted + onFilesPasted, + focusRequester ) { if (!cs.inProgress) { - sendMessage(null) + if (sendToConnect != null) { + sendToConnect() + } else { + sendMessage(null) + } } } if (clicksOnTextFieldDisabled) { - 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 (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) @@ -124,19 +128,19 @@ fun SendMsgView( } } when { - progressByTimeout -> ProgressIndicator() + cs.progressByTimeout -> ComposeProgressIndicator() cs.contextItem is ComposeContextItem.ReportedItem -> { - SendMsgButton(painterResource(MR.images.ic_check_filled), sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) + SendMsgButton(painterResource(MR.images.ic_check_filled), sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendToConnect, 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) } } @@ -152,7 +156,7 @@ fun SendMsgView( && cs.contextItem is ComposeContextItem.NoContextItem ) { Spacer(Modifier.width(12.dp)) - StartLiveMessageButton(userCanSend) { + StartLiveMessageButton { if (composeState.value.preview is ComposePreview.NoPreview) { startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) } @@ -160,7 +164,7 @@ fun SendMsgView( } } } - cs.liveMessage?.sent == false && cs.message.isEmpty() -> { + cs.liveMessage?.sent == false && cs.message.text.isEmpty() -> { CancelLiveMessageButton { cancelLiveMessage?.invoke() } @@ -174,7 +178,7 @@ fun SendMsgView( fun MenuItems(): List<@Composable () -> Unit> { val menuItems = mutableListOf<@Composable () -> Unit>() - if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) { + if (cs.liveMessage == null && !cs.editing && !nextConnect || sendMsgEnabled) { if ( cs.preview !is ComposePreview.VoicePreview && cs.contextItem is ComposeContextItem.NoContextItem && @@ -208,19 +212,21 @@ fun SendMsgView( return menuItems } - val menuItems = MenuItems() - if (menuItems.isNotEmpty()) { - SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) { showDropdown.value = true } - DefaultDropdownMenu(showDropdown) { - menuItems.forEach { composable -> composable() } + if (!hideSendButton) { + val menuItems = MenuItems() + if (menuItems.isNotEmpty()) { + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendToConnect, sendMessage) { showDropdown.value = true } + DefaultDropdownMenu(showDropdown) { + menuItems.forEach { composable -> composable() } + } + CustomDisappearingMessageDialog( + showCustomDisappearingMessageDialog, + sendMessage = sendMessage, + customDisappearingMessageTimePref = customDisappearingMessageTimePref + ) + } else { + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendToConnect, sendMessage) } - CustomDisappearingMessageDialog( - showCustomDisappearingMessageDialog, - sendMessage = sendMessage, - customDisappearingMessageTimePref = customDisappearingMessageTimePref - ) - } else { - SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) } } } @@ -280,7 +286,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) @@ -340,8 +346,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), @@ -396,8 +402,8 @@ private fun RecordVoiceButton(interactionSource: MutableInteractionSource) { } @Composable -private fun ProgressIndicator() { - CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp) +fun ComposeProgressIndicator() { + CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp) } @Composable @@ -423,6 +429,7 @@ private fun SendMsgButton( alpha: Animatable, sendButtonColor: Color, enabled: Boolean, + sendToConnect: (() -> Unit)?, sendMessage: (Int?) -> Unit, onLongClick: (() -> Unit)? = null ) { @@ -431,7 +438,13 @@ private fun SendMsgButton( Box( modifier = Modifier.requiredSize(36.dp) .combinedClickable( - onClick = { sendMessage(null) }, + onClick = { + if (sendToConnect != null) { + sendToConnect() + } else { + sendMessage(null) + } + }, onLongClick = onLongClick, enabled = enabled, role = Role.Button, @@ -457,14 +470,13 @@ 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 = ripple @@ -474,7 +486,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { 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) @@ -573,12 +585,11 @@ fun PreviewSendMsgView() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, sendButtonEnabled = true, - nextSendGrpInv = false, + nextConnect = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, placeholder = "", @@ -586,7 +597,7 @@ fun PreviewSendMsgView() { editPrevMessage = {}, onMessageChange = { _ -> }, onFilesPasted = {}, - textStyle = textStyle + textStyle = textStyle, ) } } @@ -609,12 +620,11 @@ fun PreviewSendMsgViewEditing() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, sendButtonEnabled = true, - nextSendGrpInv = false, + nextConnect = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, placeholder = "", @@ -622,7 +632,7 @@ fun PreviewSendMsgViewEditing() { editPrevMessage = {}, onMessageChange = { _ -> }, onFilesPasted = {}, - textStyle = textStyle + textStyle = textStyle, ) } } @@ -645,12 +655,11 @@ fun PreviewSendMsgViewInProgress() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, sendButtonEnabled = true, - nextSendGrpInv = false, + nextConnect = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, placeholder = "", @@ -658,7 +667,7 @@ fun PreviewSendMsgViewInProgress() { 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 e670fae5ef..91f7af2b95 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 @@ -68,7 +68,7 @@ private fun VerifyCodeLayout( } } - QRCode(connectionCode, padding = PaddingValues(vertical = DEFAULT_PADDING_HALF)) + QRCode(connectionCode, small = true, padding = PaddingValues(vertical = DEFAULT_PADDING_HALF)) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Spacer(Modifier.weight(2f)) 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 abfb3895d9..9298b600e9 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 @@ -25,8 +25,6 @@ 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.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.ChatInfoToolbarTitle import chat.simplex.common.views.helpers.* @@ -35,6 +33,7 @@ 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) { @@ -56,17 +55,27 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea GroupPreferencesView(chatModel, rhId, groupInfo.id, close) } }, + openMemberAdmission = { + ModalManager.end.showCustomModal { close -> + MemberAdmissionView( + chat.simplex.common.platform.chatModel, + rhId, + groupInfo.id, + close + ) + } + }, inviteMembers = { allowModifyMembers = false withLongRunningApi(slow = 120_000) { for (contactId in selectedContacts) { val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) if (member != null) { - withChats { - upsertGroupMember(rhId, groupInfo, member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, member) } - withReportsChatsIfOpen { - upsertGroupMember(rhId, groupInfo, member) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, member) } } else { break @@ -94,8 +103,9 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List { .asSequence() .map { it.chatInfo } .filterIsInstance() + .filter { it.sendMsgEnabled } .map { it.contact } - .filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s) + .filter { c -> !c.sendMsgToConnect && c.contactId !in memberContactIds && c.anyNameContains(s) } .sortedBy { it.displayName.lowercase() } .toList() @@ -111,6 +121,7 @@ fun AddGroupMembersLayout( allowModifyMembers: Boolean, searchText: MutableState, openPreferences: () -> Unit, + openMemberAdmission: () -> Unit, inviteMembers: () -> Unit, clearSelection: () -> Unit, addContact: (Long) -> Unit, @@ -145,7 +156,7 @@ fun AddGroupMembersLayout( horizontalArrangement = Arrangement.Center ) { ChatInfoToolbarTitle( - ChatInfo.Group(groupInfo), + ChatInfo.Group(groupInfo, groupChatScope = null), imageSize = 60.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight ) @@ -166,6 +177,9 @@ fun AddGroupMembersLayout( } else { SectionView { if (creatingGroup) { + SectionItemView(openMemberAdmission) { + Text(stringResource(MR.strings.set_member_admission)) + } SectionItemView(openPreferences) { Text(stringResource(MR.strings.set_group_preferences)) } @@ -377,6 +391,7 @@ fun PreviewAddGroupMembersLayout() { allowModifyMembers = true, searchText = remember { mutableStateOf(TextFieldValue("")) }, openPreferences = {}, + openMemberAdmission = {}, inviteMembers = {}, clearSelection = {}, addContact = {}, 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 b2963c4d4a..3f80361249 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.* @@ -30,15 +32,13 @@ 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.ChatModel.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen 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 @@ -46,9 +46,22 @@ 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 ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, scrollToItemId: MutableState, onGroupLinkUpdated: (Pair?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) { +fun ModalData.GroupChatInfoView( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chatId: String, + groupLink: GroupLink?, + selectedItems: MutableState?>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, + scrollToItemId: MutableState, + onGroupLinkUpdated: (GroupLink?) -> Unit, + close: () -> Unit, + onSearchClicked: () -> Unit +) { BackHandler(onBack = close) // TODO derivedStateOf? val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } @@ -60,6 +73,9 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin 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() + val activeSortedMembers = remember { chatModel.groupMembers }.value + .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } + .sortedByDescending { it.memberRole } GroupChatInfoLayout( chat, @@ -79,14 +95,14 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin val previousChatTTL = chatItemTTL.value chatItemTTL.value = it - setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) + setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) }, - members = remember { chatModel.groupMembers }.value - .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } - .sortedByDescending { it.memberRole }, + activeSortedMembers = activeSortedMembers, developerTools, onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) }, groupLink, + selectedItems, + appBar, scrollToItemId, addMembers = { scope.launch(Dispatchers.Default) { @@ -110,7 +126,7 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin } ModalManager.end.showModalCloseable(true) { closeCurrent -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(rhId, groupInfo, mem, stats, code, chatModel, closeCurrent) { + GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, closeCurrent) { closeCurrent() close() } @@ -124,6 +140,17 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin addOrEditWelcomeMessage = { ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, rhId, groupInfo, close) } }, + openMemberSupport = { + ModalManager.end.showCustomModal { close -> + MemberSupportView( + rhId, + chat, + groupInfo, + scrollToItemId, + close + ) + } + }, openPreferences = { ModalManager.end.showCustomModal { close -> GroupPreferencesView( @@ -138,7 +165,7 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin clearChat = { clearChatDialog(chat, close) }, leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { - ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } + ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated) } }, onSearchClicked = onSearchClicked, deletingItems = deletingItems @@ -165,8 +192,8 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl withBGApi { val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId) if (r) { - withChats { - removeChat(chat.remoteHostId, chatInfo.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(chat.remoteHostId, chatInfo.id) if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -206,25 +233,61 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe 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(messageId), - confirmText = generalGetString(MR.strings.remove_member_confirmation), - onConfirm = { - withBGApi { - val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId) - if (updatedMember != null) { - withChats { - upsertGroupMember(rhId, groupInfo, updatedMember) - } - withReportsChatsIfOpen { - upsertGroupMember(rhId, groupInfo, updatedMember) - } + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.button_remove_member_question), + generalGetString(messageId), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = false) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = true) + }) { + Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } - }, - 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.showAlertDialogButtonsColumn( + generalGetString(MR.strings.button_remove_members_question), + generalGetString(messageId), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, memberIds, withMessages = false, onSuccess = onSuccess) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, memberIds, withMessages = true, onSuccess = onSuccess) + }) { + Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + }) } @Composable @@ -258,16 +321,17 @@ fun MuteButton( chat: Chat, groupInfo: GroupInfo ) { - val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + val notificationMode = remember { mutableStateOf(groupInfo.chatSettings.enableNtfs) } + val nextNotificationMode by remember { derivedStateOf { notificationMode.value.nextMode(true) } } InfoViewActionButton( modifier = modifier, - icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), - title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), + icon = painterResource(nextNotificationMode.icon), + title = generalGetString(nextNotificationMode.text(true)), disabled = !groupInfo.ready, disabledLook = !groupInfo.ready, onClick = { - toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled.value, chatModel, ntfsEnabled) + toggleNotifications(chat.remoteHostId, chat.chatInfo, nextNotificationMode, chatModel, notificationMode) } ) } @@ -294,6 +358,40 @@ fun AddGroupMembersButton( ) } +@Composable +fun UserSupportChatButton( + chat: Chat, + groupInfo: GroupInfo, + scrollToItemId: MutableState +) { + val scope = rememberCoroutineScope() + + SettingsActionItemWithContent( + painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), + stringResource(MR.strings.button_support_chat), + click = { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) + val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo) + scope.launch { + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId = scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + }, + iconColor = (if (chat.supportUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary), + ) { + if (chat.supportUnreadCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.supportUnreadCount), + backgroundColor = MaterialTheme.colors.primary + ) + } + } +} + @Composable fun ModalData.GroupChatInfoLayout( chat: Chat, @@ -303,15 +401,18 @@ fun ModalData.GroupChatInfoLayout( setSendReceipts: (SendReceipts) -> Unit, chatItemTTL: MutableState, setChatItemTTL: (ChatItemTTL?) -> Unit, - members: List, + activeSortedMembers: List, developerTools: Boolean, onLocalAliasChanged: (String) -> Unit, - groupLink: String?, + groupLink: GroupLink?, + selectedItems: MutableState?>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, scrollToItemId: MutableState, addMembers: () -> Unit, showMemberInfo: (GroupMember) -> Unit, editGroupProfile: () -> Unit, addOrEditWelcomeMessage: () -> Unit, + openMemberSupport: () -> Unit, openPreferences: () -> Unit, deleteGroup: () -> Unit, clearChat: () -> Unit, @@ -327,20 +428,38 @@ fun ModalData.GroupChatInfoLayout( scope.launch { listState.scrollToItem(0) } } val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } } - val filteredMembers = remember(members) { + val filteredMembers = remember(activeSortedMembers) { derivedStateOf { val s = searchText.value.text.trim().lowercase() - if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) } + if (s.isEmpty()) activeSortedMembers else activeSortedMembers.filter { m -> m.anyNameContains(s) } } } Box { val oneHandUI = remember { appPrefs.oneHandUI.state } + val selectedItemsBarHeight = if (selectedItems.value != null) AppBarHeight * fontSizeSqrtMultiplier else 0.dp + val navBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + val imePadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding() LazyColumnWithScrollBar( state = listState, contentPadding = if (oneHandUI.value) { - PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) + 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)) + PaddingValues( + top = topPaddingToContent(false), + bottom = if (imePadding > 0.dp) { + imePadding + selectedItemsBarHeight + } else { + navBarPadding + selectedItemsBarHeight + } + ) } ) { item { @@ -379,6 +498,40 @@ fun ModalData.GroupChatInfoLayout( SectionSpacer() + var anyTopSectionRowShow = false + SectionView { + if (groupInfo.canAddMembers && groupInfo.businessChat == null) { + anyTopSectionRowShow = true + if (groupLink == null) { + CreateGroupLinkButton(manageGroupLink) + } else { + GroupLinkButton(manageGroupLink) + } + } + if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + anyTopSectionRowShow = true + MemberSupportButton(chat, openMemberSupport) + } + if (groupInfo.canModerate) { + anyTopSectionRowShow = true + GroupReportsButton(chat) { + scope.launch { + showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo) + } + } + } + if ( + groupInfo.membership.memberActive && + (groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null) + ) { + anyTopSectionRowShow = true + UserSupportChatButton(chat, groupInfo, scrollToItemId) + } + } + if (anyTopSectionRowShow) { + SectionDividerSpaced(maxBottomPadding = false) + } + SectionView { if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { EditGroupProfileButton(editGroupProfile) @@ -388,19 +541,17 @@ fun ModalData.GroupChatInfoLayout( } 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 (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { + } + 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) + + SectionView { + if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) } else { SendReceiptsOptionDisabled() } - WallpaperButton { ModalManager.end.showModal { val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } @@ -410,59 +561,77 @@ fun ModalData.GroupChatInfoLayout( } } } + ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems) + SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) } - 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), members.count() + 1)) { - if (groupInfo.canAddMembers) { - if (groupInfo.businessChat == null) { - if (groupLink == null) { - CreateGroupLinkButton(manageGroupLink) - } else { - GroupLinkButton(manageGroupLink) + if (!groupInfo.nextConnectPrepared) { + SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) { + if (groupInfo.canAddMembers) { + val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers + val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + 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 (activeSortedMembers.size > 8) { + SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { + MemberListSearchRowView(searchText) } } - val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers - val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - 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 + SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + MemberRow(groupInfo.membership, user = true) } - AddMembersButton(addMembersTitleId, tint, onAddMembersClick) - } - if (members.size > 8) { - SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { - SearchRowView(searchText) - } - } - SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - MemberRow(groupInfo.membership, user = true) } } } - items(filteredMembers.value) { member -> - Divider() - val showMenu = remember { mutableStateOf(false) } - SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu) - MemberRow(member, onClick = { showMemberInfo(member) }) + if (!groupInfo.nextConnectPrepared) { + items(filteredMembers.value, key = { it.groupMemberId }) { member -> + Divider() + val showMenu = remember { mutableStateOf(false) } + 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 { - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + if (!groupInfo.nextConnectPrepared) { + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + } SectionView { ClearChatButton(clearChat) if (groupInfo.canDelete) { val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat DeleteGroupButton(titleId, deleteGroup) } - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat LeaveGroupButton(titleId, leaveGroup) } @@ -476,27 +645,104 @@ fun ModalData.GroupChatInfoLayout( } } SectionBottomSpacer() - Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } } if (!oneHandUI.value) { NavigationBarBackground(oneHandUI.value, oneHandUI.value) } + SelectedItemsButtonsToolbar(chat, groupInfo, selectedItems, rememberUpdatedState(activeSortedMembers)) + SelectedItemsCounterToolbarSetter(groupInfo, selectedItems, filteredMembers, appBar) } } @Composable -fun ChatTTLSection(chatItemTTL: State, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State) { - Box { - SectionView { - TtlOptions( - chatItemTTL, - enabled = remember { derivedStateOf { !deletingItems.value } }, - onSelected = setChatItemTTL, - default = chatModel.chatItemTTL +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 + } + } + } ) - SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) } + 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 ChatTTLOption(chatItemTTL: State, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State) { + Box { + TtlOptions( + chatItemTTL, + enabled = remember { derivedStateOf { !deletingItems.value } }, + onSelected = setChatItemTTL, + default = chatModel.chatItemTTL + ) if (deletingItems.value) { Box(Modifier.matchParentSize()) { ProgressIndicator() @@ -508,31 +754,42 @@ fun ChatTTLSection(chatItemTTL: State, setChatItemTTL: (ChatItemTT @Composable private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) { Column( - Modifier.padding(horizontal = 8.dp), + Modifier.padding(horizontal = DEFAULT_PADDING), 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)) + val copyNameToClipboard = fun(name: String) { + clipboard.setText(AnnotatedString(name)) showToast(generalGetString(MR.strings.copied)) } + val displayName = groupInfo.groupProfile.displayName.trim() + val copyDisplayName = { copyNameToClipboard(displayName) } Text( - groupInfo.groupProfile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), + displayName, + style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), color = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, - maxLines = 4, + maxLines = 3, overflow = TextOverflow.Ellipsis, - modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) + modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) ) - 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, - modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) + ChatInfoDescription(cInfo, displayName, copyNameToClipboard) + } +} + +@Composable +private fun MemberSupportButton(chat: Chat, onClick: () -> Unit) { + SettingsActionItemWithContent( + painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), + stringResource(MR.strings.member_support), + click = onClick, + iconColor = (if (chat.supportUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) + ) { + if (chat.supportUnreadCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.supportUnreadCount), + backgroundColor = MaterialTheme.colors.primary ) } } @@ -548,12 +805,20 @@ private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) } @Composable -private fun GroupReportsButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_flag), +private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) { + SettingsActionItemWithContent( + painterResource(if (chat.chatStats.reportsCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), stringResource(MR.strings.group_reports_member_reports), - click = onClick - ) + click = onClick, + iconColor = (if (chat.chatStats.reportsCount > 0) Color.Red else MaterialTheme.colors.secondary) + ) { + if (chat.chatStats.reportsCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.chatStats.reportsCount), + backgroundColor = Color.Red + ) + } + } } @Composable @@ -599,14 +864,14 @@ private fun AddMembersButton(titleId: StringResource, tint: Color = MaterialThem } @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) } } @@ -628,11 +893,11 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() - verticalAlignment = Alignment.CenterVertically ) { Row( - Modifier.weight(1f).padding(top = 8.dp, end = DEFAULT_PADDING, bottom = 8.dp), + 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) ) { - MemberProfileImage(size = 42.dp, member) + MemberProfileImage(size = MEMBER_ROW_AVATAR_SIZE, member, async = true) Spacer(Modifier.width(DEFAULT_PADDING_HALF)) Column { Row(verticalAlignment = Alignment.CenterVertically) { @@ -640,33 +905,48 @@ 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 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 - ) + + 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, + ) + } } } @Composable -private fun MemberVerifiedShield() { +fun MemberVerifiedShield() { Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary) } @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) { @@ -690,6 +970,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) { @@ -777,7 +1061,7 @@ private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) { } @Composable -private fun SearchRowView( +fun MemberListSearchRowView( searchText: MutableState = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } ) { Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) { @@ -792,12 +1076,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 { - withChats { - updateGroup(chatRh, it) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(chatRh, it) } } } +fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List, withMessages: Boolean, onSuccess: () -> Unit = {}) { + withBGApi { + val r = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds, withMessages = withMessages) + if (r != null) { + val (updatedGroupInfo, updatedMembers) = r + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo) + updatedMembers.forEach { updatedMember -> + chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, updatedMember) + if (withMessages) { + chatModel.chatsContext.removeMemberItems(rhId, updatedMember, byMember = groupInfo.membership, groupInfo) + } + } + } + withContext(Dispatchers.Main) { + updatedMembers.forEach { updatedMember -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, updatedMember) + if (withMessages) { + chatModel.chatsContext.removeMemberItems(rhId, updatedMember, byMember = groupInfo.membership, groupInfo) + } + } + } + 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() { @@ -814,12 +1137,25 @@ fun PreviewGroupChatInfoLayout() { setSendReceipts = {}, chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) }, setChatItemTTL = {}, - members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), + activeSortedMembers = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, onLocalAliasChanged = {}, groupLink = null, + 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) } + addMembers = {}, + showMemberInfo = {}, + editGroupProfile = {}, + addOrEditWelcomeMessage = {}, + openMemberSupport = {}, + 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 987a80e7c0..5a94e7d505 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,7 +1,8 @@ package chat.simplex.common.views.chat.group import SectionBottomSpacer -import androidx.compose.foundation.* +import SectionItemView +import SectionViewWithButton import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -10,16 +11,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.style.TextAlign 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.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 @@ -27,46 +29,109 @@ fun GroupLinkView( chatModel: ChatModel, rhId: Long?, groupInfo: GroupInfo, - connReqContact: String?, - memberRole: GroupMemberRole?, - onGroupLinkUpdated: ((Pair?) -> Unit)?, + groupLink: GroupLink?, + onGroupLinkUpdated: ((GroupLink?) -> Unit)?, creatingGroup: Boolean = false, close: (() -> Unit)? = null ) { - var groupLink by rememberSaveable { mutableStateOf(connReqContact) } - val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) } + var groupLinkVar by rememberSaveable(stateSaver = GroupLink.nullableStateSaver) { mutableStateOf(groupLink) } + val groupLinkMemberRole = rememberSaveable { mutableStateOf(groupLink?.acceptMemberRole) } var creatingLink by rememberSaveable { mutableStateOf(false) } + val clipboard = LocalClipboardManager.current fun createLink() { creatingLink = true withBGApi { val link = chatModel.controller.apiCreateGroupLink(rhId, groupInfo.groupId) if (link != null) { - groupLink = link.first - groupLinkMemberRole.value = link.second + groupLinkVar = link + groupLinkMemberRole.value = link.acceptMemberRole onGroupLinkUpdated?.invoke(link) } creatingLink = false } } + fun addShortLink(shareOnCompletion: Boolean = false) { + creatingLink = true + withBGApi { + val link = chatModel.controller.apiAddGroupShortLink(rhId, groupInfo.groupId) + if (link != null) { + groupLinkVar = link + groupLinkMemberRole.value = link.acceptMemberRole + onGroupLinkUpdated?.invoke(link) + if (shareOnCompletion) { + clipboard.shareText(link.connLinkContact.simplexChatUri(short = true)) + } + } + creatingLink = false + } + } + fun showAddShortLinkAlert(shareAddress: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.share_group_profile_via_link), + text = generalGetString(MR.strings.share_group_profile_via_link_alert_text), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + addShortLink(shareOnCompletion = shareAddress != null) + }) { + Text( + generalGetString(MR.strings.share_profile_via_link_alert_confirm), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + + if (shareAddress != null) { + // Delete without notification + SectionItemView({ + AlertManager.shared.hideAlert() + shareAddress() + }) { + Text( + generalGetString(MR.strings.share_old_link_alert_button), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + } + ) + } LaunchedEffect(Unit) { if (groupLink == null && !creatingLink) { createLink() } } GroupLinkLayout( - groupLink = groupLink, + groupLink = groupLinkVar, groupInfo, groupLinkMemberRole, creatingLink, createLink = ::createLink, + showAddShortLinkAlert = ::showAddShortLinkAlert, updateLink = { val role = groupLinkMemberRole.value if (role != null) { withBGApi { val link = chatModel.controller.apiGroupLinkMemberRole(rhId, groupInfo.groupId, role) if (link != null) { - groupLink = link.first - groupLinkMemberRole.value = link.second + groupLinkVar = link + groupLinkMemberRole.value = link.acceptMemberRole onGroupLinkUpdated?.invoke(link) } } @@ -81,7 +146,7 @@ fun GroupLinkView( withBGApi { val r = chatModel.controller.apiDeleteGroupLink(rhId, groupInfo.groupId) if (r) { - groupLink = null + groupLinkVar = null onGroupLinkUpdated?.invoke(null) } } @@ -99,11 +164,12 @@ fun GroupLinkView( @Composable fun GroupLinkLayout( - groupLink: String?, + groupLink: GroupLink?, groupInfo: GroupInfo, groupLinkMemberRole: MutableState, creatingLink: Boolean, createLink: () -> Unit, + showAddShortLinkAlert: ((() -> Unit)?) -> Unit, updateLink: () -> Unit, deleteLink: () -> Unit, creatingGroup: Boolean = false, @@ -150,7 +216,15 @@ fun GroupLinkLayout( } initialLaunch = false } - SimpleXLinkQRCode(groupLink) + val showShortLink = remember { mutableStateOf(true) } + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + if (groupLink.connLinkContact.connShortLink == null) { + SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = false) + } else { + SectionViewWithButton(titleButton = { ToggleShortLinkButton(showShortLink) }) { + SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value) + } + } Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, @@ -160,7 +234,15 @@ fun GroupLinkLayout( SimpleButton( stringResource(MR.strings.share_link), icon = painterResource(MR.images.ic_share), - click = { clipboard.shareText(simplexChatLink(groupLink)) } + click = { + if (groupLink.shouldBeUpgraded) { + showAddShortLinkAlert { + clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) + } + } else { + clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) + } + } ) if (creatingGroup && close != null) { ContinueButton(close) @@ -173,12 +255,28 @@ fun GroupLinkLayout( ) } } + if (groupLink.shouldBeUpgraded) { + AddShortLinkButton(text = stringResource(MR.strings.upgrade_group_link)) { + showAddShortLinkAlert(null) + } + } } } SectionBottomSpacer() } } +@Composable +private fun AddShortLinkButton(text: String, onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_add), + text, + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState, enabled: Boolean = true) { Row( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index ef1c69a5bb..f09d2f44bb 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 @@ -27,8 +27,6 @@ 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.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* @@ -36,38 +34,45 @@ import chat.simplex.common.views.newchat.* import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* -import chat.simplex.common.views.chatlist.openLoadedChat +import chat.simplex.common.views.chatlist.openDirectChat import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.datetime.Clock +import kotlinx.coroutines.* @Composable fun GroupMemberInfoView( rhId: Long?, groupInfo: GroupInfo, member: GroupMember, + scrollToItemId: MutableState, connectionStats: ConnectionStats?, connectionCode: String?, chatModel: ChatModel, + openedFromSupportChat: Boolean, close: () -> Unit, closeAll: () -> Unit, // Close all open windows up to ChatView ) { + KeyChangeEffect(chat.simplex.common.platform.chatModel.chatId.value) { + ModalManager.end.closeModals() + } BackHandler(onBack = close) 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) } + val scope = rememberCoroutineScope() fun syncMemberConnection() { withBGApi { val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) if (r != null) { connStats.value = r.second - withChats { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) } - withReportsChatsIfOpen { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) } close.invoke() } @@ -80,17 +85,16 @@ fun GroupMemberInfoView( rhId = rhId, groupInfo, member, + scrollToItemId, connStats, newRole, developerTools, connectionCode, getContactChat = { chatModel.getContactChat(it) }, - openDirectChat = { - withBGApi { - apiLoadMessages(rhId, ChatType.Direct, it, null, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) - if (chatModel.getContactChat(it) != null) { - closeAll() - } + openDirectChat = { contactId -> + scope.launch { + openDirectChat(rhId, contactId) + closeAll() } }, createMemberContact = { @@ -100,12 +104,11 @@ fun GroupMemberInfoView( val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) if (memberContact != null) { val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) - withChats { - addChat(memberChat) + withContext(Dispatchers.Main) { + chatModel.chatsContext.addChat(memberChat) } - openLoadedChat(memberChat) + openDirectChat(rhId, memberContact.contactId) closeAll() - chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) } progressIndicator = false } @@ -133,26 +136,15 @@ fun GroupMemberInfoView( blockForAll = { blockForAllAlert(rhId, groupInfo, member) }, unblockForAll = { unblockForAllAlert(rhId, groupInfo, member) }, removeMember = { removeMemberDialog(rhId, groupInfo, member, chatModel, close) }, + deleteMemberMessages = { deleteMemberMessagesDialog(rhId, groupInfo, member, chatModel, close) }, onRoleSelected = { if (it == newRole.value) return@GroupMemberInfoLayout val prevValue = newRole.value newRole.value = it - updateMemberRoleDialog(it, groupInfo, 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) - withChats { - upsertGroupMember(rhId, groupInfo, mem) - } - withReportsChatsIfOpen { - upsertGroupMember(rhId, groupInfo, mem) - } - }.onFailure { - newRole.value = prevValue - } - } + updateMembersRole(newRole.value, rhId, groupInfo, listOf(member.groupMemberId), onFailure = { newRole.value = prevValue }) } }, switchMemberAddress = { @@ -161,11 +153,11 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - withChats { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) } - withReportsChatsIfOpen { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) } close.invoke() } @@ -178,11 +170,11 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - withChats { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) } - withReportsChatsIfOpen { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) } close.invoke() } @@ -198,11 +190,11 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true) if (r != null) { connStats.value = r.second - withChats { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) } - withReportsChatsIfOpen { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) } close.invoke() } @@ -224,11 +216,11 @@ fun GroupMemberInfoView( connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null ) ) - withChats { - upsertGroupMember(rhId, groupInfo, copy) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, copy) } - withReportsChatsIfOpen { - upsertGroupMember(rhId, groupInfo, copy) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, copy) } r } @@ -237,7 +229,8 @@ fun GroupMemberInfoView( ) } } - } + }, + openedFromSupportChat = openedFromSupportChat ) if (progressIndicator) { @@ -251,33 +244,69 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c 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(messageId), - confirmText = generalGetString(MR.strings.remove_member_confirmation), - onConfirm = { - withBGApi { - val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId) - if (removedMember != null) { - withChats { - upsertGroupMember(rhId, groupInfo, removedMember) - } - withReportsChatsIfOpen { - upsertGroupMember(rhId, groupInfo, removedMember) - } + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.button_remove_member_question), + generalGetString(messageId), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMember(rhId, groupInfo, member, withMessages = false, chatModel, close) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeMember(rhId, groupInfo, member, withMessages = true, chatModel, close) + }) { + Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } - close?.invoke() } + }) +} + +fun deleteMemberMessagesDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.button_delete_member_messages_question), + text = generalGetString(MR.strings.member_messages_will_be_deleted_cannot_be_undone), + confirmText = generalGetString(MR.strings.delete_member_messages_confirmation), + onConfirm = { + removeMember(rhId, groupInfo, member, withMessages = true, chatModel, close) }, destructive = true, ) } +fun removeMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, withMessages: Boolean, chatModel: ChatModel, close: (() -> Unit)? = null) { + withBGApi { + val r = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId), withMessages = withMessages) + if (r != null) { + val (updatedGroupInfo, removedMembers) = r + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo) + removedMembers.forEach { removedMember -> + chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, removedMember) + if (withMessages) { + chat.simplex.common.platform.chatModel.chatsContext.removeMemberItems(rhId, removedMember, byMember = groupInfo.membership, groupInfo) + } + } + } + } + close?.invoke() + } +} + @Composable fun GroupMemberInfoLayout( rhId: Long?, groupInfo: GroupInfo, member: GroupMember, + scrollToItemId: MutableState, connStats: MutableState, newRole: MutableState, developerTools: Boolean, @@ -291,12 +320,14 @@ fun GroupMemberInfoLayout( blockForAll: () -> Unit, unblockForAll: () -> Unit, removeMember: () -> Unit, + deleteMemberMessages: () -> Unit, onRoleSelected: (GroupMemberRole) -> Unit, switchMemberAddress: () -> Unit, abortSwitchMemberAddress: () -> Unit, syncMemberConnection: () -> Unit, syncMemberConnectionForce: () -> Unit, verifyClicked: () -> Unit, + openedFromSupportChat: Boolean ) { val cStats = connStats.value fun knownDirectChat(contactId: Long): Pair? { @@ -309,7 +340,30 @@ fun GroupMemberInfoLayout( } @Composable - fun AdminDestructiveSection() { + fun SupportChatButton() { + val scope = rememberCoroutineScope() + + SettingsActionItem( + painterResource(MR.images.ic_flag), + stringResource(MR.strings.button_support_chat_member), + click = { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = member) + val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo) + scope.launch { + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId = scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + }, + iconColor = MaterialTheme.colors.secondary, + ) + } + + @Composable + fun ModeratorDestructiveSection() { val canBlockForAll = member.canBlockForAll(groupInfo) val canRemove = member.canBeRemoved(groupInfo) if (canBlockForAll || canRemove) { @@ -323,7 +377,11 @@ fun GroupMemberInfoLayout( } } if (canRemove) { - RemoveMemberButton(removeMember) + if (member.memberStatus == GroupMemberStatus.MemRemoved || member.memberStatus == GroupMemberStatus.MemLeft) { + DeleteMemberMessagesButton(deleteMemberMessages) + } else { + RemoveMemberButton(removeMember) + } } } } @@ -422,6 +480,13 @@ fun GroupMemberInfoLayout( if (member.memberActive) { SectionView { + if ( + !openedFromSupportChat && + groupInfo.membership.memberRole >= GroupMemberRole.Moderator && + (member.memberRole < GroupMemberRole.Moderator || member.supportChat != null) + ) { + SupportChatButton() + } if (connectionCode != null) { VerifyCodeButton(member.verified, verifyClicked) } @@ -465,6 +530,17 @@ fun GroupMemberInfoLayout( if (cStats != null) { SectionDividerSpaced() SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) { + val subStatus = cStats.subStatus + if (subStatus != null) { + SectionItemView({ + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.network_status), + subStatus.statusExplanation + ) + }) { + SubStatusRow(subStatus) + } + } SwitchAddressButton( disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || !member.sendMsgEnabled, switchAddress = switchMemberAddress @@ -486,8 +562,8 @@ fun GroupMemberInfoLayout( } } - if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { - AdminDestructiveSection() + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ModeratorDestructiveSection() } else { NonAdminBlockSection() } @@ -544,11 +620,12 @@ fun GroupMemberInfoHeader(member: GroupMember) { horizontalAlignment = Alignment.CenterHorizontally ) { MemberProfileImage(size = 192.dp, member, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) + val displayName = member.displayName.trim() // alias if set val text = buildAnnotatedString { if (member.verified) { appendInlineContent(id = "shieldIcon") } - append(member.displayName) + append(displayName) } val inlineContent: Map = mapOf( "shieldIcon" to InlineTextContent( @@ -558,10 +635,11 @@ fun GroupMemberInfoHeader(member: GroupMember) { } ) val clipboard = LocalClipboardManager.current - val copyNameToClipboard = { - clipboard.setText(AnnotatedString(member.displayName)) + val copyNameToClipboard = fun(name: String) { + clipboard.setText(AnnotatedString(name)) showToast(generalGetString(MR.strings.copied)) } + val copyDisplayName = { copyNameToClipboard(displayName) } Text( text, inlineContent = inlineContent, @@ -569,18 +647,10 @@ fun GroupMemberInfoHeader(member: GroupMember) { textAlign = TextAlign.Center, maxLines = 3, overflow = TextOverflow.Ellipsis, - modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) + modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) ) - if (member.fullName != "" && member.fullName != member.displayName) { - Text( - member.fullName, style = MaterialTheme.typography.h2, - color = MaterialTheme.colors.onBackground, - textAlign = TextAlign.Center, - maxLines = 4, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) - ) - } + // passing actual display name here, as alias is used above + ChatInfoDescription(member, member.memberProfile.displayName.trim(), copyNameToClipboard) } } @@ -635,6 +705,17 @@ fun RemoveMemberButton(onClick: () -> Unit) { ) } +@Composable +fun DeleteMemberMessagesButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_delete), + stringResource(MR.strings.button_delete_member_messages), + click = onClick, + textColor = Color.Red, + iconColor = Color.Red, + ) +} + @Composable fun OpenChatButton( modifier: Modifier, @@ -690,27 +771,50 @@ fun MemberProfileImage( size: Dp, mem: GroupMember, color: Color = MaterialTheme.colors.secondaryVariant, - backgroundColor: Color? = null + backgroundColor: Color? = null, + async: Boolean = false ) { ProfileImage( size = size, image = mem.image, color = color, backgroundColor = backgroundColor, - blurred = mem.blocked + blurred = mem.blocked, + async = async ) } -private fun updateMemberRoleDialog( +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, groupInfo: GroupInfo, - member: GroupMember, + memberCurrent: Boolean, onDismiss: () -> Unit, onConfirm: () -> Unit ) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.change_member_role_question), - text = if (member.memberCurrent) { + text = if (memberCurrent) { if (groupInfo.businessChat == null) String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) else @@ -724,10 +828,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 { withBGApi { - planAndConnect(rhId, connReqUri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) + planAndConnect(rhId, connReqUri, close = { ModalManager.closeAllModalsEverywhere() }) } } catch (e: RuntimeException) { AlertManager.shared.showAlertMsg( @@ -769,11 +889,11 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem withBGApi { val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings) if (success) { - withChats { - upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) } - withReportsChatsIfOpen { - upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) } } } @@ -785,7 +905,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, ) @@ -797,20 +929,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) - withChats { - 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) + } } - withReportsChatsIfOpen { - upsertGroupMember(rhId, gInfo, updatedMember) + withContext(Dispatchers.Main) { + updatedMembers.forEach { updatedMember -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, gInfo, updatedMember) + } } + onSuccess() } } @@ -822,6 +970,7 @@ fun PreviewGroupMemberInfoLayout() { rhId = null, groupInfo = GroupInfo.sampleData, member = GroupMember.sampleData, + scrollToItemId = remember { mutableStateOf(null) }, connStats = remember { mutableStateOf(null) }, newRole = remember { mutableStateOf(GroupMemberRole.Member) }, developerTools = false, @@ -835,12 +984,14 @@ fun PreviewGroupMemberInfoLayout() { blockForAll = {}, unblockForAll = {}, removeMember = {}, + deleteMemberMessages = {}, onRoleSelected = {}, switchMemberAddress = {}, abortSwitchMemberAddress = {}, syncMemberConnection = {}, syncMemberConnectionForce = {}, verifyClicked = {}, + openedFromSupportChat = false, ) } } 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..81e7d80da3 --- /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(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(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..aa737a02d3 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt @@ -0,0 +1,326 @@ +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( + chatsCtx: ChatModel.ChatsContext, + 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) } + + fun contextMemberFilter(member: GroupMember): Boolean = + when (chatsCtx.secondaryContextFilter) { + null -> true + is SecondaryContextFilter.GroupChatScopeContext -> + when (chatsCtx.secondaryContextFilter.groupScopeInfo) { + is GroupChatScopeInfo.MemberSupport -> { + val scopeMember = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ + if (scopeMember != null) { + member.memberRole >= GroupMemberRole.Moderator || member.groupMemberId == scopeMember.groupMemberId + } else { + member.memberRole >= GroupMemberRole.Moderator + } + } + } + is SecondaryContextFilter.MsgContentTagContext -> false + } + + val filteredMembers = remember { + derivedStateOf { + val members = chatModel.groupMembers.value + .filter { + val status = it.memberStatus + status != GroupMemberStatus.MemLeft && status != GroupMemberStatus.MemRemoved && status != GroupMemberStatus.MemInvited + && contextMemberFilter(it) + } + .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 3d9f42f929..b8db5969a1 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 @@ -15,12 +15,16 @@ 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.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.usersettings.SettingsActionItem 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) ) @@ -41,12 +45,12 @@ 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) { - withChats { - updateGroup(rhId, g) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, g) currentPreferences = preferences } - withChats { - updateGroup(rhId, g) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, g) } } afterSave() @@ -69,6 +73,16 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> preferences = currentPreferences }, savePrefs = ::savePrefs, + openMemberAdmission = { + ModalManager.end.showCustomModal { close -> + MemberAdmissionView( + chatModel, + rhId, + chatId, + close + ) + } + } ) } } @@ -81,10 +95,15 @@ private fun GroupPreferencesLayout( applyPrefs: (FullGroupPreferences) -> Unit, reset: () -> Unit, savePrefs: () -> Unit, + openMemberAdmission: () -> Unit, ) { ColumnWithScrollBar { val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences AppBarTitle(stringResource(titleId)) + if (groupInfo.businessChat == null) { + MemberAdmissionButton(openMemberAdmission) + SectionDividerSpaced(maxBottomPadding = false) + } val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val onTTLUpdated = { ttl: Int? -> applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) @@ -132,6 +151,11 @@ private fun GroupPreferencesLayout( 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, _ -> @@ -149,6 +173,15 @@ private fun GroupPreferencesLayout( } } +@Composable +private fun MemberAdmissionButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_toggle_on), + stringResource(MR.strings.member_admission), + click = onClick + ) +} + @Composable private fun FeatureSection( feature: GroupFeature, @@ -169,6 +202,7 @@ private fun FeatureSection( feature.text, icon, iconTint, + disabled = feature == GroupFeature.Reports, // remove in 6.4 checked = enableFeature.value == GroupFeatureEnabled.ON, ) { checked -> onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF, enableForRole?.value) 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 3163c109e6..f15f70673a 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 @@ -17,8 +17,6 @@ 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.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.* @@ -27,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 @@ -40,8 +37,8 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl withBGApi { val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) if (gInfo != null) { - withChats { - updateGroup(rhId, gInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, gInfo) } close.invoke() } @@ -59,24 +56,27 @@ fun GroupProfileLayout( val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) } val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) } + val shortDescr = rememberSaveable { mutableStateOf(groupProfile.shortDescr ?: "") } val chosenImage = rememberSaveable { mutableStateOf(null) } val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) } val scope = rememberCoroutineScope() val scrollState = rememberScrollState() val focusRequester = remember { FocusRequester() } val dataUnchanged = - displayName.value == groupProfile.displayName && - fullName.value == groupProfile.fullName && + displayName.value.trim() == groupProfile.displayName && + fullName.value.trim() == groupProfile.fullName && + shortDescr.value.trim() == (groupProfile.shortDescr ?: "") && groupProfile.image == profileImage.value val closeWithAlert = { - if (dataUnchanged || !canUpdateProfile(displayName.value, groupProfile)) { + if (dataUnchanged || !canUpdateProfile(displayName.value, shortDescr.value, groupProfile)) { close() } else { showUnsavedChangesAlert({ saveProfile( groupProfile.copy( displayName = displayName.value.trim(), - fullName = fullName.value, + fullName = fullName.value.trim(), + shortDescr = shortDescr.value.trim().ifEmpty { null }, image = profileImage.value ) ) @@ -133,7 +133,7 @@ fun GroupProfileLayout( } } ProfileNameField(displayName, "", { isValidNewProfileName(it, groupProfile) }, focusRequester) - if (groupProfile.fullName.isNotEmpty() && groupProfile.fullName != groupProfile.displayName) { + if (groupProfile.fullName.trim().isNotEmpty() && groupProfile.fullName.trim() != groupProfile.displayName.trim()) { Spacer(Modifier.height(DEFAULT_PADDING)) Text( stringResource(MR.strings.group_full_name_field), @@ -142,8 +142,28 @@ fun GroupProfileLayout( ) ProfileNameField(fullName) } + Spacer(Modifier.height(DEFAULT_PADDING)) - val enabled = !dataUnchanged && canUpdateProfile(displayName.value, groupProfile) + + Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(MR.strings.group_short_descr_field), + fontSize = 16.sp, + ) + if (!bioFitsLimit(shortDescr.value)) { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) + IconButton( + onClick = { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.group_descr_too_large)) }, + Modifier.size(20.dp) + ) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } + ProfileNameField(shortDescr, "", isValid = { bioFitsLimit(it) }) + + Spacer(Modifier.height(DEFAULT_PADDING)) + val enabled = !dataUnchanged && canUpdateProfile(displayName.value, shortDescr.value, groupProfile) if (enabled) { Text( stringResource(MR.strings.save_group_profile), @@ -151,7 +171,8 @@ fun GroupProfileLayout( saveProfile( groupProfile.copy( displayName = displayName.value.trim(), - fullName = fullName.value, + fullName = fullName.value.trim(), + shortDescr = shortDescr.value.trim().ifEmpty { null }, image = profileImage.value ) ) @@ -177,8 +198,8 @@ fun GroupProfileLayout( } } -private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = - displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, groupProfile) +private fun canUpdateProfile(displayName: String, shortDescr: String, groupProfile: GroupProfile): Boolean = + displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, groupProfile) && bioFitsLimit(shortDescr) private fun isValidNewProfileName(displayName: String, groupProfile: GroupProfile): Boolean = displayName == groupProfile.displayName || isValidDisplayName(displayName.trim()) 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 index a1ec3ec0a9..2cc2402c0a 100644 --- 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 @@ -14,30 +14,22 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.* -val LocalContentTag: ProvidableCompositionLocal = staticCompositionLocalOf { null } - -data class GroupReports( - val reportsCount: Int, - val reportsView: Boolean, -) { - val showBar: Boolean = reportsCount > 0 && !reportsView - - fun toContentTag(): MsgContentTag? { - if (!reportsView) return null - return MsgContentTag.Report - } - - val contentTag: MsgContentTag? = if (!reportsView) null else MsgContentTag.Report -} - @Composable -private fun GroupReportsView(staleChatId: State, scrollToItemId: MutableState) { - ChatView(staleChatId, reportsView = true, scrollToItemId, onComposed = {}) +private fun GroupReportsView( + reportsChatsCtx: ChatModel.ChatsContext, + staleChatId: State, + scrollToItemId: MutableState, + close: () -> Unit +) { + KeyChangeEffect(chatModel.chatId.value) { + close() + } + ChatView(reportsChatsCtx, staleChatId, scrollToItemId, onComposed = {}) } @Composable fun GroupReportsAppBar( - groupReports: State, + chatsCtx: ChatModel.ChatsContext, close: () -> Unit, onSearchValueChanged: (String) -> Unit ) { @@ -65,11 +57,11 @@ fun GroupReportsAppBar( } } ) - ItemsReload(groupReports) + ItemsReload(chatsCtx) } @Composable -private fun ItemsReload(groupReports: State) { +fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) { LaunchedEffect(Unit) { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() @@ -79,18 +71,19 @@ private fun ItemsReload(groupReports: State) { .filterNotNull() .filter { it.chatInfo is ChatInfo.Group } .collect { chat -> - reloadItems(chat, groupReports) + reloadItems(chatsCtx, chat) } } } suspend fun showGroupReportsView(staleChatId: State, scrollToItemId: MutableState, chatInfo: ChatInfo) { - openChat(chatModel.remoteHostId(), chatInfo, MsgContentTag.Report) - ModalManager.end.showCustomModal(true, id = ModalViewId.GROUP_REPORTS) { close -> + val reportsChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.MsgContentTagContext(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(staleChatId, scrollToItemId) + GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId, close) } else { LaunchedEffect(Unit) { close() @@ -100,7 +93,6 @@ suspend fun showGroupReportsView(staleChatId: State, scrollToItemId: Mu } } -private suspend fun reloadItems(chat: Chat, groupReports: State) { - val contentFilter = groupReports.value.toContentTag() - apiLoadMessages(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, contentFilter, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) +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/MemberAdmission.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt new file mode 100644 index 0000000000..48171bfeb7 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt @@ -0,0 +1,152 @@ +package chat.simplex.common.views.chat.group + +import InfoRow +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import dev.icerock.moko.resources.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 dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit) { + val groupInfo = remember { derivedStateOf { + val ch = m.getChat(chatId) + val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo + if (g == null || ch.remoteHostId != rhId) null else g + }} + val gInfo = groupInfo.value ?: return + var admission by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.groupProfile.memberAdmission) } + var currentAdmission by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(admission) } + + fun saveAdmission(afterSave: () -> Unit = {}) { + withBGApi { + val gp = gInfo.groupProfile.copy(memberAdmission = admission) + val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) + if (g != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, g) + currentAdmission = admission + } + } + afterSave() + } + } + ModalView( + close = { + if (admission == currentAdmission) close() + else showUnsavedChangesAlert({ saveAdmission(close) }, close) + }, + ) { + MemberAdmissionLayout( + admission, + currentAdmission, + gInfo, + applyAdmission = { admsn -> + admission = admsn + }, + reset = { + admission = currentAdmission + }, + saveAdmission = ::saveAdmission, + ) + } +} + +@Composable +private fun MemberAdmissionLayout( + admission: GroupMemberAdmission?, + currentAdmission: GroupMemberAdmission?, + groupInfo: GroupInfo, + applyAdmission: (GroupMemberAdmission) -> Unit, + reset: () -> Unit, + saveAdmission: () -> Unit, +) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.member_admission)) + val review = remember(admission) { mutableStateOf(admission?.review) } + AdmissionSection(MR.strings.admission_stage_review, MR.strings.admission_stage_review_descr, review, groupInfo) { criteria -> + if (admission != null) { + applyAdmission(admission.copy(review = criteria)) + } else { + applyAdmission(GroupMemberAdmission(review = criteria)) + } + } + if (groupInfo.isOwner) { + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + ResetSaveButtons( + reset = reset, + save = saveAdmission, + disabled = admission == currentAdmission + ) + } + SectionBottomSpacer() + } +} + +private val memberCriterias: List> = listOf( + null to generalGetString(MR.strings.member_criteria_off), + MemberCriteria.All to generalGetString(MR.strings.member_criteria_all) +) + +@Composable +private fun AdmissionSection( + admissionStageStrId: StringResource, + admissionStageDescrStrId: StringResource, + memberCriteria: State, + groupInfo: GroupInfo, + onSelected: (MemberCriteria?) -> Unit +) { + SectionView { + if (groupInfo.isOwner) { + ExposedDropDownSettingRow( + generalGetString(admissionStageStrId), + memberCriterias, + memberCriteria, + onSelected = { value -> + onSelected(value) + } + ) + } else { + InfoRow( + stringResource(admissionStageStrId), + memberCriteria.value?.text ?: generalGetString(MR.strings.member_criteria_off) + ) + } + } + SectionTextFooter(stringResource( admissionStageDescrStrId)) +} + +@Composable +private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { + SectionView { + SectionItemView(reset, disabled = disabled) { + Text(stringResource(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + SectionItemView(save, disabled = disabled) { + Text(stringResource(MR.strings.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + } +} + +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.save_admission_question), + confirmText = generalGetString(MR.strings.save_and_notify_group_members), + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = revert, + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt new file mode 100644 index 0000000000..6680ef99bc --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt @@ -0,0 +1,169 @@ +package chat.simplex.common.views.chat.group + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +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.dp +import chat.simplex.common.model.* +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 + +@Composable +private fun MemberSupportChatView( + chatInfo: ChatInfo, + memberSupportChatsCtx: ChatModel.ChatsContext, + staleChatId: State, + scrollToItemId: MutableState +) { + KeyChangeEffect(chatModel.chatId.value) { + ModalManager.end.closeModals() + } + if (appPlatform.isAndroid) { + DisposableEffect(Unit) { + onDispose { + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatInfo.id } + if ( + memberSupportChatsCtx.isUserSupportChat + && chat?.chatInfo?.groupInfo_?.membership?.memberPending == true + ) { + withBGApi { + chatModel.chatId.value = null + } + } + } + } + } + ChatView(memberSupportChatsCtx, staleChatId, scrollToItemId, onComposed = {}) +} + +@Composable +fun MemberSupportChatAppBar( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chat: Chat, + scopeMember_: GroupMember?, + scrollToItemId: MutableState, + close: () -> Unit, + onSearchValueChanged: (String) -> Unit +) { + val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state } + val chatBottomBar = remember { ChatController.appPrefs.chatBottomBar.state } + val showSearch = rememberSaveable { mutableStateOf(false) } + val onBackClicked = { + if (!showSearch.value) { + close() + } else { + onSearchValueChanged("") + showSearch.value = false + } + } + BackHandler(onBack = onBackClicked) + if (chat.chatInfo is ChatInfo.Group && scopeMember_ != null) { + val groupInfo = chat.chatInfo.groupInfo + DefaultAppBar( + navigationButton = { NavigationButtonBack(onBackClicked) }, + title = { MemberSupportChatToolbarTitle(scopeMember_) }, + onTitleClick = { + withBGApi { + val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, scopeMember_.groupMemberId) + val stats = r?.second + val code = if (scopeMember_.memberActive) { + val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, scopeMember_.groupMemberId) + memCode?.second + } else { + null + } + ModalManager.end.showModalCloseable(true) { closeCurrent -> + remember { derivedStateOf { chatModel.getGroupMember(scopeMember_.groupMemberId) } }.value?.let { mem -> + GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = true, close = closeCurrent) { + closeCurrent() + close() + } + } + } + } + }, + onTop = !oneHandUI.value || !chatBottomBar.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) + } + } + ) + } else { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onBackClicked) }, + fixedTitleText = stringResource(MR.strings.support_chat), + onTitleClick = null, + onTop = !oneHandUI.value || !chatBottomBar.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 +fun MemberSupportChatToolbarTitle(member: GroupMember, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + MemberProfileImage(size = imageSize * fontSizeSqrtMultiplier, member, iconColor) + Column( + Modifier.padding(start = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (member.verified) { + MemberVerifiedShield() + } + Text( + member.displayName, fontWeight = FontWeight.SemiBold, + maxLines = 1, overflow = TextOverflow.Ellipsis + ) + } + if (member.fullName != "" && member.fullName != member.displayName && member.localAlias.isEmpty()) { + Text( + member.fullName, + maxLines = 1, overflow = TextOverflow.Ellipsis + ) + } + } + } +} + +suspend fun showMemberSupportChatView(staleChatId: State, scrollToItemId: MutableState, chatInfo: ChatInfo, scopeInfo: GroupChatScopeInfo) { + val memberSupportChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.GroupChatScopeContext(scopeInfo)) + openChat(secondaryChatsCtx = memberSupportChatsCtx, chatModel.remoteHostId(), chatInfo) + ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close -> + ModalView({}, showAppBar = false) { + if (chatInfo is ChatInfo.Group && chatInfo.groupChatScope != null) { + MemberSupportChatView(chatInfo, memberSupportChatsCtx, staleChatId, scrollToItemId) + } else { + LaunchedEffect(Unit) { + close() + } + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt new file mode 100644 index 0000000000..e696128288 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt @@ -0,0 +1,327 @@ +package chat.simplex.common.views.chat.group + +import SectionBottomSpacer +import SectionItemView +import SectionItemViewLongClickable +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.items +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.graphics.Color +import androidx.compose.ui.text.TextStyle +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 dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chatlist.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.* + +@Composable +fun ModalData.MemberSupportView( + rhId: Long?, + chat: Chat, + groupInfo: GroupInfo, + scrollToItemId: MutableState, + close: () -> Unit +) { + KeyChangeEffect(chatModel.chatId.value) { + ModalManager.end.closeModals() + } + LaunchedEffect(Unit) { + setGroupMembers(rhId, groupInfo, chatModel) + } + ModalView( + close = close, + endButtons = { RefreshMembersButton(rhId, groupInfo) } + ) { + MemberSupportViewLayout( + chat, + groupInfo, + scrollToItemId + ) + } +} + +@Composable +fun RefreshMembersButton( + rhId: Long?, + groupInfo: GroupInfo +) { + IconButton( + onClick = { + withBGApi { + setGroupMembers(rhId, groupInfo, chatModel) + } + } + ) { + Icon( + painterResource(MR.images.ic_refresh), + contentDescription = null, + tint = MaterialTheme.colors.primary + ) + } +} + +@Composable +private fun ModalData.MemberSupportViewLayout( + chat: Chat, + groupInfo: GroupInfo, + scrollToItemId: MutableState +) { + val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state } + val scope = rememberCoroutineScope() + + val membersWithChats = remember { chatModel.groupMembers }.value + .filter { it.supportChat != null && it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } + .sortedWith( + compareByDescending { it.memberPending } + .thenByDescending { (it.supportChat?.mentions ?: 0) > 0 } + .thenByDescending { (it.supportChat?.memberAttention ?: 0) > 0 } + .thenByDescending { (it.supportChat?.unread ?: 0) > 0 } + .thenByDescending { it.supportChat?.chatTs } + ) + + val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } } + val filteredmembersWithChats = remember(membersWithChats) { + derivedStateOf { + val s = searchText.value.text.trim().lowercase() + if (s.isEmpty()) membersWithChats else membersWithChats.filter { m -> m.anyNameContains(s) } + } + } + + LazyColumnWithScrollBar( + contentPadding = + PaddingValues( + top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp else topPaddingToContent(false) + ) + ) { + item { + AppBarTitle(stringResource(MR.strings.member_support)) + } + + if (membersWithChats.isEmpty()) { + item { + Box(Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) { + Text(generalGetString(MR.strings.no_support_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + } + } + } else { + item { + SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { + MemberListSearchRowView(searchText) + } + } + items(filteredmembersWithChats.value, key = { it.groupMemberId }) { member -> + Divider() + val showMenu = remember { mutableStateOf(false) } + SectionItemViewLongClickable( + click = { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = member) + val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo) + scope.launch { + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId = scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + }, + longClick = { showMenu.value = true }, + minHeight = 54.dp, + padding = PaddingValues(horizontal = DEFAULT_PADDING) + ) { + Box(contentAlignment = Alignment.CenterStart) { + DropDownMenuForSupportChat(chat.remoteHostId, member, groupInfo, showMenu) + SupportChatRow(member) + } + } + } + item { + Divider() + SectionBottomSpacer() + } + } + } +} + +@Composable +fun SupportChatRow(member: GroupMember) { + fun memberStatus(): String { + return if (member.activeConn?.connDisabled == true) { + generalGetString(MR.strings.member_info_member_disabled) + } else if (member.activeConn?.connInactive == true) { + generalGetString(MR.strings.member_info_member_inactive) + } else if (member.memberPending) { + member.memberStatus.text + } else { + member.memberRole.text + } + } + + @Composable + fun SupportChatUnreadIndicator(supportChat: GroupSupportChat) { + Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.sp.toDp())) { + if (supportChat.unread > 0 || supportChat.mentions > 0 || supportChat.memberAttention > 0) { + val unreadBadgeColor = when { + supportChat.mentions > 0 || supportChat.memberAttention > 0 -> MaterialTheme.colors.primaryVariant + else -> MaterialTheme.colors.secondary + } + if (supportChat.mentions == 1 && supportChat.unread == 1) { + Box(modifier = Modifier.offset(y = 2.sp.toDp()).size(15.sp.toDp()).background(unreadBadgeColor, 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 { + if (supportChat.mentions > 0 && supportChat.unread > 1) { + Icon( + painterResource(MR.images.ic_alternate_email), + contentDescription = generalGetString(MR.strings.notifications), + tint = unreadBadgeColor, + modifier = Modifier.size(12.sp.toDp()).offset(y = 2.sp.toDp()) + ) + } + + UnreadBadge( + text = unreadCountStr(supportChat.unread), + backgroundColor = unreadBadgeColor, + yOffset = 2.dp + ) + } + } + } + } + } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + Modifier.weight(1f).padding(top = MEMBER_ROW_VERTICAL_PADDING, end = DEFAULT_PADDING, bottom = MEMBER_ROW_VERTICAL_PADDING), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MemberProfileImage(size = MEMBER_ROW_AVATAR_SIZE, member) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + if (member.verified) { + MemberVerifiedShield() + } + Text( + member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, + color = if (member.memberIncognito) Indigo else Color.Unspecified + ) + } + + Text( + memberStatus(), + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + Row { + if (member.memberPending) { + Icon( + painterResource(MR.images.ic_flag_filled), + contentDescription = null, + Modifier.padding(end = 3.dp).size(16.dp), + tint = MaterialTheme.colors.primaryVariant + ) + } + if (member.supportChat != null) { + SupportChatUnreadIndicator(member.supportChat) + } + } + } +} + +@Composable +private fun DropDownMenuForSupportChat(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState) { + DefaultDropdownMenu(showMenu) { + if (member.memberPending) { + ItemAction(stringResource(MR.strings.accept_pending_member_button), painterResource(MR.images.ic_check), color = MaterialTheme.colors.primary, onClick = { + acceptMemberDialog(rhId, groupInfo, member) + showMenu.value = false + }) + } else { + if (member.supportChatNotRead) { + ItemAction(stringResource(MR.strings.mark_read), painterResource(MR.images.ic_check), color = MaterialTheme.colors.primary, onClick = { + markSupportChatRead(rhId, groupInfo, member) + showMenu.value = false + }) + } + ItemAction(stringResource(MR.strings.delete_member_support_chat_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { + deleteMemberSupportChatDialog(rhId, groupInfo, member) + showMenu.value = false + }) + } + } +} + +fun deleteMemberSupportChatDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.delete_member_support_chat_alert_title), + confirmText = generalGetString(MR.strings.delete_member_support_chat_button), + onConfirm = { + deleteMemberSupportChat(rhId, groupInfo, member) + }, + destructive = true, + ) +} + +private fun deleteMemberSupportChat(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) { + withBGApi { + val r = chatModel.controller.apiDeleteMemberSupportChat(rhId, groupInfo.groupId, member.groupMemberId) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second) + chatModel.chatsContext.updateGroup(rhId, r.first) + } + } + } +} + +private fun markSupportChatRead(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) { + withBGApi { + if (member.supportChatNotRead) { + val r = chatModel.controller.apiSupportChatRead( + rh = rhId, + type = ChatType.Group, + id = groupInfo.apiId, + scope = GroupChatScope.MemberSupport(member.groupMemberId) + ) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second) + chatModel.chatsContext.updateGroup(rhId, r.first) + } + } + } + } +} 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 703d74f225..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 @@ -26,13 +26,10 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.ChatModel.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.model.GroupInfo -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.chatJsonLength +import chat.simplex.common.platform.* import chat.simplex.res.MR -import kotlinx.coroutines.delay +import kotlinx.coroutines.* private const val maxByteCount = 1200 @@ -51,8 +48,8 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) if (res != null) { gInfo = res - withChats { - updateGroup(rhId, res) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, res) } welcomeText.value = welcome ?: "" } 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 7711ee73af..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 @@ -13,10 +13,10 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull import chat.simplex.common.platform.onRightClick -import chat.simplex.common.views.chat.group.LocalContentTag @Composable fun CIChatFeatureView( + chatsCtx: ChatModel.ChatsContext, chatInfo: ChatInfo, chatItem: ChatItem, feature: Feature, @@ -25,7 +25,7 @@ fun CIChatFeatureView( 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( @@ -72,11 +72,10 @@ 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() - val reversedChatItems = m.chatItemsForContent(LocalContentTag.current).value.asReversed() + val reversedChatItems = chatsCtx.chatItems.value.asReversed() var i = getChatItemIndexOrNull(chatItem, reversedChatItems) if (i != null) { while (i < reversedChatItems.size) { 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 2bcbbe29e0..39bb9545e1 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 @@ -194,7 +194,7 @@ fun CIGroupInvitationViewLongNamePreview() { CIGroupInvitationView( ci = ChatItem.getGroupInvitationSample(), groupInvitation = CIGroupInvitation.getSample( - groupProfile = GroupProfile("group_with_a_really_really_really_long_name", "Group With A Really Really Really Long Name"), + groupProfile = GroupProfile("group_with_a_really_really_really_long_name", "Group With A Really Really Really Long Name", shortDescr = null), status = CIGroupInvitationStatus.Accepted ), memberRole = GroupMemberRole.Admin, 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 401d098bea..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 @@ -8,6 +8,7 @@ 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 @@ -91,12 +92,6 @@ fun CIImageView( } } - @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( @@ -122,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), @@ -140,7 +135,7 @@ fun CIImageView( ) } 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 = {} @@ -264,6 +259,12 @@ fun CIImageView( } } +@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 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 3b2bf63f28..758980059d 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 @@ -3,8 +3,7 @@ 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.HoverInteraction -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.* @@ -30,9 +29,11 @@ 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.chat.group.LocalContentTag +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 dev.icerock.moko.resources.StringResource import kotlinx.datetime.Clock import kotlin.math.* @@ -62,8 +63,9 @@ data class ChatItemReactionMenuItem ( @Composable fun ChatItemView( + chatsCtx: ChatModel.ChatsContext, rhId: Long?, - cInfo: ChatInfo, + chat: Chat, cItem: ChatItem, composeState: MutableState, imageProvider: (() -> ImageGalleryProvider)? = null, @@ -71,17 +73,21 @@ fun ChatItemView( linkMode: SimplexLinkMode, 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, + scrollToItemId: MutableState, scrollToQuotedItemFromItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, @@ -103,6 +109,7 @@ fun ChatItemView( itemSeparation: ItemSeparation, preview: Boolean = false, ) { + val cInfo = chat.chatInfo val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart @@ -112,7 +119,7 @@ fun ChatItemView( val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value Box( - modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier, + modifier = (if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier), contentAlignment = alignment, ) { val info = cItem.meta.itemStatus.statusInto @@ -227,190 +234,418 @@ fun ChatItemView( } } - Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) { - val interactionSource = remember { MutableInteractionSource() } - val enterInteraction = remember { HoverInteraction.Enter() } - KeyChangeEffect(highlighted.value) { - if (highlighted.value) { - interactionSource.emit(enterInteraction) + @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 { - interactionSource.emit(HoverInteraction.Exit(enterInteraction)) + MaterialTheme.colors.secondary.mixWith(Color.Black, mixAlpha) + } + } 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) } } - Column( + + IconButton( + onClick, Modifier - .clipChatItem(cItem, itemSeparation.largeGap, revealed.value) - .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick, interactionSource = interactionSource, indication = LocalIndication.current) - .onRightClick { showMenu.value = true }, + .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 ) { - @Composable - fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToQuotedItemFromItem) - } + Icon(painterResource(icon), null, Modifier.size(iconSize), tint = iconTint) + } + } - 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) + // improvement could be to track "forwarded from" scope and open it + @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() + } + } + } + } - @Composable - fun MsgReactionsMenu() { - val rs = MsgReaction.old.mapNotNull { r -> - if (null == cItem.reactions.find { it.userReacted && it.reaction.text == r.text }) { - r + 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 { - null + bubbleInteractionSource.emit(HoverInteraction.Exit(enterInteraction)) } } - 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) + 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(chatsCtx, chat, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToItemId, 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(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 -> { + @Composable + fun DeleteItemMenu() { DefaultDropdownMenu(showMenu) { - if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { - ArchiveReportItemAction(cItem, showMenu, deleteMessage) - } - DeleteItemAction(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(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.membership.memberRole == GroupMemberRole.Member && !live) { - // ReportItemAction(cItem, composeState, showMenu) - // } - } + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) } } } - cItem.meta.itemDeleted != null -> { + + @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.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 PendingReviewEventItemView() { + Text( + buildAnnotatedString { + withStyle(chatEventStyle.copy(fontWeight = FontWeight.Bold)) { append(cItem.content.text) } + }, + Modifier.padding(horizontal = 6.dp, vertical = 6.dp) + ) + } + + @Composable + fun DeletedItem() { + MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) DefaultDropdownMenu(showMenu) { if (revealed.value) { HideItemAction(revealed, showMenu, reveal) @@ -420,275 +655,151 @@ fun ChatItemView( ExpandItemAction(revealed, showMenu, reveal) } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) } } } - cItem.isDeletedContent -> { - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) - } - } + + @Composable + fun e2eeInfoText(sId: StringResource) { + Text( + buildAnnotatedString { + withStyle(chatEventStyle) { append(annotatedStringResource(sId)) } + }, + Modifier.padding(horizontal = 6.dp, vertical = 6.dp) + ) } - cItem.mergeCategory != null && ((range.value?.count() ?: 0) > 1 || revealed.value) -> { - DefaultDropdownMenu(showMenu) { - if (revealed.value) { - ShrinkItemAction(revealed, showMenu, reveal) + + @Composable + fun E2EEInfoNoPQText() { + e2eeInfoText(MR.strings.e2ee_info_no_pq) + } + + @Composable + fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) { + if (e2EEInfo.pqEnabled != null) { + if (e2EEInfo.pqEnabled) { + e2eeInfoText(MR.strings.e2ee_info_pq) } else { - ExpandItemAction(revealed, showMenu, reveal) + E2EEInfoNoPQText() } - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) - } - } - } - else -> { - DefaultDropdownMenu(showMenu) { - DeleteItemAction(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(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(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() + e2eeInfoText(MR.strings.e2ee_info_e2ee) } + } + + if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { + MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + MarkedDeletedItemDropdownMenu() } else { - framedItemView() - } - MsgContentItemDropdownMenu() - } - } - - @Composable fun LegacyDeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - if (cItem.canBeDeletedForSelf) { - Divider() - SelectItemAction(showMenu, selectChatItem) + 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) + is RcvGroupEvent.NewMemberPendingReview -> PendingReviewEventItemView() + else -> EventItemView() + } + MsgContentItemDropdownMenu() + } + is CIContent.SndGroupEventContent -> { + when (c.sndGroupEvent) { + is SndGroupEvent.UserPendingReview -> PendingReviewEventItemView() + else -> 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.ChatBanner -> Spacer(modifier = Modifier.size(0.dp)) + is CIContent.InvalidJSON -> { + CIInvalidJSONView(c.json) + DeleteItemMenu() + } + } } } } - - @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 = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed() - CIEventView(eventItemViewText(reversedChatItems)) - } - - @Composable - fun DeletedItem() { - MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(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(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() - } + if (!cItem.chatDir.sent) { + GoToItemButton(false, bubbleHovered) } } - if (cItem.content.msgContent != null && (cItem.meta.itemDeleted == null || revealed.value) && cItem.reactions.isNotEmpty()) { ChatItemReactions() } @@ -740,6 +851,7 @@ fun ItemInfoAction( @Composable fun DeleteItemAction( + chatsCtx: ChatModel.ChatsContext, cItem: ChatItem, revealed: State, showMenu: MutableState, @@ -748,14 +860,13 @@ fun DeleteItemAction( deleteMessages: (List) -> Unit, buttonText: String = stringResource(MR.strings.delete_verb), ) { - val contentTag = LocalContentTag.current ItemAction( buttonText, painterResource(MR.images.ic_delete), onClick = { showMenu.value = false if (!revealed.value) { - val reversedChatItems = chatModel.chatItemsForContent(contentTag).value.asReversed() + val reversedChatItems = chatsCtx.chatItems.value.asReversed() val currIndex = chatModel.getChatItemIndexOrNull(cItem, reversedChatItems) val ciCategory = cItem.mergeCategory if (currIndex != null && ciCategory != null) { @@ -807,14 +918,14 @@ fun ModerateItemAction( @Composable fun SelectItemAction( showMenu: MutableState, - selectChatItem: () -> Unit, + selectItem: () -> Unit, ) { ItemAction( stringResource(MR.strings.select_verb), painterResource(MR.images.ic_check_circle), onClick = { showMenu.value = false - selectChatItem() + selectItem() } ) } @@ -914,23 +1025,53 @@ private fun ReportItemAction( } @Composable -private fun ArchiveReportItemAction(cItem: ChatItem, showMenu: MutableState, deleteMessage: (Long, CIDeleteMode) -> Unit) { +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 = { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.report_archive_alert_title), - text = generalGetString(MR.strings.report_archive_alert_desc), - onConfirm = { - deleteMessage(cItem.id, CIDeleteMode.cidmInternalMark) - }, - destructive = true, - confirmText = generalGetString(MR.strings.archive_verb), - ) + 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) }, - color = Color.Red + 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 + ) + } + } + } ) } @@ -1078,7 +1219,7 @@ fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = fal 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 = if (sent && tailVisible) size.width - msgTailWidth else size.width + val width = size.width val height = size.height val rxMax = min(msgBubbleMaxRadius, width / 2) val ryMax = min(msgBubbleMaxRadius, height / 2) @@ -1141,7 +1282,11 @@ sealed class ShapeStyle { data class RoundRect(val radius: Dp) : ShapeStyle() } -fun shapeStyle(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Boolean, revealed: Boolean): ShapeStyle { +val shapeStyle: (chatItem: ChatItem?, tailEnabled: Boolean, tailVisible: Boolean, revealed: Boolean) -> ShapeStyle = + if (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) > 27) ::shapeStyleWithTail + else { _, _, _, _ -> ShapeStyle.RoundRect(msgRectMaxRadius) } + +fun shapeStyleWithTail(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Boolean, revealed: Boolean): ShapeStyle { if (chatItem == null) { return ShapeStyle.RoundRect(msgRectMaxRadius) } @@ -1193,6 +1338,12 @@ fun shapeStyle(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Bo } } +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), @@ -1297,24 +1448,29 @@ fun PreviewChatItemView( chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello") ) { ChatItemView( + chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), rhId = null, - ChatInfo.Direct.sampleData, + Chat.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 = {}, + scrollToItemId = remember { mutableStateOf(null) }, scrollToQuotedItemFromItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, @@ -1343,24 +1499,29 @@ fun PreviewChatItemView( fun PreviewChatItemViewDeletedContent() { SimpleXTheme { ChatItemView( + chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), rhId = null, - ChatInfo.Direct.sampleData, + Chat.sampleData, ChatItem.getDeletedContentSampleData(), 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 = {}, + scrollToItemId = remember { mutableStateOf(null) }, scrollToQuotedItemFromItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, 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 784563dbb2..f36da6c908 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -21,13 +21,17 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.ComposeState import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlin.math.ceil @Composable fun FramedItemView( - chatInfo: ChatInfo, + chatsCtx: ChatModel.ChatsContext, + chat: Chat, ci: ChatItem, uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, @@ -39,8 +43,10 @@ fun FramedItemView( receiveFile: (Long) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, scrollToItem: (Long) -> Unit = {}, + scrollToItemId: MutableState, scrollToQuotedItemFromItem: (Long) -> Unit = {}, ) { + val chatInfo = chat.chatInfo val sent = ci.chatDir.sent val chatTTL = chatInfo.timedMessagesTTL @@ -59,14 +65,18 @@ fun FramedItemView( style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), linkMode = linkMode, uriHandler = if (appPlatform.isDesktop) uriHandler else null, - showTimestamp = showTimestamp + 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,7 +86,7 @@ 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, showTimestamp = showTimestamp) @@ -176,7 +186,7 @@ fun FramedItemView( fun ciFileView(ci: ChatItem, text: String) { CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } @@ -197,12 +207,12 @@ fun FramedItemView( var metaColor = MaterialTheme.colors.secondary Box(contentAlignment = Alignment.BottomEnd) { val chatItemTail = remember { appPreferences.chatItemTail.state } - val style = shapeStyle(ci, chatItemTail.value, tailVisible, revealed = true) + val style = shapeStyle(ci, chatItemTail.value, tailVisible, true) val tailRendered = style is ShapeStyle.Bubble && style.tailVisible Column( Modifier .width(IntrinsicSize.Max) - .padding(start = if (tailRendered) msgTailWidthDp else 0.dp, end = if (sent && tailRendered) msgTailWidthDp else 0.dp) + .padding(start = if (!sent && tailRendered) msgTailWidthDp else 0.dp, end = if (sent && tailRendered) msgTailWidthDp else 0.dp) ) { PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) { @Composable @@ -249,7 +259,11 @@ fun FramedItemView( onLongClick = { showMenu.value = true }, onClick = { if (ci.quotedItem.itemId != null) { - scrollToItem(ci.quotedItem.itemId) + if (ci.isReport && chatsCtx.secondaryContextFilter != null) { + scrollToItemId.value = ci.quotedItem.itemId + } else { + scrollToItem(ci.quotedItem.itemId) + } } else { scrollToQuotedItemFromItem(ci.id) } @@ -285,7 +299,7 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVideo -> { @@ -293,26 +307,26 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVoice -> { CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true }) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCReport -> { @@ -321,9 +335,9 @@ fun FramedItemView( append(if (mc.text.isEmpty()) mc.reason.text else "${mc.reason.text}: ") } } - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix) } - else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + else -> CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -343,7 +357,9 @@ fun FramedItemView( @Composable fun CIMarkdownText( + chatsCtx: ChatModel.ChatsContext, ci: ChatItem, + chat: Chat, chatTTL: Int?, linkMode: SimplexLinkMode, uriHandler: UriHandler?, @@ -353,15 +369,47 @@ fun CIMarkdownText( prefix: AnnotatedString? = null ) { Box(Modifier.padding(vertical = 7.dp, horizontal = 12.dp)) { + val chatInfo = chat.chatInfo val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true, + sendCommandMsg = if (chatInfo.useCommands && chat.chatInfo.sndReady) { { msg -> sendCommandMsg(chatsCtx, chat, msg) } } else null, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, + mentions = ci.mentions, userMemberId = when { + chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId + else -> null + }, uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix ) } } +fun sendCommandMsg(chatsCtx: ChatModel.ChatsContext, chat: Chat, msg: String) { + if (chat.chatInfo.sndReady) { + withLongRunningApi(slow = 60_000) { + val cInfo = chat.chatInfo + val chatItems = + chatModel.controller.apiSendMessages( + rh = chat.remoteHostId, + type = cInfo.chatType, + id = cInfo.apiId, + scope = cInfo.groupChatScope(), + composedMessages = listOf(ComposedMessage(fileSource = null, quotedItemId = null, msgContent = MsgContent.MCText(msg), mentions = emptyMap())) + ) + if (!chatItems.isNullOrEmpty()) { + chatItems.forEach { aChatItem -> + withContext(Dispatchers.Main) { + chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) + } + } + } + } + } else { + AlertManager.shared.showAlertMsg(MR.strings.cant_send_message_alert_title, MR.strings.cant_send_commands_alert_text) + } +} + + const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index d63094cd1d..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 @@ -12,17 +12,15 @@ import androidx.compose.runtime.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* -import chat.simplex.common.model.ChatController.chatModel import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.group.LocalContentTag import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: Int?, revealed: State, showViaProxy: Boolean, showTimestamp: Boolean) { +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( @@ -35,7 +33,7 @@ fun MarkedDeletedItemView(ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: In verticalAlignment = Alignment.CenterVertically ) { Box(Modifier.weight(1f, false)) { - MergedMarkedDeletedText(ci, chatInfo, revealed) + MergedMarkedDeletedText(chatsCtx, ci, chatInfo, revealed) } CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } @@ -43,8 +41,8 @@ fun MarkedDeletedItemView(ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: In } @Composable -private fun MergedMarkedDeletedText(chatItem: ChatItem, chatInfo: ChatInfo, revealed: State) { - val reversedChatItems = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed() +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) { @@ -95,7 +93,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, chatInfo: ChatInfo, reve fun markedDeletedText(cItem: ChatItem, chatInfo: ChatInfo): String = if (cItem.meta.itemDeleted != null && cItem.isReport) { - if (cItem.meta.itemDeleted is CIDeleted.Moderated && cItem.meta.itemDeleted.byGroupMember.groupMemberId != (chatInfo as ChatInfo.Group?)?.groupInfo?.membership?.groupMemberId) { + if (cItem.meta.itemDeleted is CIDeleted.Moderated && cItem.meta.itemDeleted.byGroupMember.groupMemberId != (chatInfo as? ChatInfo.Group)?.groupInfo?.membership?.groupMemberId) { generalGetString(MR.strings.report_item_archived_by).format(cItem.meta.itemDeleted.byGroupMember.displayName) } else { generalGetString(MR.strings.report_item_archived) 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 257ede7d4a..60595fc255 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -1,5 +1,8 @@ package chat.simplex.common.views.chat.item +import SectionItemView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.MaterialTheme @@ -11,18 +14,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* -import androidx.compose.ui.text.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.text.AnnotatedString.Range +import androidx.compose.ui.text.font.* +import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.* +import chat.simplex.res.* import kotlinx.coroutines.* -import java.awt.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) val boldFont = SpanStyle(fontWeight = FontWeight.Medium) @@ -60,7 +62,10 @@ fun MarkdownText ( sender: String? = null, meta: CIMeta? = null, chatTTL: Int? = null, + mentions: Map? = null, + userMemberId: String? = null, toggleSecrets: Boolean, + sendCommandMsg: ((String) -> Unit)? = null, style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp), maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, @@ -134,31 +139,111 @@ fun MarkdownText ( } Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent?.second ?: mapOf()) } else { - var hasAnnotations = false + var hasLinks = false + var hasSecrets = false + var hasCommands = false val annotatedText = buildAnnotatedString { inlineContent?.first?.invoke(this) appendSender(this, sender, senderBold) if (prefix != null) append(prefix) for ((i, ft) in formattedText.withIndex()) { if (ft.format == null) append(ft.text) - else if (toggleSecrets && ft.format is Format.Secret) { - val ftStyle = ft.format.style - hasAnnotations = true - val key = i.toString() - withAnnotation(tag = "SECRET", annotation = key) { - if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) } - } - } else { - val link = ft.link(linkMode) - if (link != null) { - hasAnnotations = true + else when(ft.format) { + is Format.Bold -> withStyle(ft.format.style) { append(ft.text) } + is Format.Italic -> withStyle(ft.format.style) { append(ft.text) } + is Format.StrikeThrough -> withStyle(ft.format.style) { append(ft.text) } + is Format.Snippet -> withStyle(ft.format.style) { append(ft.text) } + is Format.Colored -> withStyle(ft.format.style) { append(ft.text) } + is Format.Secret -> { val ftStyle = ft.format.style - withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) { - withStyle(ftStyle) { append(ft.viewText(linkMode)) } + if (toggleSecrets) { + hasSecrets = true + val key = i.toString() + withAnnotation(tag = "SECRET", annotation = key) { + if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) } + } + } else { + withStyle(ftStyle) { append(ft.text) } } - } else { - withStyle(ft.format.style) { append(ft.text) } } + is Format.Mention -> { + val mention = mentions?.get(ft.format.memberName) + if (mention != null) { + val ftStyle = ft.format.style + if (mention.memberRef != null) { + val displayName = mention.memberRef.displayName + val name = if (mention.memberRef.localAlias.isNullOrEmpty()) { + displayName + } else { + "${mention.memberRef.localAlias} ($displayName)" + } + val mentionStyle = if (mention.memberId == userMemberId) ftStyle.copy(color = MaterialTheme.colors.primary) else ftStyle + withStyle(mentionStyle) { append(mentionText(name)) } + } else { + withStyle(ftStyle) { append(mentionText(ft.format.memberName)) } + } + } else { + append(ft.text) + } + } + is Format.Command -> + if (sendCommandMsg == null) { + append(ft.text) + } else { + hasCommands = true + val ftStyle = ft.format.style + val cmd = ft.format.commandStr + withAnnotation(tag = "COMMAND", annotation = cmd) { + withStyle(ftStyle) { append("/$cmd") } + } + } + is Format.Uri -> { + hasLinks = true + val ftStyle = Format.linkStyle + val s = ft.text + val link = if (s.startsWith("http://") || s.startsWith("https://")) s else "https://$s" + withAnnotation(tag = "WEB_URL", annotation = link) { + withStyle(ftStyle) { append(ft.text) } + } + } + is Format.HyperLink -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "WEB_URL", annotation = ft.format.linkUri) { + withStyle(ftStyle) { append(ft.format.showText ?: ft.text) } + } + } + is Format.SimplexLink -> { + hasLinks = true + val ftStyle = Format.linkStyle + val link = + if (linkMode == SimplexLinkMode.BROWSER && ft.format.showText == null && !ft.text.startsWith("[")) ft.text + else ft.format.simplexUri + val t = ft.format.showText ?: if (linkMode == SimplexLinkMode.DESCRIPTION) ft.format.linkType.description else null + withAnnotation(tag = "SIMPLEX_URL", annotation = link) { + if (t == null) { + withStyle(ftStyle) { append(ft.text) } + } else { + withStyle(ftStyle) { append("$t ") } + withStyle(ftStyle.copy(fontStyle = FontStyle.Italic)) { append(ft.format.viaHosts) } + } + } + } + is Format.Email -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "OTHER_URL", annotation = "mailto:${ft.text}") { + withStyle(ftStyle) { append(ft.text) } + } + } + is Format.Phone -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "OTHER_URL", annotation = "tel:${ft.text}") { + withStyle(ftStyle) { append(ft.text) } + } + } + is Format.Unknown -> append(ft.text) } } if (meta?.isLive == true) { @@ -169,51 +254,51 @@ fun MarkdownText ( withStyle(reserveTimestampStyle) { append("\n" + metaText) } else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } - if (hasAnnotations && uriHandler != null) { + if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null)) { val icon = remember { mutableStateOf(PointerIcon.Default) } ClickableText(annotatedText, style = style, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } + if (hasLinks) { + val withAnnotation: (String, (Range) -> Unit) -> Unit = { tag, f -> + annotatedText.getStringAnnotations(tag, start = offset, end = offset).firstOrNull()?.let(f) + } + withAnnotation("WEB_URL") { a -> onLinkLongClick(a.item) } + withAnnotation("SIMPLEX_URL") { a -> onLinkLongClick(a.item) } + withAnnotation("OTHER_URL") { a -> onLinkLongClick(a.item) } + } }, onClick = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> - try { - uriHandler.openUri(annotation.item) - } catch (e: Exception) { - // It can happen, for example, when you click on a text 0.00001 but don't have any app that can catch - // `tel:` scheme in url installed on a device (no phone app or contacts, maybe) - Log.e(TAG, "Open url: ${e.stackTraceToString()}") - } - } - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> - uriHandler.openVerifiedSimplexUri(annotation.item) - } - annotatedText.getStringAnnotations(tag = "SECRET", start = offset, end = offset) - .firstOrNull()?.let { annotation -> - val key = annotation.item + val withAnnotation: (String, (Range) -> Unit) -> Unit = { tag, f -> + annotatedText.getStringAnnotations(tag, start = offset, end = offset).firstOrNull()?.let(f) + } + if (hasLinks && uriHandler != null) { + withAnnotation("WEB_URL") { a -> openBrowserAlert(a.item, uriHandler) } + withAnnotation("OTHER_URL") { a -> safeOpenUri(a.item, uriHandler) } + withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) } + } + if (hasSecrets) { + withAnnotation("SECRET") { a -> + val key = a.item showSecrets[key] = !(showSecrets[key] ?: false) } + } + if (hasCommands && sendCommandMsg != null) { + withAnnotation("COMMAND") { a -> sendCommandMsg("/${a.item}") } + } }, onHover = { offset -> - icon.value = annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { + val hasAnnotation: (String) -> Boolean = { tag -> annotatedText.hasStringAnnotations(tag, start = offset, end = offset) } + icon.value = + if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) { PointerIcon.Hand - } ?: annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { - PointerIcon.Hand - } ?: annotatedText.getStringAnnotations(tag = "SECRET", start = offset, end = offset) - .firstOrNull()?.let { - PointerIcon.Hand - } ?: PointerIcon.Default + } else { + PointerIcon.Default + } }, shouldConsumeEvent = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any() - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset).any() + annotatedText.hasStringAnnotations(tag = "WEB_URL", start = offset, end = offset) + || annotatedText.hasStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) + || annotatedText.hasStringAnnotations(tag = "OTHER_URL", start = offset, end = offset) } ) } else { @@ -282,6 +367,74 @@ fun ClickableText( ) } +fun openBrowserAlert(uri: String, uriHandler: UriHandler) { + val (res, err) = sanitizeUri(uri) + if (res == null) { + showInvalidLinkAlert(uri, err) + } else { + val message = if (uri.count() > 160) uri.substring(0, 159) + "…" else uri + val sanitizedUri = res.second + if (sanitizedUri == null) { + AlertManager.shared.showAlertDialog( + generalGetString(MR.strings.privacy_chat_list_open_web_link_question), + message, + confirmText = generalGetString(MR.strings.open_verb), + onConfirm = { safeOpenUri(uri, uriHandler) } + ) + } else { + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.privacy_chat_list_open_web_link_question), + message, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + safeOpenUri(uri, uriHandler) + }) { + Text(generalGetString(MR.strings.privacy_chat_list_open_full_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + safeOpenUri(sanitizedUri, uriHandler) + }) { + Text(generalGetString(MR.strings.privacy_chat_list_open_clean_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + }) + } + } +} + +fun safeOpenUri(uri: String, uriHandler: UriHandler) { + try { + uriHandler.openUri(uri) + } catch (e: Exception) { + // It can happen, for example, when you click on a text 0.00001 but don't have any app that can catch + // `tel:` scheme in url installed on a device (no phone app or contacts, maybe) + Log.e(TAG, "Open url: ${e.stackTraceToString()}") + showInvalidLinkAlert(uri, error = e.message) + } +} + +fun showInvalidLinkAlert(uri: String, error: String? = null) { + val message = if (error.isNullOrEmpty()) { uri } else { error + "\n" + uri } + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), message) +} + +fun sanitizeUri(s: String): Pair?, String?> { + val parsed = parseSanitizeUri(s, safe = false) + return if (parsed?.uriInfo != null) { + (true to parsed.uriInfo.sanitized) to null + } else { + null to parsed?.parseError + } +} + private fun isRtl(s: CharSequence): Boolean { for (element in s) { val d = Character.getDirectionality(element) @@ -291,3 +444,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 3ba15bc79c..77ab62dbf1 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 @@ -19,8 +19,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* -import chat.simplex.common.model.ChatModel.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* @@ -63,12 +62,11 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { 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, defaultClickAction) + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) } }, click = defaultClickAction, @@ -88,7 +86,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { 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, defaultClickAction) + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, disabled, linkMode, inProgress.value, progressByTimeout, defaultClickAction) } }, click = defaultClickAction, @@ -104,13 +102,14 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ) } 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, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) } }, - click = { if (chatModel.chatId.value != chat.id) scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } }, + click = defaultClickAction, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { NoteFolderMenuItems(chat, showMenu, showMarkRead) @@ -132,7 +131,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) { onRequestAccepted(it) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { - ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) + ContactRequestMenuItems(chat.remoteHostId, contactRequestId = chat.chatInfo.apiId, chatModel, showMenu) } }, showMenu, @@ -188,8 +187,8 @@ fun ErrorChatListItem() { suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { when { - contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) - else -> openChat(rhId, ChatInfo.Direct(contact)) + contact.isContactCard -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) + else -> openDirectChat(rhId, contact.contactId) } } @@ -197,35 +196,56 @@ suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId) - else -> openChat(rhId, ChatInfo.Group(groupInfo)) + else -> openGroupChat(rhId, groupInfo.groupId) } } -suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat(rhId, ChatInfo.Local(noteFolder)) +suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat(secondaryChatsCtx = null, rhId, ChatInfo.Local(noteFolder)) -suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(rhId, ChatType.Direct, contactId) +suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(secondaryChatsCtx = null, rhId, ChatType.Direct, contactId) -suspend fun openGroupChat(rhId: Long?, groupId: Long, contentTag: MsgContentTag? = null) = openChat(rhId, ChatType.Group, groupId, contentTag) +suspend fun openGroupChat(rhId: Long?, groupId: Long) = openChat(secondaryChatsCtx = null, rhId, ChatType.Group, groupId) -suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, contentTag: MsgContentTag? = null) = openChat(rhId, chatInfo.chatType, chatInfo.apiId, contentTag) +suspend fun openChat(secondaryChatsCtx: ChatModel.ChatsContext?, rhId: Long?, chatInfo: ChatInfo) = openChat(secondaryChatsCtx, rhId, chatInfo.chatType, chatInfo.apiId) -private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long, contentTag: MsgContentTag? = null) = - apiLoadMessages(rhId, chatType, apiId, contentTag, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) +suspend fun openChat( + secondaryChatsCtx: ChatModel.ChatsContext?, + rhId: Long?, + chatType: ChatType, + apiId: Long, + openAroundItemId: Long? = null +) { + if (secondaryChatsCtx != null) { + chatModel.secondaryChatsContext.value = secondaryChatsCtx + } + apiLoadMessages( + chatsCtx = secondaryChatsCtx ?: chatModel.chatsContext, + rhId, + chatType, + apiId, + if (openAroundItemId != null) { + ChatPagination.Around(openAroundItemId, ChatPagination.INITIAL_COUNT) + } else { + ChatPagination.Initial(ChatPagination.INITIAL_COUNT) + }, + "", + openAroundItemId + ) +} -suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) { - withChats(contentTag) { - chatItemStatuses.clear() - chatItems.replaceAll(chat.chatItems) +suspend fun openLoadedChat(chat: Chat) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.replaceAll(chat.chatItems) chatModel.chatId.value = chat.chatInfo.id - chatModel.chatStateForContent(contentTag).clear() + chatModel.chatsContext.chatState.clear() } } -suspend fun apiFindMessages(ch: Chat, search: String, contentTag: MsgContentTag?) { - withChats(contentTag) { - chatItems.clearAndNotify() +suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, search: String) { + withContext(Dispatchers.Main) { + chatsCtx.chatItems.clearAndNotify() } - apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, contentTag, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search = search) + 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 setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope { @@ -244,23 +264,34 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo } 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, showMenu) + if (contact.nextAcceptContactRequest) { + if (contact.contactRequestId != null) { + ContactRequestMenuItems(chat.remoteHostId, contactRequestId = contact.contactRequestId, chatModel, showMenu) + } else if (contact.groupDirectInv != null && !contact.groupDirectInv.memberRemoved) { + MemberContactRequestMenuItems(chat.remoteHostId, contact, showMenu) } else { - MarkUnreadChatAction(chat, chatModel, showMenu) + DeleteContactAction(chat, chatModel, showMenu) } - ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) - ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) - TagListAction(chat, showMenu) - ClearChatAction(chat, showMenu) + } else { + if (contact.activeConn != null) { + if (showMarkRead) { + MarkReadChatAction(chat, showMenu) + } else { + MarkUnreadChatAction(chat, chatModel, showMenu) + } + ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) + ToggleNotificationsChatAction(chat, chatModel, contact.chatSettings.enableNtfs.nextMode(false), showMenu) + TagListAction(chat, showMenu) + ClearChatAction(chat, showMenu) + } + DeleteContactAction(chat, chatModel, showMenu) } - DeleteContactAction(chat, chatModel, showMenu) } @Composable @@ -282,7 +313,7 @@ fun GroupMenuItems( } } GroupMemberStatus.MemAccepted -> { - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu) } if (groupInfo.canDelete) { @@ -296,10 +327,15 @@ fun GroupMenuItems( 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) { + if (groupInfo.membership.memberCurrentOrPending) { LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu) } if (groupInfo.canDelete) { @@ -379,12 +415,12 @@ fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolea } @Composable -fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState) { +fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, nextMsgFilter: MsgFilter, 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), + generalGetString(nextMsgFilter.text(chat.chatInfo.hasMentions)), + painterResource(nextMsgFilter.icon), onClick = { - toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled, chatModel) + toggleNotifications(chat.remoteHostId, chat.chatInfo, nextMsgFilter, chatModel) showMenu.value = false } ) @@ -482,22 +518,46 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh } @Composable -fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState, onSuccess: ((chat: Chat) -> Unit)? = null) { +fun ContactRequestMenuItems(rhId: Long?, contactRequestId: Long, 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, onSuccess) + acceptContactRequest(rhId, incognito = false, contactRequestId, true, chatModel, onSuccess) showMenu.value = false } ) + if (!chatModel.addressShortLinkDataSet()) { + ItemAction( + stringResource(MR.strings.accept_contact_incognito_button), + painterResource(MR.images.ic_theater_comedy), + color = MaterialTheme.colors.onBackground, + onClick = { + acceptContactRequest(rhId, incognito = true, contactRequestId, true, chatModel, onSuccess) + showMenu.value = false + } + ) + } ItemAction( - stringResource(MR.strings.accept_contact_incognito_button), - painterResource(MR.images.ic_theater_comedy), + stringResource(MR.strings.reject_contact_button), + painterResource(MR.images.ic_close), + onClick = { + rejectContactRequest(rhId, contactRequestId, chatModel) + showMenu.value = false + }, + color = Color.Red + ) +} + +@Composable +fun MemberContactRequestMenuItems(rhId: Long?, contact: Contact, 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 = true, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) + acceptMemberContact(rhId, contact.contactId, onSuccess) showMenu.value = false } ) @@ -505,7 +565,7 @@ fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chat stringResource(MR.strings.reject_contact_button), painterResource(MR.images.ic_close), onClick = { - rejectContactRequest(rhId, chatInfo, chatModel) + showRejectMemberContactRequestAlert(rhId, contact) showMenu.value = false }, color = Color.Red @@ -521,7 +581,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 }, @@ -562,15 +622,27 @@ private fun InvalidDataView() { } } +@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) { - withChats { - markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) } - withReportsChatsIfOpen { - markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) } chatModel.controller.apiChatRead( chat.remoteHostId, @@ -587,9 +659,9 @@ fun markChatRead(c: Chat) { false ) if (success) { - withChats { - replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) - markChatTagRead(chat) + withContext(Dispatchers.Main) { + chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + chatModel.chatsContext.markChatTagRead(chat) } } } @@ -609,9 +681,9 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { true ) if (success) { - withChats { - replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) - updateChatTagReadNoContentTag(chat, wasUnread) + withContext(Dispatchers.Main) { + chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + chatModel.chatsContext.updateChatTagReadInPrimaryContext(chat, wasUnread) } } } @@ -625,19 +697,21 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque Column { SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel, onSucess) + acceptContactRequest(rhId, incognito = false, contactRequest.apiId, 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, onSucess) - }) { - Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + if (!chatModel.addressShortLinkDataSet()) { + SectionItemView({ + AlertManager.shared.hideAlert() + acceptContactRequest(rhId, incognito = true, contactRequest.apiId, true, chatModel, onSucess) + }) { + Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } SectionItemView({ AlertManager.shared.hideAlert() - rejectContactRequest(rhId, contactRequest, chatModel) + rejectContactRequest(rhId, contactRequest.apiId, chatModel) }) { Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) } @@ -647,25 +721,47 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque ) } -fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) { +fun acceptContactRequest( + rhId: Long?, + incognito: Boolean, + contactRequestId: Long, + isCurrentUser: Boolean, + chatModel: ChatModel, + close: ((chat: Chat) -> Unit)? = null, + inProgress: MutableState? = null +) { withBGApi { - val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) - if (contact != null && isCurrentUser && contactRequest != null) { + inProgress?.value = true + val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, contactRequestId) + if (contact != null && isCurrentUser) { val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) - withChats { - replaceChat(rhId, contactRequest.id, chat) + withContext(Dispatchers.Main) { + if (contact.contactRequestId != null) { // means contact request was initially created with contact, so we don't need to replace it + chatModel.chatsContext.updateContact(rhId, contact) + } else { + chatModel.chatsContext.replaceChat(rhId, contactRequestChatId(contactRequestId), chat) + } + inProgress?.value = false } - chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) close?.invoke(chat) + } else { + inProgress?.value = false } } } -fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { +fun rejectContactRequest(rhId: Long?, contactRequestId: Long, chatModel: ChatModel, dismissToChatList: Boolean = false) { withBGApi { - chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) - withChats { - removeChat(rhId, contactRequest.id) + val contact_ = chatModel.controller.apiRejectContactRequest(rhId, contactRequestId) + withContext(Dispatchers.Main) { + if (contact_ != null) { // means contact request was initially created with contact, so we need to remove contact chat + chatModel.chatsContext.removeChat(rhId, contact_.id) + } else { + chatModel.chatsContext.removeChat(rhId, contactRequestChatId(contactRequestId)) + } + if (dismissToChatList) { + chatModel.chatId.value = null + } } } } @@ -682,8 +778,8 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti withBGApi { AlertManager.shared.hideAlert() if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) { - withChats { - removeChat(rhId, connection.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, connection.id) } onSuccess() } @@ -703,8 +799,8 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId) if (r) { - withChats { - removeChat(rhId, chatInfo.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, chatInfo.id) } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null @@ -725,33 +821,49 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( close: (() -> Unit)?, openChat: Boolean ) { + @Composable + fun UseCurrentProfileButton() { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { + val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) + if (ok && openChat) { + close?.invoke() + openDirectChat(rhId, contact.contactId) + } + } + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + + @Composable + fun UseIncognitoProfileButton(text: String) { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { + val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) + if (ok && openChat) { + close?.invoke() + openDirectChat(rhId, contact.contactId) + } + } + }) { + Text(text, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName), buttons = { Column { - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - close?.invoke() - val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) - if (ok && openChat) { - openDirectChat(rhId, contact.contactId) - } - } - }) { - Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - close?.invoke() - val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) - if (ok && openChat) { - openDirectChat(rhId, contact.contactId) - } - } - }) { - Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + if (!contact.profileChangeProhibited) { + UseCurrentProfileButton() + UseIncognitoProfileButton(generalGetString(MR.strings.connect_use_new_incognito_profile)) + } else if (!contact.contactConnIncognito) { + UseCurrentProfileButton() + } else { + UseIncognitoProfileButton(generalGetString(MR.strings.connect_use_incognito_profile)) } SectionItemView({ AlertManager.privacySensitive.hideAlert() @@ -767,8 +879,8 @@ 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) { - withChats { - updateContact(rhId, contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, contact) } AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), @@ -810,8 +922,8 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId) if (r) { - withChats { - removeChat(rhId, groupInfo.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, groupInfo.id) } if (chatModel.chatId.value == groupInfo.id) { chatModel.chatId.value = null @@ -830,8 +942,8 @@ fun groupInvitationAcceptedAlert(rhId: Long?) { ) } -fun toggleNotifications(remoteHostId: Long?, chatInfo: ChatInfo, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState? = null) { - val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None) +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) } @@ -840,13 +952,13 @@ fun toggleChatFavorite(remoteHostId: Long?, chatInfo: ChatInfo, favorite: Boolea updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel) } -fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState? = null) { +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(chatInfo) { - ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings)) + ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings), groupChatScope = null) } else -> null } @@ -865,26 +977,45 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch val wasUnread = chat?.unreadTag ?: false val wasFavorite = chatInfo.chatSettings?.favorite ?: false chatModel.updateChatFavorite(favorite = chatSettings.favorite, wasFavorite) - withChats { - updateChatInfo(remoteHostId, newChatInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatInfo(remoteHostId, newChatInfo) } - if (chatSettings.enableNtfs != MsgFilter.All) { + if (chatSettings.enableNtfs == MsgFilter.None) { ntfManager.cancelNotificationsForChat(chatInfo.id) } val updatedChat = chatModel.getChat(chatInfo.id) if (updatedChat != null) { - withChats { - updateChatTagReadNoContentTag(updatedChat, wasUnread) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatTagReadInPrimaryContext(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, @@ -924,7 +1055,6 @@ fun PreviewChatListNavLinkDirect() { null, null, null, - null, disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, @@ -970,7 +1100,6 @@ fun PreviewChatListNavLinkGroup() { null, null, null, - null, disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = 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 3205a084f0..2109e21bfe 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 @@ -27,7 +27,6 @@ import androidx.compose.ui.unit.* import chat.simplex.common.AppLock import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs -import chat.simplex.common.model.ChatController.setConditionsNotified import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -38,8 +37,6 @@ 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.common.views.usersettings.networkAndServers.ConditionsLinkButton -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 @@ -58,6 +55,7 @@ sealed class ActiveFilter { } private fun showNewChatSheet(oneHandUI: State) { + connectProgressManager.cancelConnectProgress() ModalManager.start.closeModals() ModalManager.end.closeModals() chatModel.newChatSheetVisible.value = true @@ -127,32 +125,19 @@ fun ToggleChatListCard() { @Composable fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { val oneHandUI = remember { appPrefs.oneHandUI.state } - val rhId = chatModel.remoteHostId() LaunchedEffect(Unit) { val showWhatsNew = shouldShowWhatsNew(chatModel) val showUpdatedConditions = chatModel.conditions.value.conditionsAction?.shouldShowNotice ?: false - if (showWhatsNew) { + if (showWhatsNew || showUpdatedConditions) { delay(1000L) ModalManager.center.showCustomModal { close -> WhatsNewView(close = close, updatedConditions = showUpdatedConditions) } - } else if (showUpdatedConditions) { - ModalManager.center.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> - LaunchedEffect(Unit) { - val conditionsId = chatModel.conditions.value.currentConditions.conditionsId - try { - setConditionsNotified(rh = rhId, conditionsId = conditionsId) - } catch (e: Exception) { - Log.d(TAG, "UsageConditionsView setConditionsNotified error: ${e.message}") - } - } - UsageConditionsView(userServers = mutableStateOf(emptyList()), currUserServers = mutableStateOf(emptyList()), close = close, rhId = rhId) - } } } if (appPlatform.isDesktop) { KeyChangeEffect(chatModel.chatId.value) { - if (chatModel.chatId.value != null && !ModalManager.end.isLastModalOpen(ModalViewId.GROUP_REPORTS)) { + if (chatModel.chatId.value != null && !ModalManager.end.isLastModalOpen(ModalViewId.SECONDARY_CHAT)) { ModalManager.end.closeModalsExceptFirst() } AudioPlayer.stop() @@ -364,22 +349,64 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow 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 (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) } } } - if (!stopped) { + } + + if (stopped) { + barButtons.add { + IconButton(onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.chat_is_stopped_indication), + generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) + ) + }) { + Icon( + painterResource(MR.images.ic_report_filled), + generalGetString(MR.strings.chat_is_stopped_indication), + tint = Color.Red, + ) + } + } + } else { + if (connectProgressManager.showConnectProgress != null) { + barButtons.add { + Box(Modifier.padding(horizontal = DEFAULT_PADDING_HALF)) { + CIFileViewScope.progressIndicator() + } + } + } + + 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)) + } + } + } + } + barButtons.add { IconButton( onClick = { @@ -404,38 +431,6 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow } } - 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( - 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, - ) - } - } - } val clipboard = LocalClipboardManager.current val scope = rememberCoroutineScope() val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } } @@ -606,7 +601,8 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { chatModel.appOpenUrl.value = rhId to uri } else { withBGApi { - planAndConnect(rhId, uri, incognito = null, close = null) + chatModel.appOpenUrlConnecting.value = true + planAndConnect(rhId, uri, close = null, cleanup = { chatModel.appOpenUrlConnecting.value = false }) } } } @@ -664,7 +660,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState // if SimpleX link is pasted, show connection dialogue hideKeyboard(view) if (link.format is Format.SimplexLink) { - val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts) + val linkText = link.format.simplexLinkText searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) } searchShowingSimplexLink.value = true @@ -674,8 +670,13 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState if (it.isNotEmpty()) { // if some other text is pasted, enter search mode focusRequester.requestFocus() - } else if (listState.layoutInfo.totalItemsCount > 0) { - listState.scrollToItem(0) + } else { + if (!chatModel.appOpenUrlConnecting.value) { + connectProgressManager.cancelConnectProgress() + } + if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } } searchShowingSimplexLink.value = false searchChatFilteredBySimplexLink.value = null @@ -693,7 +694,6 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState< planAndConnect( chatModel.remoteHostId(), link, - incognito = null, filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id }, close = null, @@ -1219,7 +1219,7 @@ 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.chatStats.unreadChat || chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0 + is ActiveFilter.Unread -> chat.unreadTag else -> true } @@ -1228,7 +1228,7 @@ fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo, chatStats: Chat 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.Direct -> !chatInfo.contact.isContactCard && !chatInfo.contact.chatDeleted is ChatInfo.ContactRequest -> true is ChatInfo.ContactConnection -> true is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Customer 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 ba7334522a..4280845867 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 @@ -19,7 +19,6 @@ 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.input.pointer.pointerHoverIcon import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontStyle @@ -43,7 +42,6 @@ fun ChatPreviewView( chatModelDraft: ComposeState?, chatModelDraftChatId: ChatId?, currentUserProfileDisplayName: String?, - contactNetworkStatus: NetworkStatus?, disabled: Boolean, linkMode: SimplexLinkMode, inProgress: Boolean, @@ -71,6 +69,7 @@ fun ChatPreviewView( } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemRejected -> inactiveIcon() GroupMemberStatus.MemLeft -> inactiveIcon() GroupMemberStatus.MemRemoved -> inactiveIcon() GroupMemberStatus.MemGroupDeleted -> inactiveIcon() @@ -137,108 +136,155 @@ fun ChatPreviewView( fun chatPreviewTitle() { val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } when (cInfo) { - is ChatInfo.Direct -> + is ChatInfo.Direct -> { Row(verticalAlignment = Alignment.CenterVertically) { if (cInfo.contact.verified) { VerifiedIcon() } - chatPreviewTitleText( - if (deleting) - MaterialTheme.colors.secondary - else - Color.Unspecified - ) + val color = if (deleting) + MaterialTheme.colors.secondary + else if ((cInfo.contact.nextAcceptContactRequest && cInfo.contact.groupDirectInv?.memberRemoved != true) || cInfo.contact.sendMsgToConnect) { + MaterialTheme.colors.primary + } else if (!cInfo.contact.sndReady) { + MaterialTheme.colors.secondary + } else { + Color.Unspecified + } + chatPreviewTitleText(color = color) } - is ChatInfo.Group -> - when (cInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> chatPreviewTitleText( - if (inProgress || deleting) - MaterialTheme.colors.secondary - else - if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary - ) - GroupMemberStatus.MemAccepted -> chatPreviewTitleText(MaterialTheme.colors.secondary) - else -> chatPreviewTitleText( - if (deleting) - MaterialTheme.colors.secondary - else - Color.Unspecified - ) + } + is ChatInfo.Group -> { + val color = if (deleting) { + MaterialTheme.colors.secondary + } else { + when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemInvited -> if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary + GroupMemberStatus.MemAccepted, GroupMemberStatus.MemRejected -> MaterialTheme.colors.secondary + else -> if (cInfo.groupInfo.nextConnectPrepared) MaterialTheme.colors.primary else Color.Unspecified + } } + chatPreviewTitleText(color = color) + } else -> chatPreviewTitleText() } } @Composable - fun chatPreviewText() { - 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, sp20) } - ci.meta.itemDeleted == null -> ci.text 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 + fun chatPreviewInfoText(): Pair? { + return when (cInfo) { + is ChatInfo.Direct -> + if (cInfo.contact.isContactCard) { + stringResource(MR.strings.contact_tap_to_connect) to MaterialTheme.colors.primary + } else if (cInfo.contact.isBot && cInfo.contact.nextConnectPrepared) { + stringResource(MR.strings.open_to_use_bot) to Color.Unspecified + } else if (cInfo.contact.sendMsgToConnect) { + stringResource(MR.strings.open_to_connect) to Color.Unspecified + } else if (cInfo.contact.nextAcceptContactRequest) { + stringResource(MR.strings.open_to_accept) to Color.Unspecified + } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null && cInfo.contact.active) { + if ((cInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con && !cInfo.contact.isBot) || cInfo.contact.contactGroupMemberId != null) { + stringResource(MR.strings.contact_should_accept) to Color.Unspecified + } else { + stringResource(MR.strings.contact_connection_pending) to Color.Unspecified + } + } else { + null } - MarkdownText( - text, - formattedText, - sender = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> null - cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName + is ChatInfo.Group -> + if (cInfo.groupInfo.nextConnectPrepared) { + stringResource( + if (cInfo.groupInfo.businessChat?.chatType == BusinessChatType.Business) MR.strings.open_to_connect + else MR.strings.group_preview_open_to_join + ) to Color.Unspecified + } else { + when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemRejected -> stringResource(MR.strings.group_preview_rejected) to Color.Unspecified + GroupMemberStatus.MemInvited -> groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo) to Color.Unspecified + GroupMemberStatus.MemAccepted -> stringResource(MR.strings.group_connection_pending) to Color.Unspecified + GroupMemberStatus.MemPendingReview, GroupMemberStatus.MemPendingApproval -> + stringResource(MR.strings.reviewed_by_admins) to MaterialTheme.colors.secondary else -> null - }, - toggleSecrets = false, - linkMode = linkMode, - senderBold = true, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = TextStyle( - fontFamily = Inter, - fontSize = 15.sp, - color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, - lineHeight = 21.sp - ), - inlineContent = inlineTextContent, - modifier = Modifier.fillMaxWidth(), - prefix = prefix - ) + } + } + + else -> null + } + } + + @Composable + fun chatPreviewText() { + val previewText = chatPreviewInfoText() + val ci = chat.chatItems.lastOrNull() + if (chatModelDraftChatId == chat.id && chatModelDraft != null) { + val sp20 = with(LocalDensity.current) { 20.sp.toDp() } + val (text: CharSequence, inlineTextContent) = remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) } + val formattedText = null + MarkdownText( + text, + formattedText, + toggleSecrets = false, + linkMode = linkMode, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ), + inlineContent = inlineTextContent, + modifier = Modifier.fillMaxWidth() + ) + } else if (ci?.content?.hasMsgContent != true && previewText != null) { + Text(previewText.first, color = previewText.second) + } else if (ci != null && showChatPreviews) { + val (text: CharSequence, inlineTextContent) = when { + ci.meta.itemDeleted == null -> ci.text to null + else -> markedDeletedText(ci, chat.chatInfo) to null } - } else { - when (cInfo) { - is ChatInfo.Direct -> - 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.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) { - Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary) + val formattedText = when { + 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}: ") } } - is ChatInfo.Group -> - when (cInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo)) - GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary) - else -> {} - } - else -> {} + + else -> null } + + MarkdownText( + text, + formattedText, + sender = when { + cInfo is ChatInfo.Group && !ci.chatDir.sent && !ci.meta.showGroupAsSender -> 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 = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ), + inlineContent = inlineTextContent, + modifier = Modifier.fillMaxWidth(), + prefix = prefix + ) } } @@ -251,37 +297,9 @@ fun ChatPreviewView( val uriHandler = LocalUriHandler.current when (mc) { is MsgContent.MCLink -> SmallContentPreview { - val linkClicksEnabled = remember { appPrefs.privacyChatListOpenLinks.state }.value != PrivacyChatListOpenLinksMode.NO - IconButton({ - when (appPrefs.privacyChatListOpenLinks.get()) { - PrivacyChatListOpenLinksMode.YES -> uriHandler.openUriCatching(mc.preview.uri) - PrivacyChatListOpenLinksMode.NO -> defaultClickAction() - PrivacyChatListOpenLinksMode.ASK -> AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.privacy_chat_list_open_web_link_question), - text = mc.preview.uri, - buttons = { - Column { - if (chatModel.chatId.value != chat.id) { - SectionItemView({ - AlertManager.shared.hideAlert() - defaultClickAction() - }) { - Text(stringResource(MR.strings.open_chat), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - SectionItemView({ - AlertManager.shared.hideAlert() - uriHandler.openUriCatching(mc.preview.uri) - } - ) { - Text(stringResource(MR.strings.privacy_chat_list_open_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - } - ) - } - }, - if (linkClicksEnabled) Modifier.desktopPointerHoverIconHand() else Modifier, + IconButton( + { openBrowserAlert(mc.preview.uri, uriHandler) }, + Modifier.desktopPointerHoverIconHand(), ) { Image(base64ToBitmap(mc.preview.image), null, contentScale = ContentScale.Crop) } @@ -330,34 +348,15 @@ fun ChatPreviewView( @Composable fun chatStatusImage() { - if (cInfo is ChatInfo.Direct) { - if (cInfo.contact.active && cInfo.contact.activeConn != null) { - val descr = contactNetworkStatus?.statusString - when (contactNetworkStatus) { - is NetworkStatus.Connected -> - IncognitoIcon(chat.chatInfo.incognito) - - is NetworkStatus.Error -> - Icon( - painterResource(MR.images.ic_error), - contentDescription = descr, - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(19.sp.toDp()) - .offset(x = 2.sp.toDp()) - ) - - else -> - progressView() - } - } else { - IncognitoIcon(chat.chatInfo.incognito) - } - } else if (cInfo is ChatInfo.Group) { + if (cInfo is ChatInfo.Group) { if (progressByTimeout) { progressView() } else if (chat.chatStats.reportsCount > 0) { - GroupReportsIcon() + FlagIcon(color = MaterialTheme.colors.error) + } else if (chat.supportUnreadCount > 0) { + FlagIcon(color = MaterialTheme.colors.primary) + } else if (chat.chatInfo.groupInfo_?.membership?.memberPending == true) { + FlagIcon(color = MaterialTheme.colors.secondary) } else { IncognitoIcon(chat.chatInfo.incognito) } @@ -366,103 +365,132 @@ fun ChatPreviewView( } } - Row { - Box(contentAlignment = Alignment.BottomEnd) { - ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier) - Box(Modifier.padding(end = 6.sp.toDp(), bottom = 6.sp.toDp())) { - chatPreviewImageOverlayIcon() - } - } - Spacer(Modifier.width(8.dp)) - Column(Modifier.weight(1f)) { - Row { - Box(Modifier.weight(1f)) { - chatPreviewTitle() + 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() } - 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) + Spacer(Modifier.width(8.dp)) + Column(Modifier.weight(1f)) { + Row { + Box(Modifier.weight(1f)) { + chatPreviewTitle() } - 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() + 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) } - } - 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) + 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() } - else -> if (playing.fileSource.filePath != ci?.file?.fileSource?.filePath) { - activeVoicePreview.value = null + } + 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) + } + + 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) } } } - 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 { + UnreadBadge( + text = if (n > 0) unreadCountStr(n) else "", + backgroundColor = if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, + yOffset = 3.dp + ) + } + } + } 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() } - } - } - - Spacer(Modifier.width(8.sp.toDp())) - - Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) { - 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) { - 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) { - Icon( - painterResource(MR.images.ic_notifications_off_filled), - 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() } } } @@ -506,11 +534,11 @@ fun IncognitoIcon(incognito: Boolean) { } @Composable -fun GroupReportsIcon() { +fun FlagIcon(color: Color) { Icon( painterResource(MR.images.ic_flag), contentDescription = null, - tint = MaterialTheme.colors.error, + tint = color, modifier = Modifier .size(21.sp.toDp()) .offset(x = 2.sp.toDp()) @@ -525,6 +553,26 @@ private fun groupInvitationPreviewText(currentUserProfileDisplayName: String?, g stringResource(MR.strings.group_preview_you_are_invited) } +@Composable +fun UnreadBadge( + text: String, + backgroundColor: Color, + yOffset: Dp? = null +) { + Text( + text, + color = Color.White, + fontSize = 10.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .offset(y = yOffset ?: 0.dp) + .background(backgroundColor, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 2.sp.toDp()) + .padding(vertical = 1.sp.toDp()) + ) +} + @Composable fun unreadCountStr(n: Int): String { return if (n < 1000) "$n" else "${n / 1000}" + stringResource(MR.strings.thousand_abbreviation) @@ -561,6 +609,6 @@ private data class ActiveVoicePreview( @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, "", disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false, {}) } } 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 index acbc72ff48..ed1c7116e6 100644 --- 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 @@ -64,7 +64,7 @@ enum class SubscriptionColorType { ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED } -data class SubscriptionStatus( +data class AppSubscriptionStatus( val color: SubscriptionColorType, val variableValue: Float, val statusPercent: Float @@ -75,7 +75,7 @@ fun subscriptionStatusColorAndPercentage( socksProxy: String?, subs: SMPServerSubs, hasSess: Boolean -): SubscriptionStatus { +): AppSubscriptionStatus { fun roundedToQuarter(n: Float): Float = when { n >= 1 -> 1f n <= 0 -> 0f @@ -83,25 +83,25 @@ fun subscriptionStatusColorAndPercentage( } val activeColor: SubscriptionColorType = if (socksProxy != null) SubscriptionColorType.ACTIVE_SOCKS_PROXY else SubscriptionColorType.ACTIVE - val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 0f) + val noConnColorAndPercent = AppSubscriptionStatus(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) + AppSubscriptionStatus(activeColor, 0f, 0f) else if (subs.ssActive == 0) { if (hasSess) - SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) + AppSubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) else noConnColorAndPercent } else { // ssActive > 0 if (hasSess) - SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) + AppSubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) else // This would mean implementation error - SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive) + AppSubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive) } } @@ -120,7 +120,7 @@ fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, hasSess: Boolean, leadi 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()}%" + val percentageText = if (subs.total > 0 || hasSess) "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%" else "%" Row( verticalAlignment = Alignment.CenterVertically, 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 index 1b563e6d02..8dfe138da1 100644 --- 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 @@ -31,8 +31,7 @@ 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.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen +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 @@ -42,6 +41,7 @@ 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) { @@ -416,15 +416,15 @@ private fun setTag(rhId: Long?, tagId: Long?, chat: Chat, close: () -> Unit) { when (val cInfo = chat.chatInfo) { is ChatInfo.Direct -> { val contact = cInfo.contact.copy(chatTags = result.second) - withChats { - updateContact(rhId, contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, contact) } } is ChatInfo.Group -> { val group = cInfo.groupInfo.copy(chatTags = result.second) - withChats { - updateGroup(rhId, group) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, group) } } @@ -447,21 +447,19 @@ private fun deleteTag(rhId: Long?, tag: ChatTag, saving: MutableState) val tagId = tag.chatTagId if (apiDeleteChatTag(rhId, tagId)) { chatModel.userTags.value = chatModel.userTags.value.filter { it.chatTagId != tagId } - if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) { - chatModel.activeChatTagFilter.value = null - } + 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 }) - withChats { - updateContact(rhId, contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, contact) } } is ChatInfo.Group -> { val group = cInfo.groupInfo.copy(chatTags = cInfo.groupInfo.chatTags.filter { it != tagId }) - withChats { - updateGroup(rhId, group) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, group) } } else -> {} 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 185ec3925f..ed74e083e7 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 @@ -131,7 +131,7 @@ fun UserPicker( } LaunchedEffect(Unit) { // Controller.ctrl can be null when self-destructing activates - if (controller.ctrl != null && controller.ctrl != -1L) { + if (controller.hasChatCtrl()) { withBGApi { controller.reloadRemoteHosts() } @@ -634,7 +634,7 @@ fun HostDisconnectButton(onClick: (() -> Unit)?) { } @Composable -fun BoxScope.unreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) { +fun BoxScope.userUnreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) { Text( if (unreadCount > 0) unreadCountStr(unreadCount) else "", color = Color.White, 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 index da70aef621..371e9072ea 100644 --- 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 @@ -5,7 +5,6 @@ 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.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.item.ItemAction @@ -54,15 +53,9 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel click = { hideKeyboard(view) when (contactType) { - ContactType.RECENT -> { + ContactType.RECENT, ContactType.CONTACT_WITH_REQUEST, ContactType.CHAT_DELETED -> { withApi { - openChat(rhId, chat.chatInfo) - ModalManager.start.closeModals() - } - } - ContactType.CHAT_DELETED -> { - withApi { - openChat(rhId, chat.chatInfo) + openChat(secondaryChatsCtx = null, rhId, chat.chatInfo) ModalManager.start.closeModals() } } @@ -80,7 +73,28 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel }, dropdownMenuItems = { tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { - DeleteContactAction(chat, chatModel, showMenu) + if (contactType == ContactType.CONTACT_WITH_REQUEST) { + if (chat.chatInfo.contact.contactRequestId != null) { + ContactRequestMenuItems( + rhId = chat.remoteHostId, + contactRequestId = chat.chatInfo.contact.contactRequestId, + chatModel = chatModel, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } else if (chat.chatInfo.contact.groupDirectInv != null && !chat.chatInfo.contact.groupDirectInv.memberRemoved) { + MemberContactRequestMenuItems( + rhId = chat.remoteHostId, + contact = chat.chatInfo.contact, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } else { + DeleteContactAction(chat, chatModel, showMenu) + } + } else { + DeleteContactAction(chat, chatModel, showMenu) + } } }, showMenu, @@ -109,7 +123,7 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { ContactRequestMenuItems( rhId = chat.remoteHostId, - chatInfo = chat.chatInfo, + contactRequestId = chat.chatInfo.apiId, chatModel = chatModel, showMenu = showMenu, onSuccess = { onRequestAccepted(it) } 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 index dd03bca921..636887275c 100644 --- 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 @@ -31,18 +31,23 @@ fun ContactPreviewView( Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) } + 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.CONTACT_WITH_REQUEST -> + if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.groupDirectInv?.memberRemoved == true) + MaterialTheme.colors.secondary + else + MaterialTheme.colors.primary + contactType == ContactType.REQUEST -> MaterialTheme.colors.primary + contactType == ContactType.RECENT -> if (chat.chatInfo.nextConnect) MaterialTheme.colors.primary else Color.Unspecified + else -> Color.Unspecified + } + @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) { @@ -85,11 +90,11 @@ fun ContactPreviewView( Spacer(Modifier.fillMaxWidth().weight(1f)) - if (chat.chatInfo is ChatInfo.ContactRequest) { + if (chat.chatInfo is ChatInfo.ContactRequest || contactType == ContactType.CONTACT_WITH_REQUEST) { Icon( painterResource(MR.images.ic_check), contentDescription = null, - tint = MaterialTheme.colors.primary, + tint = textColor, modifier = Modifier .size(23.dp) ) @@ -103,6 +108,9 @@ fun ContactPreviewView( modifier = Modifier .size(21.dp) ) + if (chat.chatInfo.incognito) { + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + } } if (showDeletedChatIcon && chat.chatInfo.chatDeleted) { 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 c2e1d67d50..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 @@ -435,7 +435,7 @@ suspend fun encryptDatabase( } val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value) appPrefs.encryptionStartedAt.set(null) - val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError + val sqliteError = ((error as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError when { sqliteError is SQLiteError.ErrorNotADatabase -> { operationEnded(m, progressIndicator) { @@ -449,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 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 4a3e1cda54..d55d89f26b 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 @@ -21,8 +21,6 @@ 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.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -36,6 +34,7 @@ import java.nio.file.StandardCopyOption import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList +import kotlinx.coroutines.* @Composable fun DatabaseView() { @@ -367,6 +366,7 @@ fun startChat( chatDbChanged: MutableState, progressIndicator: MutableState? = null ) { + Log.d(TAG, "startChat") withLongRunningApi { try { progressIndicator?.value = true @@ -533,20 +533,20 @@ fun deleteChatDatabaseFilesAndState() { appPrefs.newDatabaseInitialized.set(false) chatModel.desktopOnboardingRandomPassword.value = false controller.appPrefs.storeDBPassphrase.set(true) - controller.ctrl = null + controller.setChatCtrl(null) // Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself chatModel.chatId.value = null withLongRunningApi { - withChats { - chatItems.clearAndNotify() - chats.clear() - popChatCollector.clear() + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() + chatModel.chatsContext.chats.clear() + chatModel.chatsContext.popChatCollector.clear() } - withReportsChatsIfOpen { - chatItems.clearAndNotify() - chats.clear() - popChatCollector.clear() + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.chatItems?.clearAndNotify() + chatModel.secondaryChatsContext.value?.chats?.clear() + chatModel.secondaryChatsContext.value?.popChatCollector?.clear() } } chatModel.users.clear() @@ -785,10 +785,10 @@ private fun afterSetCiTTL( appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) withApi { try { - withChats { + 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()) - updateChats(chats) + chatModel.chatsContext.updateChats(chats) } } catch (e: Exception) { Log.e(TAG, "apiGetChats error: ${e.message}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 6bfcf2809f..dc0a86b8fb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatModel @@ -253,7 +254,8 @@ class AlertManager { CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp) } } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } } @@ -266,6 +268,88 @@ class AlertManager { hostDevice: Pair? = null, ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm, hostDevice) + fun showOpenChatAlert( + profileName: String, + profileFullName: String, + profileImage: @Composable () -> Unit, + confirmText: String = generalGetString(MR.strings.connect_plan_open_chat), + onConfirm: () -> Unit, + dismissText: String = generalGetString(MR.strings.cancel_verb), + onDismiss: (() -> Unit)?, + ) { + showAlert { + AlertDialog( + onDismissRequest = { + onDismiss?.invoke() + hideAlert() + }, + buttons = { + AlertContent(text = null as String?, null) { + Column( + Modifier + .padding(top = DEFAULT_PADDING_HALF) + .width(360.dp), + verticalArrangement = Arrangement.SpaceEvenly + ) { + Column( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally + ) { + profileImage() + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text( + profileName, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.h4, + lineHeight = 20.sp, + fontWeight = FontWeight.SemiBold, + maxLines = 2, + modifier = Modifier.fillMaxWidth() + ) + + if (profileFullName.isNotEmpty() && profileFullName != profileName) { + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text( + profileFullName, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2, + maxLines = 2, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Column( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + 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 = { + onConfirm.invoke() + hideAlert() + }, Modifier.focusRequester(focusRequester)) { + Text(confirmText) + } + TextButton(onClick = { + onDismiss?.invoke() + hideAlert() + }) { + Text(dismissText) + } + } + } + } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) + ) + } + } + @Composable fun showInView() { alertViews.collectAsState().value.lastOrNull()?.invoke() 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 4fdbd97d23..a55b1a0d56 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt @@ -2,10 +2,14 @@ package chat.simplex.common.views.helpers import androidx.compose.animation.core.* -fun chatListAnimationSpec() = tween(durationMillis = 250, easing = FastOutSlowInEasing) - -fun newChatSheetAnimSpec() = tween(256, 0, LinearEasing) +fun chatListAnimationSpec() = tween(durationMillis = 350, easing = FastOutSlowInEasing) fun audioProgressBarAnimationSpec() = tween(durationMillis = 30, easing = LinearEasing) -fun userPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) +fun userPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) + +fun mentionPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) + +fun commandMenuAnimSpec() = tween(350, 0, FastOutSlowInEasing) + +fun contextUserPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index c3e97dd27b..5f3a73e7ea 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 @@ -12,12 +12,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 @@ -31,13 +28,9 @@ import kotlin.math.max fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false) { val icon = when (chatInfo) { - is ChatInfo.Group -> - when (chatInfo.groupInfo.businessChat?.chatType) { - BusinessChatType.Business -> MR.images.ic_work_filled_padded - BusinessChatType.Customer -> MR.images.ic_account_circle_filled - null -> MR.images.ic_supervised_user_circle_filled - } + is ChatInfo.Group -> chatInfo.groupInfo.chatIconName is ChatInfo.Local -> MR.images.ic_folder_filled + is ChatInfo.Direct -> chatInfo.contact.chatIconName else -> MR.images.ic_account_circle_filled } ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor) @@ -61,7 +54,8 @@ fun ProfileImage( icon: ImageResource = MR.images.ic_account_circle_filled, color: Color = MaterialTheme.colors.secondaryVariant, backgroundColor: Color? = null, - blurred: Boolean = false + blurred: Boolean = false, + async: Boolean = false ) { Box(Modifier.size(size)) { if (image == null) { @@ -89,13 +83,22 @@ fun ProfileImage( ) } } else { - val imageBitmap = base64ToBitmap(image) - Image( - imageBitmap, - stringResource(MR.strings.image_descr_profile_image), - contentScale = ContentScale.Crop, - modifier = ProfileIconModifier(size, blurred = blurred) - ) + if (async) { + Base64AsyncImage( + base64ImageString = image, + contentDescription = stringResource(MR.strings.image_descr_profile_image), + contentScale = ContentScale.Crop, + modifier = ProfileIconModifier(size, blurred = blurred) + ) + } else { + val imageBitmap = base64ToBitmap(image) + Image( + bitmap = imageBitmap, + contentDescription = stringResource(MR.strings.image_descr_profile_image), + contentScale = ContentScale.Crop, + modifier = ProfileIconModifier(size, blurred = blurred) + ) + } } } } 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 7ed91adbd9..893ff5a467 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 @@ -46,7 +46,7 @@ fun ExposedDropDownSetting( horizontalArrangement = Arrangement.End ) { Text( - values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), + (values.firstOrNull { it.first == selection.value }?.second ?: "") + (if (label != null) " $label" else ""), Modifier.widthIn(max = maxWidth), maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -120,8 +120,10 @@ fun ExposedDropDownSettingWithIcon( ), contentAlignment = Alignment.Center ) { - val choice = values.first { it.first == selection.value } - Icon(painterResource(choice.second), choice.third, Modifier.padding(boxSize * iconPaddingPercent).fillMaxSize(), tint = iconColor) + val choice = values.firstOrNull { it.first == selection.value } + if (choice != null) { + 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 cce7cf17a5..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 @@ -9,6 +9,7 @@ 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 @@ -19,6 +20,8 @@ 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 @@ -40,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() @@ -123,10 +136,15 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel @Composable fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State, onLongClick: () -> Unit) { - Column(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { + 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() 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 3e24629ab1..21520f5424 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 @@ -32,6 +32,7 @@ fun ModalView( searchAlwaysVisible: Boolean = false, onSearchValueChanged: (String) -> Unit = {}, endButtons: @Composable RowScope.() -> Unit = {}, + appBar: @Composable (BoxScope.() -> Unit)? = null, content: @Composable BoxScope.() -> Unit, ) { if (showClose && showAppBar) { @@ -48,14 +49,20 @@ fun ModalView( StatusBarBackground() } Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { - DefaultAppBar( - navigationButton = if (showClose) {{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }} else null, - onTop = !oneHandUI.value, - showSearch = showSearch, - searchAlwaysVisible = searchAlwaysVisible, - onSearchValueChanged = onSearchValueChanged, - buttons = endButtons - ) + 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 + ) + } } } } @@ -78,7 +85,8 @@ class ModalData(val keyboardCoversBar: Boolean = true) { } enum class ModalViewId { - GROUP_REPORTS + SECONDARY_CHAT, + CONTEXT_USER_PICKER_INCOGNITO } class ModalManager(private val placement: ModalPlacement? = null) { @@ -154,13 +162,20 @@ class ModalManager(private val placement: ModalPlacement? = null) { fun closeModal() { if (modalViews.isNotEmpty()) { - if (modalViews.lastOrNull()?.animated == 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 } fun closeModals() { + chatModel.secondaryChatsContext.value = null modalViews.clear() toRemove.clear() _modalCount.value = 0 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt index 5fa097fb6b..3d21cfaf0f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt @@ -58,6 +58,7 @@ class ProcessedErrors (val interval: Long) { text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr), ) } + else -> {} } } } 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 37bf5b10b1..7ee52af784 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 @@ -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?, @@ -73,7 +91,7 @@ fun SectionViewSelectable( } } } - SectionTextFooter(values.first { it.value == currentValue.value }.description) + SectionTextFooter(values.firstOrNull { it.value == currentValue.value }?.description ?: AnnotatedString("")) } @Composable @@ -203,7 +221,7 @@ fun SectionItemWithValue( horizontalArrangement = Arrangement.End ) { Text( - values.first { it.value == currentValue.value }.title + (if (label != null) " $label" else ""), + (values.firstOrNull { it.value == currentValue.value }?.title ?: "") + (if (label != null) " $label" else ""), maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colors.secondary 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 65e1864935..f86edb0388 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 @@ -33,7 +33,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { } } else { val r: LAResult = if (passcode.value == authRequest.password) { - if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) { + if (authRequest.selfDestruct && sdPassword != null && controller.getChatCtrl() == -1L) { initChatControllerOnStart() } LAResult.Success @@ -58,7 +58,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( if (m.chatRunning.value == true) { stopChatAsync(m) } - val ctrl = m.controller.ctrl + val ctrl = m.controller.getChatCtrl() if (ctrl != null && ctrl != -1L) { /** * The following sequence can bring a user here: @@ -82,7 +82,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( } var profile: Profile? = null if (!displayName.isNullOrEmpty()) { - profile = Profile(displayName = displayName, fullName = "") + profile = Profile(displayName = displayName, fullName = "", shortDescr = null) } val createdUser = m.controller.apiCreateActiveUser(null, profile, pastTimestamp = true) m.currentUser.value = createdUser 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 8588e0e981..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 @@ -468,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 ) } } @@ -556,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 -> { @@ -578,7 +579,7 @@ private fun MutableState.startUploading( 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( @@ -692,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") @@ -701,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 1a28bbf589..6199621c39 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 @@ -580,12 +580,13 @@ private fun MutableState.startDownloading( ) { 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, networkProxy, ctrl) + 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) { @@ -595,22 +596,22 @@ private fun MutableState.startDownloading( 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, 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, networkProxy) } else { - Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.chatError)}") + Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.err)}") } } else -> Log.d(TAG, "unsupported event: ${msg.responseType}") @@ -632,7 +633,7 @@ private fun MutableState.startDownloading( private fun MutableState.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { withLongRunningApi { try { - if (ChatController.ctrl == null || ChatController.ctrl == -1L) { + if (!ChatController.hasChatCtrl()) { chatInitControllerRemovingDatabases() } controller.apiDeleteStorage() @@ -739,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") @@ -748,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/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 2380c64a4c..e8084e055a 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 @@ -18,7 +18,6 @@ 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.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.group.AddGroupMembersView import chat.simplex.common.views.chatlist.setGroupMembers @@ -26,10 +25,12 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* import chat.simplex.common.views.* import chat.simplex.common.views.chat.group.GroupLinkView +import chat.simplex.common.views.chatlist.openGroupChat import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.net.URI @Composable @@ -42,11 +43,9 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c withBGApi { val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile) if (groupInfo != null) { - withChats { - updateGroup(rhId = rhId, groupInfo) - chatItems.clearAndNotify() - chatItemStatuses.clear() - chatModel.chatId.value = groupInfo.id + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId = rhId, groupInfo) + openGroupChat(rhId, groupInfo.groupId) } setGroupMembers(rhId, groupInfo, chatModel) closeAll.invoke() @@ -57,7 +56,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c } } else { ModalManager.end.showModalCloseable(true) { close -> - GroupLinkView(chatModel, rhId, groupInfo, connReqContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close) + GroupLinkView(chatModel, rhId, groupInfo, groupLink = null, onGroupLinkUpdated = null, creatingGroup = true, close) } } } @@ -141,6 +140,7 @@ fun AddGroupLayout( createGroup(incognito.value, GroupProfile( displayName = displayName.value.trim(), fullName = "", + shortDescr = null, image = profileImage.value, groupPreferences = GroupPreferences(history = GroupPreference(GroupFeatureEnabled.ON)) )) 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 1b5b475b35..434cb6ce27 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -3,17 +3,18 @@ package chat.simplex.common.views.newchat import SectionItemView import androidx.compose.foundation.layout.* import androidx.compose.material.* +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.model.* -import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.* -import java.net.URI enum class ConnectionLinkType { INVITATION, CONTACT, GROUP @@ -21,83 +22,91 @@ enum class ConnectionLinkType { suspend fun planAndConnect( rhId: Long?, - uri: String, - incognito: Boolean?, + shortOrFullLink: String, close: (() -> Unit)?, cleanup: (() -> Unit)? = null, filterKnownContact: ((Contact) -> Unit)? = null, filterKnownGroup: ((GroupInfo) -> Unit)? = null, +): CompletableDeferred { + connectProgressManager.cancelConnectProgress() + val inProgress = mutableStateOf(true) + connectProgressManager.startConnectProgress(generalGetString(MR.strings.loading_profile)) { + inProgress.value = false + cleanup?.invoke() + } + return planAndConnectTask(rhId, shortOrFullLink, close, cleanup, filterKnownContact, filterKnownGroup, inProgress) +} + +private suspend fun planAndConnectTask( + rhId: Long?, + shortOrFullLink: String, + close: (() -> Unit)?, + cleanup: (() -> Unit)? = null, + filterKnownContact: ((Contact) -> Unit)? = null, + filterKnownGroup: ((GroupInfo) -> Unit)? = null, + inProgress: MutableState ): CompletableDeferred { val completable = CompletableDeferred() - val close: (() -> Unit)? = { + val close: (() -> Unit) = { close?.invoke() // if close was called, it means the connection was created completable.complete(true) } - val cleanup: (() -> Unit)? = { + val cleanup: (() -> Unit) = { cleanup?.invoke() completable.complete(!completable.isActive) } - val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri) - if (connectionPlan != null) { - val link = strHasSingleSimplexLink(uri.trim()) + val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, inProgress = inProgress) + connectProgressManager.stopConnectProgress() + if (!inProgress.value) { return completable } + 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)}" + "

${link.format.simplexLinkText}" else "" when (connectionPlan) { is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) { - InvitationLinkPlan.Ok -> { - Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito") - if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) + is InvitationLinkPlan.Ok -> + if (connectionPlan.invitationLinkPlan.contactSLinkData_ != null) { + Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, short link data present") + showPrepareContactAlert( + rhId, + connectionLink, + connectionPlan.invitationLinkPlan.contactSLinkData_, + close, + cleanup + ) } else { + Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, no short link data") 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, cleanup = cleanup, ) } - } InvitationLinkPlan.OwnLink -> { - Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + 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) } }, - onDismiss = cleanup, - onDismissRequest = cleanup, - destructive = true, - hostDevice = hostDevice(rhId), - ) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, 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, - cleanup = cleanup, - ) - } + Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink") + askCurrentOrIncognitoProfileAlert( + 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, + cleanup = cleanup, + ) } is InvitationLinkPlan.Connecting -> { - Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito") + Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting") val contact = connectionPlan.invitationLinkPlan.contact_ if (contact != null) { if (filterKnownContact != null) { filterKnownContact(contact) } else { - openKnownContact(chatModel, rhId, close, contact) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, - hostDevice = hostDevice(rhId), - ) - cleanup?.invoke() + showOpenKnownContactAlert(chatModel, rhId, close, contact) + cleanup() } } else { AlertManager.privacySensitive.showAlertMsg( @@ -105,185 +114,130 @@ 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 -> { - Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito") + Log.d(TAG, "planAndConnect, .InvitationLink, .Known") val contact = connectionPlan.invitationLinkPlan.contact if (filterKnownContact != null) { filterKnownContact(contact) } else { - openKnownContact(chatModel, rhId, close, contact) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, - hostDevice = hostDevice(rhId), - ) - cleanup?.invoke() + showOpenKnownContactAlert(chatModel, rhId, close, contact) + cleanup() } } } is ConnectionPlan.ContactAddress -> when (connectionPlan.contactAddressPlan) { - ContactAddressPlan.Ok -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito") - if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) + is ContactAddressPlan.Ok -> + if (connectionPlan.contactAddressPlan.contactSLinkData_ != null) { + Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, short link data present") + showPrepareContactAlert( + rhId, + connectionLink, + connectionPlan.contactAddressPlan.contactSLinkData_, + close, + cleanup + ) } else { + Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, no short link data") 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, cleanup, ) } - } ContactAddressPlan.OwnLink -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + 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) } }, - destructive = true, - onDismiss = cleanup, - onDismissRequest = cleanup, - hostDevice = hostDevice(rhId), - ) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, 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, - cleanup = cleanup, - ) - } + Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink") + askCurrentOrIncognitoProfileAlert( + 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, + cleanup = cleanup, + ) } ContactAddressPlan.ConnectingConfirmReconnect -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_plan_repeat_connection_request), - text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + 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) } }, - onDismiss = cleanup, - onDismissRequest = cleanup, - destructive = true, - hostDevice = hostDevice(rhId), - ) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, 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, - cleanup = cleanup, - ) - } + Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect") + askCurrentOrIncognitoProfileAlert( + 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, + cleanup = cleanup, + ) } is ContactAddressPlan.ConnectingProhibit -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito") + Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit") val contact = connectionPlan.contactAddressPlan.contact if (filterKnownContact != null) { filterKnownContact(contact) } else { - openKnownContact(chatModel, rhId, close, contact) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, - hostDevice = hostDevice(rhId), - ) - cleanup?.invoke() + showOpenKnownContactAlert(chatModel, rhId, close, contact) + cleanup() } } is ContactAddressPlan.Known -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito") + Log.d(TAG, "planAndConnect, .ContactAddress, .Known") val contact = connectionPlan.contactAddressPlan.contact if (filterKnownContact != null) { filterKnownContact(contact) } else { - openKnownContact(chatModel, rhId, close, contact) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, - hostDevice = hostDevice(rhId), - ) - cleanup?.invoke() + showOpenKnownContactAlert(chatModel, rhId, close, contact) + cleanup() } } is ContactAddressPlan.ContactViaAddress -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress, incognito=$incognito") + Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress") val contact = connectionPlan.contactAddressPlan.contact - if (incognito != null) { - close?.invoke() - connectContactViaAddress(chatModel, rhId, contact.contactId, incognito) - } else { - askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) - } - cleanup?.invoke() + askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) + cleanup() } } is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { - GroupLinkPlan.Ok -> { - Log.d(TAG, "planAndConnect, .GroupLink, .Ok, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - 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) } }, - onDismiss = cleanup, - onDismissRequest = cleanup, - hostDevice = hostDevice(rhId), + is GroupLinkPlan.Ok -> + if (connectionPlan.groupLinkPlan.groupSLinkData_ != null) { + Log.d(TAG, "planAndConnect, .GroupLink, .Ok, short link data present") + showPrepareGroupAlert( + rhId, + connectionLink, + connectionPlan.groupLinkPlan.groupSLinkData_, + close, + cleanup ) } else { + Log.d(TAG, "planAndConnect, .GroupLink, .Ok, no short link data") 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, cleanup = cleanup, ) } - } is GroupLinkPlan.OwnLink -> { - Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito") + Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink") val groupInfo = connectionPlan.groupLinkPlan.groupInfo if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - ownGroupLinkConfirmConnect(chatModel, rhId, uri, linkText, incognito, connectionPlan, groupInfo, close, cleanup) + ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, connectionPlan, groupInfo, close, cleanup) } } GroupLinkPlan.ConnectingConfirmReconnect -> { - Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_plan_repeat_join_request), - text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + 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) } }, - onDismiss = cleanup, - onDismissRequest = cleanup, - destructive = true, - hostDevice = hostDevice(rhId), - ) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, 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, - cleanup = cleanup, - ) - } + Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect") + askCurrentOrIncognitoProfileAlert( + 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, + cleanup = cleanup, + ) } is GroupLinkPlan.ConnectingProhibit -> { - Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito") + Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit") val groupInfo = connectionPlan.groupLinkPlan.groupInfo_ if (groupInfo != null) { if (groupInfo.businessChat == null) { @@ -304,45 +258,31 @@ suspend fun planAndConnect( hostDevice = hostDevice(rhId), ) } - cleanup?.invoke() + cleanup() } is GroupLinkPlan.Known -> { - Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") + Log.d(TAG, "planAndConnect, .GroupLink, .Known") val groupInfo = connectionPlan.groupLinkPlan.groupInfo if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - openKnownGroup(chatModel, rhId, close, groupInfo) - if (groupInfo.businessChat == null) { - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.connect_plan_group_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + linkText, - hostDevice = hostDevice(rhId), - ) - } else { - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.connect_plan_chat_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_connected_with_vName), groupInfo.displayName) + linkText, - hostDevice = hostDevice(rhId), - ) - } - cleanup?.invoke() + showOpenKnownGroupAlert(chatModel, rhId, close, groupInfo) + cleanup() } } } + is ConnectionPlan.Error -> { + Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}") + askCurrentOrIncognitoProfileAlert( + chatModel, rhId, connectionLink, connectionPlan = null, close, + title = generalGetString(MR.strings.connect_plan_connect_via_link), + connectDestructive = false, + cleanup = cleanup, + ) + } } } else { - Log.d(TAG, "planAndConnect, plan error") - if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close, cleanup) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan = null, close, - title = generalGetString(MR.strings.connect_plan_connect_via_link), - connectDestructive = false, - cleanup = cleanup, - ) - } + cleanup() } return completable } @@ -350,17 +290,17 @@ suspend fun planAndConnect( suspend fun connectViaUri( chatModel: ChatModel, rhId: Long?, - uri: String, + connLink: CreatedConnLink, incognito: Boolean, connectionPlan: ConnectionPlan?, close: (() -> Unit)?, cleanup: (() -> Unit)?, ): Boolean { - val pcc = chatModel.controller.apiConnect(rhId, incognito, uri) - val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION + val pcc = chatModel.controller.apiConnect(rhId, incognito, connLink) + val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) ?: ConnectionLinkType.INVITATION else ConnectionLinkType.INVITATION if (pcc != null) { - withChats { - updateContactConnection(rhId, pcc) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, pcc) } close?.invoke() AlertManager.privacySensitive.showAlertMsg( @@ -378,18 +318,19 @@ suspend fun connectViaUri( 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: String, + connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, close: (() -> Unit)?, title: String, @@ -406,7 +347,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) @@ -414,7 +355,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) @@ -432,6 +373,34 @@ fun askCurrentOrIncognitoProfileAlert( ) } +fun openChat_(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, chat: Chat) { + withBGApi { + close?.invoke() + openChat(secondaryChatsCtx = null, rhId, chat.chatInfo) + } +} + +val alertProfileImageSize = 138.dp + +private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = contact.profile.displayName, + profileFullName = contact.profile.fullName, + profileImage = { + ProfileImage( + size = alertProfileImageSize, + image = contact.profile.image, + icon = contact.chatIconName + ) + }, + confirmText = generalGetString(if (contact.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat), + onConfirm = { + openKnownContact(chatModel, rhId, close, contact) + }, + onDismiss = null + ) +} + fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) { withBGApi { val c = chatModel.getContactChat(contact.contactId) @@ -445,9 +414,8 @@ fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, co fun ownGroupLinkConfirmConnect( chatModel: ChatModel, rhId: Long?, - uri: String, + connectionLink: CreatedConnLink, linkText: String, - incognito: Boolean?, connectionPlan: ConnectionPlan?, groupInfo: GroupInfo, close: (() -> Unit)?, @@ -466,38 +434,23 @@ fun ownGroupLinkConfirmConnect( }) { Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } - if (incognito != null) { - // Join incognito / Join with current profile - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) - } - }) { - Text( - if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error - ) + // Use current profile + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup) } - } else { - // Use current profile - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) - } - }) { - Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - // Use new incognito profile - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) - } - }) { - Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Use new incognito profile + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup) } + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) } // Cancel SectionItemView({ @@ -513,6 +466,31 @@ fun ownGroupLinkConfirmConnect( ) } +private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = groupInfo.groupProfile.displayName, + profileFullName = groupInfo.groupProfile.fullName, + profileImage = { + ProfileImage( + size = alertProfileImageSize, + image = groupInfo.groupProfile.image, + icon = groupInfo.chatIconName + ) + }, + confirmText = generalGetString( + if (groupInfo.businessChat == null) { + if (groupInfo.nextConnectPrepared) MR.strings.connect_plan_open_new_group else MR.strings.connect_plan_open_group + } else { + if (groupInfo.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat + } + ), + onConfirm = { + openKnownGroup(chatModel, rhId, close, groupInfo) + }, + onDismiss = null + ) +} + fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) { withBGApi { val g = chatModel.getGroupChat(groupInfo.groupId) @@ -522,3 +500,74 @@ fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, grou } } } + +fun showPrepareContactAlert( + rhId: Long?, + connectionLink: CreatedConnLink, + contactShortLinkData: ContactShortLinkData, + close: (() -> Unit)?, + cleanup: (() -> Unit)? +) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = contactShortLinkData.profile.displayName, + profileFullName = contactShortLinkData.profile.fullName, + profileImage = { + ProfileImage( + size = alertProfileImageSize, + image = contactShortLinkData.profile.image, + icon = + if (contactShortLinkData.business) MR.images.ic_work_filled_padded + else if (contactShortLinkData.profile.peerType == ChatPeerType.Bot) MR.images.ic_cube + else MR.images.ic_account_circle_filled + ) + }, + confirmText = generalGetString(MR.strings.connect_plan_open_new_chat), + onConfirm = { + AlertManager.privacySensitive.hideAlert() + withBGApi { + val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData) + if (chat != null) { + withContext(Dispatchers.Main) { + ChatController.chatModel.chatsContext.addChat(chat) + openChat_(chatModel, rhId, close, chat) + } + } + cleanup?.invoke() + } + }, + onDismiss = { + cleanup?.invoke() + } + ) +} + +fun showPrepareGroupAlert( + rhId: Long?, + connectionLink: CreatedConnLink, + groupShortLinkData: GroupShortLinkData, + close: (() -> Unit)?, + cleanup: (() -> Unit)? +) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = groupShortLinkData.groupProfile.displayName, + profileFullName = groupShortLinkData.groupProfile.fullName, + profileImage = { ProfileImage(size = alertProfileImageSize, image = groupShortLinkData.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled) }, + confirmText = generalGetString(MR.strings.connect_plan_open_new_group), + onConfirm = { + AlertManager.privacySensitive.hideAlert() + withBGApi { + val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, groupShortLinkData) + if (chat != null) { + withContext(Dispatchers.Main) { + ChatController.chatModel.chatsContext.addChat(chat) + openChat_(chatModel, rhId, close, chat) + } + } + cleanup?.invoke() + } + }, + onDismiss = { + cleanup?.invoke() + } + ) +} 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 1623f8510d..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.* @@ -22,24 +22,24 @@ import chat.simplex.common.views.chat.LocalAliasEditor import chat.simplex.common.views.chatlist.deleteContactConnectionAlert import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.ChatModel.withChats 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, conn = contactConnection) + 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]. @@ -54,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) @@ -75,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() { @@ -127,13 +127,19 @@ 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) } @@ -149,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, @@ -185,8 +191,8 @@ fun DeleteButton(onClick: () -> Unit) { private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi { chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let { - withChats { - updateContactConnection(rhId, it) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, it) } } } @@ -201,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 cb4991c99f..ef6e426141 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -29,6 +29,7 @@ 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.item.CIFileViewScope import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.contacts.* @@ -40,6 +41,12 @@ import kotlinx.coroutines.flow.filter @Composable fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { + DisposableEffect(Unit) { + onDispose { + connectProgressManager.cancelConnectProgress() + } + } + val oneHandUI = remember { appPrefs.oneHandUI.state } Box { @@ -73,7 +80,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { } enum class ContactType { - CARD, REQUEST, RECENT, CHAT_DELETED, UNLISTED + CARD, CONTACT_WITH_REQUEST, REQUEST, RECENT, CHAT_DELETED, UNLISTED } fun chatContactType(chat: Chat): ContactType { @@ -81,9 +88,9 @@ fun chatContactType(chat: Chat): ContactType { 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.nextAcceptContactRequest -> ContactType.CONTACT_WITH_REQUEST + contact.isContactCard -> ContactType.CARD contact.chatDeleted -> ContactType.CHAT_DELETED contact.contactStatus == ContactStatus.Active -> ContactType.RECENT else -> ContactType.UNLISTED @@ -127,7 +134,7 @@ private fun ModalData.NewChatSheetLayout( 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 baseContactTypes = remember { listOf(ContactType.CARD, ContactType.CONTACT_WITH_REQUEST, ContactType.REQUEST, ContactType.RECENT) } val contactTypes by remember(searchText.value.text.isEmpty()) { derivedStateOf { contactTypesSearchTargets(baseContactTypes, searchText.value.text.isEmpty()) } } @@ -472,6 +479,13 @@ private fun ContactsSearchBar( ) { searchText.value = searchText.value.copy(it) } + + if (connectProgressManager.showConnectProgress != null) { + Box(Modifier.padding(end = DEFAULT_PADDING_HALF)) { + CIFileViewScope.progressIndicator(sizeMultiplier = 0.75f) + } + } + val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } if (hasText.value) { val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } @@ -505,10 +519,8 @@ private fun ContactsSearchBar( // if SimpleX link is pasted, show connection dialogue hideKeyboard(view) if (link.format is Format.SimplexLink) { - val linkText = - link.simplexLinkText(link.format.linkType, link.format.smpHosts) - searchText.value = - searchText.value.copy(linkText, selection = TextRange.Zero) + val linkText = link.format.simplexLinkText + searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) } searchShowingSimplexLink.value = true searchChatFilteredBySimplexLink.value = null @@ -522,8 +534,11 @@ private fun ContactsSearchBar( if (it.isNotEmpty()) { // if some other text is pasted, enter search mode focusRequester.requestFocus() - } else if (listState.layoutInfo.totalItemsCount > 0) { - listState.scrollToItem(0) + } else { + connectProgressManager.cancelConnectProgress() + if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } } searchShowingSimplexLink.value = false searchChatFilteredBySimplexLink.value = null @@ -557,7 +572,6 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState< planAndConnect( chatModel.remoteHostId(), link, - incognito = null, filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, close = close, cleanup = cleanup, 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 923c0256a8..f520a86999 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,6 +4,7 @@ import SectionBottomSpacer import SectionItemView import SectionTextFooter import SectionView +import SectionViewWithButton import TextIconSpaced import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource @@ -12,6 +13,7 @@ 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.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.* import androidx.compose.runtime.* @@ -31,15 +33,14 @@ 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.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.CIFileViewScope import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.* -import java.net.URI enum class NewChatOption { INVITE, CONNECT @@ -50,17 +51,17 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC val selection = remember { stateGetOrPut("selection") { selection } } val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } } val contactConnection: MutableState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) } - val connReqInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connReq ?: "" } } + 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) { @@ -145,12 +146,12 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC Modifier .fillMaxWidth() .heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp), - verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connReqInvitation.isEmpty()) Arrangement.Center else Arrangement.Top + 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, connReqInvitation, creatingConnReq) + PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq) } NewChatOption.CONNECT.ordinal -> { ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) @@ -164,17 +165,17 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC } @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) } } } @@ -187,7 +188,7 @@ private fun updateShownConnection(conn: PendingContactConnection) { chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy( conn = conn, connId = conn.id, - connReq = conn.connReqInv ?: "", + connLink = conn.connLinkInv ?: CreatedConnLink("", null), connChatUsed = true ) } @@ -315,8 +316,8 @@ fun ActiveProfilePicker( if (contactConnection != null) { updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId) if (updatedConn != null) { - withChats { - updateContactConnection(rhId, updatedConn) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, updatedConn) updateShownConnection(updatedConn) } } @@ -338,8 +339,8 @@ fun ActiveProfilePicker( } if (updatedConn != null) { - withChats { - updateContactConnection(user.remoteHostId, updatedConn) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(user.remoteHostId, updatedConn) } } @@ -368,8 +369,8 @@ fun ActiveProfilePicker( appPreferences.incognito.set(true) val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true) if (conn != null) { - withChats { - updateContactConnection(rhId, conn) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, conn) updateShownConnection(conn) } close() @@ -380,14 +381,7 @@ fun ActiveProfilePicker( } }, 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)) + IncognitoOptionImage() }, onInfo = { ModalManager.start.showModal { IncognitoView() } }, ) @@ -410,7 +404,10 @@ fun ActiveProfilePicker( val activeProfile = filteredProfiles.firstOrNull { it.activeUser } if (activeProfile != null) { - val otherProfiles = filteredProfiles.filter { it.userId != activeProfile.userId } + val otherProfiles = + filteredProfiles + .filter { it.userId != activeProfile.userId } + .sortedByDescending { it.activeOrder } item { when { !showIncognito -> @@ -451,15 +448,21 @@ fun ActiveProfilePicker( } @Composable -private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState) { - SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { - LinkTextView(connReqInvitation, true) - } - +private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState) { + val showShortLink = remember { mutableStateOf(true) } Spacer(Modifier.height(10.dp)) - SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase(), headerBottomPadding = 5.dp) { - SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() }) + 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)) @@ -530,6 +533,32 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection } } +@Composable +fun ToggleShortLinkButton(short: MutableState) { + if (appPrefs.developerTools.state.value) { + 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 IncognitoOptionImage() { + 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)) +} + @Composable fun AddContactLearnMoreButton() { IconButton( @@ -549,6 +578,12 @@ fun AddContactLearnMoreButton() { @Composable private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, pastedLink: MutableState, close: () -> Unit) { + DisposableEffect(Unit) { + onDispose { + connectProgressManager.cancelConnectProgress() + } + } + SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) { PasteLinkView(rhId, pastedLink, showQRCodeScanner, close) } @@ -589,10 +624,25 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState, showQRC ) } }) { - Text(stringResource(MR.strings.tap_to_paste_link)) + Box(Modifier.weight(1f)) { + Text(stringResource(MR.strings.tap_to_paste_link)) + } + if (connectProgressManager.showConnectProgress != null) { + CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f) + } } } else { - LinkTextView(pastedLink.value, false) + Row( + Modifier.padding(end = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.weight(1f)) { + LinkTextView(pastedLink.value, false) + } + if (connectProgressManager.showConnectProgress != null) { + CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f) + } + } } } @@ -670,24 +720,23 @@ private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanu rhId, link, close = close, - cleanup = cleanup, - incognito = null + cleanup = cleanup ).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) { - withChats { - updateContactConnection(rhId, r.second) - chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false, conn = 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 { 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..74c9a55ecf 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,34 @@ 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), + small = short && connLink.connShortLink != null, + modifier, + padding, + tintColor, + withLogo, + onShare, + ) +} + @Composable fun SimpleXLinkQRCode( connReq: String, @@ -30,6 +51,7 @@ fun SimpleXLinkQRCode( ) { QRCode( simplexChatLink(connReq), + small = connReq.count() < 200, modifier, padding, tintColor, @@ -38,17 +60,10 @@ 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, + small: Boolean = false, modifier: Modifier = Modifier, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), tintColor: Color = Color(0xff062d56), @@ -56,9 +71,11 @@ fun QRCode( onShare: (() -> Unit)? = null, ) { val scope = rememberCoroutineScope() + val logoSize = if (small) 0.21f else 0.16f + val errorLevel = if (small) QrCode.ErrorLevel.M else QrCode.ErrorLevel.L val qr = remember(connReq, tintColor, withLogo) { - qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) - .let { if (withLogo) it.addLogo() else it } + qrCodeBitmap(connReq, 1024, errorLevel).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) + .let { if (withLogo) it.addLogo(logoSize) else it } } Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Image( @@ -67,12 +84,13 @@ fun QRCode( Modifier .padding(padding) .widthIn(max = 400.dp) + .fillMaxWidth(if (small) 0.63f else 1f) .aspectRatio(1f) .then(modifier) .clickable { scope.launch { - val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) - .let { if (withLogo) it.addLogo() else it } + val image = qrCodeBitmap(connReq, 1024, errorLevel).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) + .let { if (withLogo) it.addLogo(logoSize) else it } val file = saveTempImageUncompressed(image, true) if (file != null) { shareFile("", CryptoFile.plain(file.absolutePath)) @@ -84,8 +102,8 @@ fun QRCode( } } -fun qrCodeBitmap(content: String, size: Int = 1024): ImageBitmap { - val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate() +fun qrCodeBitmap(content: String, size: Int = 1024, errorLevel: QrCode.ErrorLevel): ImageBitmap { + val qrCode = QrCodeEncoder().addAutomatic(content).setError(errorLevel).fixate() /** See [QrCodeGeneratorImage.initialize] and [FiducialImageEngine.configure] for size calculation */ val numModules = QrCode.totalModules(qrCode.version) // Hide border on light themes to make it fit to the same place as camera in QRCodeScanner. diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 2f84166362..9c6c0fa635 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -7,15 +7,17 @@ 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.AnnotatedString -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.TextStyle + 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.* @@ -27,11 +29,7 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -fun ModalData.ChooseServerOperators( - onboarding: Boolean, - close: (() -> Unit) = { ModalManager.fullscreen.closeModals() }, - modalManager: ModalManager = ModalManager.fullscreen -) { +fun ModalData.OnboardingConditionsView() { LaunchedEffect(Unit) { prepareChatBeforeFinishingOnboarding() } @@ -39,8 +37,74 @@ fun ModalData.ChooseServerOperators( 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(), 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), @@ -53,7 +117,7 @@ fun ModalData.ChooseServerOperators( Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingInformationButton( stringResource(MR.strings.how_it_helps_privacy), - onClick = { modalManager.showModal { ChooseServerOperatorsInfoView(modalManager) } } + onClick = { ModalManager.fullscreen.showModal { ChooseServerOperatorsInfoView() } } ) } @@ -77,42 +141,11 @@ fun ModalData.ChooseServerOperators( } Spacer(Modifier.weight(1f)) - val reviewForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } - val canReviewLater = reviewForOperators.all { it.conditionsAcceptance.usageAllowed } - val currEnabledOperatorIds = serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { val enabled = selectedOperatorIds.value.isNotEmpty() - when { - reviewForOperators.isNotEmpty() -> ReviewConditionsButton(enabled, onboarding, selectedOperators, selectedOperatorIds, modalManager) - selectedOperatorIds.value != currEnabledOperatorIds && enabled -> SetOperatorsButton(true, onboarding, serverOperators, selectedOperatorIds, close) - else -> ContinueButton(enabled, onboarding, close) - } - if (onboarding && reviewForOperators.isEmpty()) { - TextButtonBelowOnboardingButton(stringResource(MR.strings.operator_conditions_of_use)) { - modalManager.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> - UsageConditionsView( - currUserServers = remember { mutableStateOf(emptyList()) }, - userServers = remember { mutableStateOf(emptyList()) }, - close = close, - rhId = null, - ) - } - } - } else if (onboarding || reviewForOperators.isEmpty()) { - // Reserve space - TextButtonBelowOnboardingButton("", null) - } - if (!onboarding && reviewForOperators.isNotEmpty()) { - ReviewLaterButton(canReviewLater, close) - SectionTextFooter( - annotatedStringResource(MR.strings.onboarding_network_operators_conditions_will_be_accepted) + - AnnotatedString(" ") + - annotatedStringResource(MR.strings.onboarding_network_operators_conditions_you_can_configure), - textAlign = TextAlign.Center - ) - SectionBottomSpacer() - } + SetOperatorsButton(enabled, close) + // Reserve space + TextButtonBelowOnboardingButton("", null) } } } @@ -167,121 +200,39 @@ private fun CircleCheckbox(checked: Boolean) { } @Composable -private fun ReviewConditionsButton( - enabled: Boolean, - onboarding: Boolean, - selectedOperators: State>, - selectedOperatorIds: State>, - modalManager: ModalManager -) { +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.operator_review_conditions, + labelId = MR.strings.ok, onboarding = null, enabled = enabled, onclick = { - modalManager.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> - ReviewConditionsView(onboarding, selectedOperators, selectedOperatorIds, close) - } + close() } ) } -@Composable -private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOperators: State>, selectedOperatorIds: State>, close: () -> Unit) { - OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), - labelId = MR.strings.onboarding_network_operators_update, - onboarding = null, - enabled = enabled, - onclick = { - withBGApi { - val enabledOperators = enabledOperators(serverOperators.value, selectedOperatorIds.value) - if (enabledOperators != null) { - val r = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators) - if (r != null) { - chatModel.conditions.value = r - } - continueToNextStep(onboarding, close) - } - } - } - ) -} - -@Composable -private fun ContinueButton(enabled: Boolean, onboarding: 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.onboarding_network_operators_continue, - onboarding = null, - enabled = enabled, - onclick = { - continueToNextStep(onboarding, close) - } - ) -} - -@Composable -private fun ReviewLaterButton(enabled: Boolean, close: () -> Unit) { - TextButtonBelowOnboardingButton( - stringResource(MR.strings.onboarding_network_operators_review_later), - onClick = if (!enabled) null else {{ continueToNextStep(false, close) }} - ) -} - -@Composable -private fun ReviewConditionsView( - onboarding: Boolean, - selectedOperators: State>, - selectedOperatorIds: State>, - close: () -> Unit -) { - // remembering both since we don't want to reload the view after the user accepts conditions - val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } } - val acceptForOperators = remember { selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } } - ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = if (onboarding) DEFAULT_ONBOARDING_HORIZONTAL_PADDING else DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false, bottomPadding = DEFAULT_PADDING) - if (operatorsWithConditionsAccepted.isNotEmpty()) { - ReadableText(MR.strings.operator_conditions_accepted_for_some, args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ }) - ReadableText(MR.strings.operator_same_conditions_will_apply_to_operators, args = acceptForOperators.joinToString(", ") { it.legalName_ }) - } else { - ReadableText(MR.strings.operator_conditions_will_be_accepted_for_some, args = acceptForOperators.joinToString(", ") { it.legalName_ }) - } - Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF)) { - ConditionsTextView(chatModel.remoteHostId()) - } - Column(Modifier.padding(vertical = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { - AcceptConditionsButton(onboarding, selectedOperators, selectedOperatorIds, close) - } - } -} - @Composable private fun AcceptConditionsButton( - onboarding: Boolean, - selectedOperators: State>, - selectedOperatorIds: State>, - close: () -> Unit + enabled: Boolean, + selectedOperatorIds: State> ) { fun continueOnAccept() { - if (appPlatform.isDesktop || !onboarding) { - if (onboarding) { close() } - continueToNextStep(onboarding, close) + if (appPlatform.isDesktop) { + continueToNextStep() } else { continueToSetNotificationsAfterAccept() } } OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.fillMaxWidth() else Modifier, - labelId = MR.strings.accept_conditions, + 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) + val r = chatController.acceptConditions(chatModel.remoteHostId(), conditionsId = conditionsId, operatorIds = selectedOperatorIds.value.toList()) if (r != null) { chatModel.conditions.value = r val enabledOperators = enabledOperators(r.serverOperators, selectedOperatorIds.value) @@ -300,12 +251,8 @@ private fun AcceptConditionsButton( ) } -private fun continueToNextStep(onboarding: Boolean, close: () -> Unit) { - if (onboarding) { +private fun continueToNextStep() { appPrefs.onboardingStage.set(if (appPlatform.isAndroid) OnboardingStage.Step4_SetNotificationsMode else OnboardingStage.OnboardingComplete) - } else { - close() - } } private fun continueToSetNotificationsAfterAccept() { @@ -344,9 +291,7 @@ private fun enabledOperators(operators: List, selectedOperatorId } @Composable -private fun ChooseServerOperatorsInfoView( - modalManager: ModalManager -) { +private fun ChooseServerOperatorsInfoView() { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.onboarding_network_operators)) @@ -362,21 +307,20 @@ private fun ChooseServerOperatorsInfoView( SectionView(title = stringResource(MR.strings.onboarding_network_about_operators).uppercase()) { chatModel.conditions.value.serverOperators.forEach { op -> - ServerOperatorRow(op, modalManager) + ServerOperatorRow(op) } } SectionBottomSpacer() } } -@Composable() +@Composable private fun ServerOperatorRow( - operator: ServerOperator, - modalManager: ModalManager + operator: ServerOperator ) { SectionItemView( { - modalManager.showModalCloseable { close -> + ModalManager.fullscreen.showModalCloseable { close -> OperatorInfoView(operator) } } 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 a66ef9ff7a..f64f1dcecd 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 @@ -14,6 +14,9 @@ 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.platform.LocalClipboardManager +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatController.appPrefs @@ -23,8 +26,11 @@ 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.chat.item.CIFileViewScope import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.UserAddressView import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView +import chat.simplex.common.views.usersettings.showAddShortLinkAlert import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @@ -163,11 +169,20 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool Text( stringResource(MR.strings.view_updated_conditions), color = MaterialTheme.colors.primary, - modifier = Modifier.clickable { - modalManager.showModalCloseable { - close -> UsageConditionsView(userServers = mutableStateOf(emptyList()), currUserServers = mutableStateOf(emptyList()), close = close, rhId = rhId) + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + modalManager.showModalCloseable { close -> + UsageConditionsView( + userServers = mutableStateOf(emptyList()), + currUserServers = mutableStateOf(emptyList()), + close = close, + rhId = rhId + ) + } } - } ) } @@ -176,14 +191,21 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool 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)) } @@ -199,8 +221,17 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool 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 { uriHandler.openUriCatching(url) }) + 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) } } @@ -737,17 +768,7 @@ private val versionDescriptions: List = listOf( 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)) - Row { - Text( - stringResource(MR.strings.v6_2_network_decentralization_enable_flux), - color = MaterialTheme.colors.primary, - modifier = Modifier.clickable { - modalManager.showModalCloseable { close -> ChooseServerOperators(onboarding = false, close, modalManager) } - } - ) - Text(" ") - Text(stringResource(MR.strings.v6_2_network_decentralization_enable_flux_reason)) - } + Text(stringResource(MR.strings.v6_2_network_decentralization_enable_flux)) } } ), @@ -762,7 +783,103 @@ private val versionDescriptions: List = listOf( 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 + ) + ), + ) + ), + VersionDescription( + version = "v6.4", + post = "https://simplex.chat/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = MR.images.ic_person, + titleId = MR.strings.v6_4_connect_faster, + descrId = MR.strings.v6_4_connect_faster_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_chat_person, + titleId = MR.strings.v6_4_review_members, + descrId = MR.strings.v6_4_review_members_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_contact_support, + titleId = MR.strings.v6_4_support_chat, + descrId = MR.strings.v6_4_support_chat_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_flag, + titleId = MR.strings.v6_4_role_moderator, + descrId = MR.strings.v6_4_role_moderator_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_battery_3_bar, + titleId = MR.strings.v5_8_message_delivery, + descrId = MR.strings.v6_4_message_delivery_descr + ), + ) + ), + VersionDescription( + version = "v6.4.1", + post = "https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = MR.images.ic_waving_hand, + titleId = MR.strings.v6_4_1_welcome_contacts, + descrId = MR.strings.v6_4_1_welcome_contacts_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_timer, + titleId = MR.strings.v6_4_1_keep_chats_clean, + descrId = MR.strings.v6_4_1_keep_chats_clean_descr + ), + VersionFeature.FeatureView( + icon = null, + titleId = MR.strings.v6_4_1_short_address, + view = { modalManager -> CreateUpdateAddressShortLinkView(modalManager) } + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_translate, + titleId = MR.strings.v6_4_1_new_interface_languages, + descrId = MR.strings.v6_4_1_new_interface_languages_descr, + ), + ) + ), ) private val lastVersion = versionDescriptions.last().version @@ -779,6 +896,85 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean { return v != lastVersion } +@Composable +fun CreateUpdateAddressShortLinkView(modalManager: ModalManager) { + val clipboard = LocalClipboardManager.current + val progressIndicator = remember { mutableStateOf(false) } + + fun share(userAddress: String) { clipboard.shareText(userAddress) } + + Column(modifier = Modifier.padding(bottom = 12.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(bottom = 4.dp) + ) { + Icon(painterResource(MR.images.ic_link), stringResource(MR.strings.v6_4_1_short_address), tint = MaterialTheme.colors.secondary) + Text( + generalGetString(MR.strings.v6_4_1_short_address), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.h4, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 6.dp) + ) + } + val addr = chatModel.userAddress.value + if (addr != null) { + if (addr.shouldBeUpgraded) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + stringResource(MR.strings.v6_4_1_short_address_update), + color = MaterialTheme.colors.primary, + fontSize = 15.sp, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + showAddShortLinkAlert(progressIndicator = progressIndicator, share = ::share) + } + ) + if (progressIndicator.value) { + CIFileViewScope.progressIndicator(sizeMultiplier = 0.5f) + } + } + } else { + Text( + stringResource(MR.strings.v6_4_1_short_address_share), + color = MaterialTheme.colors.primary, + fontSize = 15.sp, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + share(addr.connLinkContact.simplexChatUri(short = true)) + } + ) + } + } else { + Text( + stringResource(MR.strings.v6_4_1_short_address_create), + color = MaterialTheme.colors.primary, + fontSize = 15.sp, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + modalManager.showModalCloseable { close -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) + } + } + ) + } + } +} + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, 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 3b6e176ca3..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 @@ -492,7 +492,7 @@ private suspend fun connectDesktopAddress(sessionAddress: MutableState, } } -private suspend fun connectDesktop(sessionAddress: MutableState, connect: suspend () -> Pair): Boolean { +private suspend fun connectDesktop(sessionAddress: MutableState, connect: suspend () -> Pair): Boolean { val res = connect() if (res.first != null) { val (rc_, ctrlAppInfo, v) = res.first!! @@ -505,13 +505,13 @@ private suspend fun connectDesktop(sessionAddress: MutableState, connect } else { val e = res.second ?: return false 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() + 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 = "${e.responseType}: ${e.details}" + val errMsg = "error: ${e.string}" Log.e(TAG, "bad response: $errMsg") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg) } 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 b4fead6692..e24c09afd0 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 @@ -195,7 +195,9 @@ object AppearanceScope { ) ) } - SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail) + if (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) > 27) { + SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail) + } } } } @@ -1096,6 +1098,7 @@ object AppearanceScope { "en" to "English", "ar" to "العربية", "bg" to "Български", + "ca" to "Català", "cs" to "Čeština", "de" to "Deutsch", "es" to "Español", @@ -1103,6 +1106,7 @@ object AppearanceScope { "fi" to "Suomi", "fr" to "Français", "hu" to "Magyar", + "in" to "Indonesia", "it" to "Italiano", "iw" to "עִברִית", "ja" to "日本語", @@ -1110,10 +1114,12 @@ object AppearanceScope { "nl" to "Nederlands", "pl" to "Polski", "pt-BR" to "Português, Brasil", + "ro" to "Română", "ru" to "Русский", "th" to "ภาษาไทย", "tr" to "Türkçe", "uk" to "Українська", + "vi" to "Tiếng Việt", "zh-CN" to "简体中文" ) val values by remember(appPrefs.appLanguage.state.value) { mutableStateOf(supportedLanguages.map { it.key to it.value }) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index c5a4ae5f70..dcb71a552d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -2,18 +2,10 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced -import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler -import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import dev.icerock.moko.resources.compose.painterResource @@ -67,7 +59,15 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls) } } - SectionBottomSpacer() + SectionDividerSpaced(maxTopPadding = true) + SectionView(stringResource(MR.strings.deprecated_options_section).uppercase()) { + val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode + SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { + simplexLinkMode.set(it) + chatModel.simplexLinkMode.value = it + }) + SectionBottomSpacer() + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 5af5d5fb90..2fc427cd2e 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 @@ -4,10 +4,8 @@ import SectionBottomSpacer import SectionTextFooter import SectionView import SectionViewSelectable -import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.capitalize @@ -15,7 +13,6 @@ 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 @@ -62,7 +59,7 @@ fun NotificationsSettingsLayout( if (appPlatform == AppPlatform.ANDROID) { SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), { showPage(CurrentPage.NOTIFICATIONS_MODE) }) { Text( - modes.first { it.value == notificationsMode.value }.title, + modes.firstOrNull { it.value == notificationsMode.value }?.title ?: "", maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colors.secondary @@ -71,7 +68,7 @@ fun NotificationsSettingsLayout( } SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notification_preview_mode_title), { showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) { Text( - previewModes.first { it.value == notificationPreviewMode.value }.title, + previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "", maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colors.secondary 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 5132516669..fe9137ee35 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,13 +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.model.ChatModel.withChats 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,) { @@ -34,8 +34,8 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { if (updated != null) { val (updatedProfile, updatedContacts) = updated m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences) - withChats { - updatedContacts.forEach { updateContact(user.remoteHostId, it) } + withContext(Dispatchers.Main) { + updatedContacts.forEach { chatModel.chatsContext.updateContact(user.remoteHostId, it) } } currentPreferences = preferences } @@ -69,9 +69,18 @@ private fun PreferencesLayout( ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_preferences)) val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) } - TimedMessagesFeatureSection(timedMessages) { - applyPrefs(preferences.copy(timedMessages = TimedMessagesPreference(allow = if (it) FeatureAllowed.YES else FeatureAllowed.NO))) + val onTTLUpdated = { ttl: Int? -> + applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) } + TimedMessagesFeatureSection( + preferences, + timedMessages, + onSelected = { + applyPrefs(preferences.copy(timedMessages = TimedMessagesPreference(allow = if (it) FeatureAllowed.YES else FeatureAllowed.NO))) + }, + onTTLUpdated = onTTLUpdated + ) + SectionDividerSpaced(true, maxBottomPadding = false) val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) } FeatureSection(ChatFeature.FullDelete, allowFullDeletion) { @@ -117,7 +126,13 @@ private fun FeatureSection(feature: ChatFeature, allowFeature: State, onSelected: (Boolean) -> Unit) { +private fun TimedMessagesFeatureSection( + preferences: FullChatPreferences, + allowFeature: State, + onSelected: (Boolean) -> Unit, + onTTLUpdated: (Int?) -> Unit +) { + val ttl = rememberSaveable(preferences) { mutableStateOf(preferences.timedMessages.ttl) } SectionView { PreferenceToggleWithIcon( ChatFeature.TimedMessages.text, @@ -127,8 +142,23 @@ private fun TimedMessagesFeatureSection(allowFeature: State, onS extraPadding = false, onChange = onSelected ) + if (allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES) { + ExposedDropDownSettingRow( + generalGetString(MR.strings.delete_after), + TimedMessagesPreference.profileLevelTTLValues.map { v -> v to timeText(v) }, + ttl, + icon = null, + onSelected = onTTLUpdated + ) + } } - SectionTextFooter(ChatFeature.TimedMessages.allowDescription(allowFeature.value)) + SectionTextFooter( + if ((allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES) && ttl.value != null) { + ChatFeature.TimedMessages.allowDescription(allowFeature.value) + "\n" + generalGetString(MR.strings.time_to_disappear_is_set_only_for_new_contacts) + } else { + ChatFeature.TimedMessages.allowDescription(allowFeature.value) + } + ) } @Composable 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 c411eb0d78..2771b5ac62 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -30,8 +30,8 @@ import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.localauth.SetAppPasscodeView import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* +import kotlinx.coroutines.* enum class LAMode { SYSTEM, @@ -56,16 +56,22 @@ fun PrivacySettingsView( setPerformLA: (Boolean) -> Unit ) { ColumnWithScrollBar { - val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode AppBarTitle(stringResource(MR.strings.your_privacy)) PrivacyDeviceSection(showSettingsModal, setPerformLA) SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { - SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) - ChatListLinksOptions(appPrefs.privacyChatListOpenLinks.state, onSelected = { - appPrefs.privacyChatListOpenLinks.set(it) - }) + SettingsPreferenceItem( + painterResource(MR.images.ic_travel_explore), + stringResource(MR.strings.send_link_previews), + chatModel.controller.appPrefs.privacyLinkPreviews, + onChange = { _ -> chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) } // to avoid showing alert to current users, show alert in v6.5 + ) + SettingsPreferenceItem( + painterResource(MR.images.ic_link), + stringResource(MR.strings.sanitize_links_toggle), + chatModel.controller.appPrefs.privacySanitizeLinks + ) SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), stringResource(MR.strings.privacy_show_last_messages), @@ -84,10 +90,6 @@ fun PrivacySettingsView( chatModel.draftChatId.value = null } }) - SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { - simplexLinkMode.set(it) - chatModel.simplexLinkMode.value = it - }) } SectionDividerSpaced() @@ -119,15 +121,15 @@ fun PrivacySettingsView( chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable) if (clearOverrides) { // For loop here is to prevent ConcurrentModificationException that happens with forEach - withChats { - for (i in 0 until chats.size) { - val chat = chats[i] + 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)) - updateContact(currentUser.remoteHostId, contact) + chatModel.chatsContext.updateContact(currentUser.remoteHostId, contact) } } } @@ -143,16 +145,16 @@ fun PrivacySettingsView( chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable) if (clearOverrides) { - withChats { + withContext(Dispatchers.Main) { // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chats.size) { - val chat = chats[i] + 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)) - updateGroup(currentUser.remoteHostId, groupInfo) + chatModel.chatsContext.updateGroup(currentUser.remoteHostId, groupInfo) } } } @@ -161,7 +163,22 @@ fun PrivacySettingsView( } } + fun setAutoAcceptGrpDirectInvs(enable: Boolean) { + withApi { + chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable) + chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable) + } + } + if (!chatModel.desktopNoUserNoRemote) { + SectionDividerSpaced(maxTopPadding = true) + ContacRequestsFromGroupsSection( + currentUser = currentUser, + setAutoAcceptGrpDirectInvs = { enable -> + setAutoAcceptGrpDirectInvs(enable) + } + ) + SectionDividerSpaced(maxTopPadding = true) DeliveryReceiptsSection( currentUser = currentUser, @@ -203,27 +220,7 @@ fun PrivacySettingsView( } @Composable -private fun ChatListLinksOptions(state: State, onSelected: (PrivacyChatListOpenLinksMode) -> Unit) { - val values = remember { - PrivacyChatListOpenLinksMode.entries.map { - when (it) { - PrivacyChatListOpenLinksMode.YES -> it to generalGetString(MR.strings.privacy_chat_list_open_links_yes) - PrivacyChatListOpenLinksMode.NO -> it to generalGetString(MR.strings.privacy_chat_list_open_links_no) - PrivacyChatListOpenLinksMode.ASK -> it to generalGetString(MR.strings.privacy_chat_list_open_links_ask) - } - } - } - ExposedDropDownSettingRow( - generalGetString(MR.strings.privacy_chat_list_open_links), - values, - state, - icon = painterResource(MR.images.ic_open_in_new), - onSelected = onSelected - ) -} - -@Composable -private fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) { +fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) { val modeValues = listOf(SimplexLinkMode.DESCRIPTION, SimplexLinkMode.FULL) val pickerValues = modeValues + if (modeValues.contains(simplexLinkModeState.value)) emptyList() else listOf(simplexLinkModeState.value) val values = remember { @@ -275,6 +272,34 @@ expect fun PrivacyDeviceSection( setPerformLA: (Boolean) -> Unit, ) +@Composable +private fun ContacRequestsFromGroupsSection( + currentUser: User, + setAutoAcceptGrpDirectInvs: (Boolean) -> Unit +) { + SectionView(stringResource(MR.strings.settings_section_title_contact_requests_from_groups)) { + SettingsActionItemWithContent(painterResource(MR.images.ic_check), stringResource(MR.strings.auto_accept_contact)) { + DefaultSwitch( + checked = currentUser.autoAcceptMemberContacts, + onCheckedChange = { enable -> + setAutoAcceptGrpDirectInvs(enable) + } + ) + } + } + SectionTextFooter( + remember(currentUser.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.this_setting_is_for_your_current_profile) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser.displayName) + } + append(".") + } + } + ) +} + @Composable private fun DeliveryReceiptsSection( currentUser: User, @@ -284,7 +309,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) } @@ -292,7 +317,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) } 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 5bd45ccaab..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 @@ -21,6 +21,7 @@ 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.* @@ -127,7 +128,9 @@ fun SettingsLayout( SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_support)) { - ContributeItem(uriHandler) + if (!BuildConfigCommon.ANDROID_BUNDLE) { + ContributeItem(uriHandler) + } RateAppItem(uriHandler) StarOnGithubItem(uriHandler) } 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 7bc35bc0de..3b6cf34b7c 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 @@ -15,16 +16,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.ShareAddressButton import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.model.MsgContent import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @@ -37,33 +39,41 @@ fun UserAddressView( ) { // TODO close when remote host changes val shareViaProfile = remember { mutableStateOf(shareViaProfile) } - var progressIndicator by remember { mutableStateOf(false) } + val progressIndicator = remember { mutableStateOf(false) } val user = remember { chatModel.currentUser } + val clipboard = LocalClipboardManager.current KeyChangeEffect(user.value?.remoteHostId, user.value?.userId) { close() } + fun setProfileAddress(on: Boolean) { - progressIndicator = true + progressIndicator.value = true withBGApi { try { - val u = chatModel.controller.apiSetProfileAddress(user?.value?.remoteHostId, on) + val u = chatModel.controller.apiSetProfileAddress(user.value?.remoteHostId, on) if (u != null) { chatModel.updateUser(u) } } catch (e: Exception) { Log.e(TAG, "UserAddressView apiSetProfileAddress: ${e.stackTraceToString()}") } finally { - progressIndicator = false + progressIndicator.value = false } } } fun createAddress() { withBGApi { - progressIndicator = true - val connReqContact = chatModel.controller.apiCreateUserAddress(user?.value?.remoteHostId) + progressIndicator.value = true + val connReqContact = chatModel.controller.apiCreateUserAddress(user.value?.remoteHostId) if (connReqContact != null) { - chatModel.userAddress.value = UserContactLinkRec(connReqContact) + val slDataSet = connReqContact.connShortLink != null + chatModel.userAddress.value = UserContactLinkRec( + connReqContact, + shortLinkDataSet = slDataSet, + shortLinkLargeDataSet = slDataSet, + addressSettings = AddressSettings(businessAddress = false, autoAccept = null, autoReply = null) + ) AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.share_address_with_contacts_question), @@ -75,34 +85,38 @@ fun UserAddressView( } ) } - progressIndicator = false + progressIndicator.value = false } } + fun share(userAddress: String) { clipboard.shareText(userAddress) } + LaunchedEffect(autoCreateAddress) { if (chatModel.userAddress.value == null && autoCreateAddress) { createAddress() } } val userAddress = remember { chatModel.userAddress } - val clipboard = LocalClipboardManager.current val uriHandler = LocalUriHandler.current val showLayout = @Composable { UserAddressLayout( user = user.value, userAddress = userAddress.value, shareViaProfile, - createAddress = { createAddress() }, + createAddress = ::createAddress, + showAddShortLinkAlert = { shareAddress: (() -> Unit)? -> + showAddShortLinkAlert(progressIndicator = progressIndicator, share = ::share, shareAddress = shareAddress) + }, learnMore = { ModalManager.start.showModal { UserAddressLearnMore() } }, - share = { userAddress: String -> clipboard.shareText(userAddress) }, + share = ::share, 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, @@ -112,26 +126,26 @@ fun UserAddressView( text = if (shareViaProfile.value) generalGetString(MR.strings.all_your_contacts_will_remain_connected_update_sent) else generalGetString(MR.strings.all_your_contacts_will_remain_connected), confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { - progressIndicator = true + progressIndicator.value = true withBGApi { - val u = chatModel.controller.apiDeleteUserAddress(user?.value?.remoteHostId) + val u = chatModel.controller.apiDeleteUserAddress(user.value?.remoteHostId) if (u != null) { chatModel.userAddress.value = null chatModel.updateUser(u) shareViaProfile.value = false - progressIndicator = false + progressIndicator.value = false } } }, destructive = true, ) }, - saveAas = { aas: AutoAcceptState, savedAAS: MutableState -> + saveAddressSettings = { settings: AddressSettingsState, savedSettings: MutableState -> withBGApi { - val address = chatModel.controller.userAddressAutoAccept(user?.value?.remoteHostId, aas.autoAccept) + val address = chatModel.controller.apiSetUserAddressSettings(user.value?.remoteHostId, settings.addressSettings) if (address != null) { chatModel.userAddress.value = address - savedAAS.value = aas + savedSettings.value = settings } } }, @@ -142,7 +156,7 @@ fun UserAddressView( showLayout() } - if (progressIndicator) { + if (progressIndicator.value) { Box( Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -161,18 +175,88 @@ fun UserAddressView( } } +private fun addShortLink( + progressIndicator: MutableState, + share: (String) -> Unit, + shareOnCompletion: Boolean = false +) { + withBGApi { + progressIndicator.value = true + val userAddress = chatModel.controller.apiAddMyAddressShortLink(chatModel.currentUser.value?.remoteHostId) + if (userAddress != null) { + chatModel.userAddress.value = userAddress + if (shareOnCompletion) { + share(userAddress.connLinkContact.simplexChatUri(short = true)) + } + } + progressIndicator.value = false + } +} + +fun showAddShortLinkAlert( + progressIndicator: MutableState, + share: (String) -> Unit, + shareAddress: (() -> Unit)? = null +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.share_profile_via_link), + text = generalGetString(MR.strings.share_profile_via_link_alert_text), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + addShortLink(progressIndicator = progressIndicator, share = share, shareOnCompletion = shareAddress != null) + }) { + Text( + generalGetString(MR.strings.share_profile_via_link_alert_confirm), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + + if (shareAddress != null) { + SectionItemView({ + AlertManager.shared.hideAlert() + shareAddress() + }) { + Text( + generalGetString(MR.strings.share_old_address_alert_button), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + } + ) +} + @Composable private fun UserAddressLayout( user: User?, userAddress: UserContactLinkRec?, shareViaProfile: MutableState, createAddress: () -> Unit, + showAddShortLinkAlert: ((() -> Unit)?) -> Unit, learnMore: () -> Unit, share: (String) -> Unit, sendEmail: (UserContactLinkRec) -> Unit, setProfileAddress: (Boolean) -> Unit, deleteAddress: () -> Unit, - saveAas: (AutoAcceptState, MutableState) -> Unit, + saveAddressSettings: (AddressSettingsState, MutableState) -> Unit, ) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) @@ -196,22 +280,35 @@ private fun UserAddressLayout( LearnMoreButton(learnMore) } } else { - val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } - val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } + val addressSettingsState = remember { mutableStateOf(AddressSettingsState(settings = userAddress.addressSettings)) } + val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) } + val showShortLink = remember { mutableStateOf(true) } - SectionView(stringResource(MR.strings.for_social_media).uppercase()) { - SimpleXLinkQRCode(userAddress.connReqContact) - ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } + SectionViewWithButton( + stringResource(MR.strings.for_social_media).uppercase(), + titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null + ) { + SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value) + if (userAddress.shouldBeUpgraded) { + AddShortLinkButton(text = stringResource(MR.strings.add_short_link)) { showAddShortLinkAlert(null) } + } + ShareAddressButton { + if (userAddress.shouldBeUpgraded) { + showAddShortLinkAlert { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } + } else { + share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) + } + } // ShareViaEmailButton { sendEmail(userAddress) } - BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } - AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) + BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } + AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings) - if (autoAcceptState.value.business) { + if (addressSettingsState.value.businessAddress) { SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) } } - SectionDividerSpaced(maxTopPadding = autoAcceptState.value.business) + SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress) SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { CreateOneTimeLinkButton() } @@ -242,6 +339,17 @@ private fun CreateAddressButton(onClick: () -> Unit) { ) } +@Composable +private fun AddShortLinkButton(text: String, onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_arrow_upward), + text, + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun CreateOneTimeLinkButton() { val closeAll = { ModalManager.start.closeModals() } @@ -284,14 +392,14 @@ private fun AddressSettingsButton( userAddress: UserContactLinkRec, shareViaProfile: MutableState, setProfileAddress: (Boolean) -> Unit, - saveAas: (AutoAcceptState, MutableState) -> Unit, + saveAddressSettings: (AddressSettingsState, 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) + UserAddressSettings(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings, close = close) } } ) @@ -303,20 +411,20 @@ private fun ModalData.UserAddressSettings( userAddress: UserContactLinkRec, shareViaProfile: MutableState, setProfileAddress: (Boolean) -> Unit, - saveAas: (AutoAcceptState, MutableState) -> Unit, + saveAddressSettings: (AddressSettingsState, MutableState) -> Unit, close: () -> Unit ) { - val autoAcceptState = remember { stateGetOrPut("autoAcceptState") { (AutoAcceptState(userAddress)) } } - val autoAcceptStateSaved = remember { stateGetOrPut("autoAcceptStateSaved") { (autoAcceptState.value) } } + val addressSettingsState = remember { stateGetOrPut("autoAcceptState") { (AddressSettingsState(userAddress.addressSettings)) } } + val savedAddressSettingsState = remember { stateGetOrPut("autoAcceptStateSaved") { (addressSettingsState.value) } } - fun onClose(close: () -> Unit): Boolean = if (autoAcceptState.value == autoAcceptStateSaved.value) { + fun onClose(close: () -> Unit): Boolean = if (addressSettingsState.value == savedAddressSettingsState.value) { chatModel.centerPanelBackgroundClickHandler = null close() false } else { showUnsavedChangesAlert( save = { - saveAas(autoAcceptState.value, autoAcceptStateSaved) + saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) chatModel.centerPanelBackgroundClickHandler = null close() }, @@ -345,12 +453,20 @@ private fun ModalData.UserAddressSettings( ) { SectionView { ShareWithContactsButton(shareViaProfile, setProfileAddress) - AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } + AutoAcceptToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } + if (addressSettingsState.value.autoAccept && !chatModel.addressShortLinkDataSet() && !addressSettingsState.value.businessAddress) { + AcceptIncognitoToggle(addressSettingsState) + } } + SectionDividerSpaced() - if (autoAcceptState.value.enable) { - SectionDividerSpaced() - AutoAcceptSection(autoAcceptState, autoAcceptStateSaved, saveAas) + SectionView(stringResource(MR.strings.address_welcome_message).uppercase()) { + AutoReplyEditor(addressSettingsState) + } + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + + saveAddressSettingsButton(addressSettingsState.value == savedAddressSettingsState.value) { + saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } } } @@ -398,33 +514,53 @@ fun ShareWithContactsButton(shareViaProfile: MutableState, setProfileAd } @Composable -private fun BusinessAddressToggle(autoAcceptState: MutableState, saveAas: (AutoAcceptState) -> Unit) { +private fun BusinessAddressToggle(addressSettingsState: MutableState, saveAddressSettings: (AddressSettingsState) -> 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) + checked = addressSettingsState.value.businessAddress, + ) { businessToggle -> + addressSettingsState.value = if (businessToggle) + AddressSettingsState( + businessAddress = true, + autoAccept = true, + autoAcceptIncognito = false, + autoReply = addressSettingsState.value.autoReply + ) else - AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, business = false, autoAcceptState.value.welcomeText) - saveAas(autoAcceptState.value) + AddressSettingsState( + businessAddress = false, + autoAccept = addressSettingsState.value.autoAccept, + autoAcceptIncognito = addressSettingsState.value.autoAcceptIncognito, + autoReply = addressSettingsState.value.autoReply + ) + saveAddressSettings(addressSettingsState.value) } } @Composable -private fun AutoAcceptToggle(autoAcceptState: MutableState, saveAas: (AutoAcceptState) -> Unit) { +private fun AutoAcceptToggle(addressSettingsState: MutableState, saveAddressSettings: (AddressSettingsState) -> Unit) { 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() + disabled = addressSettingsState.value.businessAddress, + checked = addressSettingsState.value.autoAccept + ) { autoAcceptToggle -> + addressSettingsState.value = if (autoAcceptToggle) + AddressSettingsState( + businessAddress = addressSettingsState.value.businessAddress, + autoAccept = true, + autoAcceptIncognito = false, + autoReply = "" + ) else - AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.business, autoAcceptState.value.welcomeText) - saveAas(autoAcceptState.value) + AddressSettingsState( + businessAddress = false, + autoAccept = false, + autoAcceptIncognito = false, + autoReply = "" + ) + saveAddressSettings(addressSettingsState.value) } } @@ -439,103 +575,93 @@ private fun DeleteAddressButton(onClick: () -> Unit) { ) } -private class AutoAcceptState { - var enable: Boolean = false +private class AddressSettingsState { + var businessAddress: Boolean = false private set - var incognito: Boolean = false + var autoAccept: Boolean = false private set - var business: Boolean = false + var autoAcceptIncognito: Boolean = false private set - var welcomeText: String = "" + var autoReply: String = "" private set - constructor(enable: Boolean = false, incognito: Boolean = false, business: Boolean = false, welcomeText: String = "") { - this.enable = enable - this.incognito = incognito - this.business = business - this.welcomeText = welcomeText + constructor(businessAddress: Boolean = false, autoAccept: Boolean = false, autoAcceptIncognito: Boolean = false, autoReply: String = "") { + this.businessAddress = businessAddress + this.autoAccept = autoAccept + this.autoAcceptIncognito = autoAcceptIncognito + this.autoReply = autoReply } - constructor(contactLink: UserContactLinkRec) { - contactLink.autoAccept?.let { aa -> - enable = true - incognito = aa.acceptIncognito - business = aa.businessAddress - aa.autoReply?.let { msg -> - welcomeText = msg.text - } ?: run { - welcomeText = "" - } - } + constructor(settings: AddressSettings) { + this.businessAddress = settings.businessAddress + this.autoAccept = settings.autoAccept != null + this.autoAcceptIncognito = settings.autoAccept?.acceptIncognito == true + this.autoReply = settings.autoReply?.text ?: "" } - val autoAccept: AutoAccept? + val addressSettings: AddressSettings get() { - if (enable) { - var autoReply: MsgContent? = null - val s = welcomeText.trim() - if (s != "") { - autoReply = MsgContent.MCText(s) - } - return AutoAccept(business, incognito, autoReply) - } - return null + return AddressSettings( + businessAddress = this.businessAddress, + autoAccept = if (this.autoAccept) AutoAccept(acceptIncognito = this.autoAcceptIncognito) else null, + autoReply = if (this.autoReply.isEmpty()) null else MsgContent.MCText(this.autoReply) + ) } override fun equals(other: Any?): Boolean { - if (other !is AutoAcceptState) return false - return this.enable == other.enable && this.incognito == other.incognito && this.business == other.business && this.welcomeText == other.welcomeText + if (other !is AddressSettingsState) return false + return ( + this.businessAddress == other.businessAddress + && this.autoAccept == other.autoAccept + && this.autoAcceptIncognito == other.autoAcceptIncognito + && this.autoReply == other.autoReply + ) } override fun hashCode(): Int { - var result = enable.hashCode() - result = 31 * result + incognito.hashCode() - result = 31 * result + business.hashCode() - result = 31 * result + welcomeText.hashCode() + var result = businessAddress.hashCode() + result = 31 * result + autoAccept.hashCode() + result = 31 * result + autoAcceptIncognito.hashCode() + result = 31 * result + autoReply.hashCode() return result } } @Composable -private fun AutoAcceptSection( - autoAcceptState: MutableState, - savedAutoAcceptState: MutableState, - saveAas: (AutoAcceptState, MutableState) -> Unit -) { - SectionView(stringResource(MR.strings.auto_accept_contact).uppercase()) { - if (!autoAcceptState.value.business) { - AcceptIncognitoToggle(autoAcceptState) - } - WelcomeMessageEditor(autoAcceptState) - SaveAASButton(autoAcceptState.value == savedAutoAcceptState.value) { saveAas(autoAcceptState.value, savedAutoAcceptState) } - } -} - -@Composable -private fun AcceptIncognitoToggle(autoAcceptState: MutableState) { +private fun AcceptIncognitoToggle(addressSettingsState: MutableState) { PreferenceToggleWithIcon( 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, - checked = autoAcceptState.value.incognito, - ) { - autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.business, autoAcceptState.value.welcomeText) + if (addressSettingsState.value.autoAcceptIncognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy), + if (addressSettingsState.value.autoAcceptIncognito) Indigo else MaterialTheme.colors.secondary, + checked = addressSettingsState.value.autoAcceptIncognito, + ) { incognitoToggle -> + addressSettingsState.value = AddressSettingsState( + businessAddress = addressSettingsState.value.businessAddress, + autoAccept = addressSettingsState.value.autoAccept, + autoAcceptIncognito = incognitoToggle, + autoReply = addressSettingsState.value.autoReply + ) } } @Composable -private fun WelcomeMessageEditor(autoAcceptState: MutableState) { - val welcomeText = rememberSaveable { mutableStateOf(autoAcceptState.value.welcomeText) } - 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, autoAcceptState.value.business, welcomeText.value) +private fun AutoReplyEditor(addressSettingsState: MutableState) { + val autoReply = rememberSaveable { mutableStateOf(addressSettingsState.value.autoReply) } + TextEditor(autoReply, Modifier.height(100.dp), placeholder = stringResource(MR.strings.enter_welcome_message_optional)) + LaunchedEffect(autoReply.value) { + if (autoReply.value != addressSettingsState.value.autoReply) { + addressSettingsState.value = AddressSettingsState( + businessAddress = addressSettingsState.value.businessAddress, + autoAccept = addressSettingsState.value.autoAccept, + autoAcceptIncognito = addressSettingsState.value.autoAcceptIncognito, + autoReply = autoReply.value + ) } } } @Composable -private fun SaveAASButton(disabled: Boolean, onClick: () -> Unit) { +private fun saveAddressSettingsButton(disabled: Boolean, onClick: () -> Unit) { SectionItemView(onClick, disabled = disabled) { Text(stringResource(MR.strings.save_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } @@ -553,9 +679,10 @@ fun PreviewUserAddressLayoutNoAddress() { user = User.sampleData, userAddress = null, createAddress = {}, + showAddShortLinkAlert = {}, share = { _ -> }, deleteAddress = {}, - saveAas = { _, _ -> }, + saveAddressSettings = { _, _ -> }, setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, @@ -584,11 +711,17 @@ 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), + shortLinkDataSet = false, + shortLinkLargeDataSet = false, + addressSettings = AddressSettings(businessAddress = false, autoAccept = null, autoReply = null) + ), createAddress = {}, + showAddShortLinkAlert = {}, share = { _ -> }, deleteAddress = {}, - saveAas = { _, _ -> }, + saveAddressSettings = { _, _ -> }, setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, 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 90122bd29d..45cdee6108 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 @@ -40,9 +40,10 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { UserProfileLayout( profile = profile, close, - saveProfile = { displayName, fullName, image -> + saveProfile = { displayName, fullName, shortDescr, image -> withBGApi { - val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, profile.copy(displayName = displayName.trim(), fullName = fullName, image = image)) + val updatedProfile = profile.copy(displayName = displayName.trim(), fullName = fullName.trim(), shortDescr = shortDescr.trim().ifEmpty { null }, image = image) + val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, updatedProfile) if (updated != null) { val (newProfile, _) = updated chatModel.updateCurrentUser(user.remoteHostId, newProfile) @@ -59,11 +60,12 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { fun UserProfileLayout( profile: Profile, close: () -> Unit, - saveProfile: (String, String, String?) -> Unit, + saveProfile: (String, String, String, String?) -> Unit, ) { val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = remember { mutableStateOf(profile.displayName) } val fullName = remember { mutableStateOf(profile.fullName) } + val shortDescr = remember { mutableStateOf(profile.shortDescr ?: "") } val chosenImage = rememberSaveable { mutableStateOf(null) } val profileImage = rememberSaveable { mutableStateOf(profile.image) } val scope = rememberCoroutineScope() @@ -85,14 +87,15 @@ fun UserProfileLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { val dataUnchanged = - displayName.value == profile.displayName && - fullName.value == profile.fullName && + displayName.value.trim() == profile.displayName && + fullName.value.trim() == profile.fullName && + shortDescr.value.trim() == (profile.shortDescr ?: "") && profile.image == profileImage.value val closeWithAlert = { - if (dataUnchanged || !canSaveProfile(displayName.value, profile)) { + if (dataUnchanged || !canSaveProfile(displayName.value, shortDescr.value, profile)) { close() } else { - showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, profileImage.value) }, close) + showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, shortDescr.value, profileImage.value) }, close) } } ModalView(close = closeWithAlert) { @@ -144,9 +147,29 @@ fun UserProfileLayout( ) ProfileNameField(fullName) } + Spacer(Modifier.height(DEFAULT_PADDING)) - val enabled = !dataUnchanged && canSaveProfile(displayName.value, profile) - val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, profileImage.value) } + + Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(MR.strings.short_descr__field), + fontSize = 16.sp, + ) + if (!bioFitsLimit(shortDescr.value)) { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) + IconButton( + onClick = { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.bio_too_large)) }, + Modifier.size(20.dp) + ) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } + ProfileNameField(shortDescr) + + Spacer(Modifier.height(DEFAULT_PADDING)) + val enabled = !dataUnchanged && canSaveProfile(displayName.value, shortDescr.value, profile) + val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, shortDescr.value, profileImage.value) } val saveColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary Text( stringResource(MR.strings.save_and_notify_contacts), @@ -209,10 +232,10 @@ private fun isValidNewProfileName(displayName: String, profile: Profile): Boolea displayName == profile.displayName || isValidDisplayName(displayName.trim()) private fun showFullName(profile: Profile): Boolean = - profile.fullName.isNotEmpty() && profile.fullName != profile.displayName + profile.fullName.trim().isNotEmpty() && profile.fullName.trim() != profile.displayName.trim() -private fun canSaveProfile(displayName: String, profile: Profile): Boolean = - displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, profile) +private fun canSaveProfile(displayName: String, shortDescr: String, profile: Profile): Boolean = + displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, profile) && bioFitsLimit(shortDescr) @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, @@ -225,7 +248,7 @@ fun PreviewUserProfileLayoutEditOff() { UserProfileLayout( profile = Profile.sampleData, close = {}, - saveProfile = { _, _, _ -> } + saveProfile = { _, _, _, _ -> } ) } } @@ -241,7 +264,7 @@ fun PreviewUserProfileLayoutEditOn() { UserProfileLayout( profile = Profile.sampleData, close = {}, - saveProfile = { _, _, _ -> } + saveProfile = { _, _, _, _ -> } ) } } 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 index fc042cc46c..8c38070c98 100644 --- 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 @@ -19,6 +19,7 @@ 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.* @@ -26,12 +27,13 @@ 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) { +fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() -> Unit) -> Unit, close: () -> Unit) { val currentRemoteHost by remember { chatModel.currentRemoteHost } val developerTools = remember { appPrefs.developerTools.get() } @@ -44,9 +46,12 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U 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 networkTCPConnectTimeoutInteractive = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout.interactiveTimeout) } + val networkTCPConnectTimeoutBackground = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout.backgroundTimeout) } + val networkTCPTimeoutInteractive = remember { mutableStateOf(currentCfgVal.tcpTimeout.interactiveTimeout) } + val networkTCPTimeoutBackground = remember { mutableStateOf(currentCfgVal.tcpTimeout.backgroundTimeout) } val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) } val networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) } val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) } @@ -82,8 +87,15 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U sessionMode = sessionMode.value, smpProxyMode = smpProxyMode.value, smpProxyFallback = smpProxyFallback.value, - tcpConnectTimeout = networkTCPConnectTimeout.value, - tcpTimeout = networkTCPTimeout.value, + smpWebPortServers = smpWebPortServers.value, + tcpConnectTimeout = NetworkTimeout( + backgroundTimeout = networkTCPConnectTimeoutBackground.value, + interactiveTimeout = networkTCPConnectTimeoutInteractive.value + ) , + tcpTimeout = NetworkTimeout( + backgroundTimeout = networkTCPTimeoutBackground.value, + interactiveTimeout = networkTCPTimeoutInteractive.value + ), tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, rcvConcurrency = networkRcvConcurrency.value, tcpKeepAlive = tcpKeepAlive, @@ -96,8 +108,11 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U sessionMode.value = cfg.sessionMode smpProxyMode.value = cfg.smpProxyMode smpProxyFallback.value = cfg.smpProxyFallback - networkTCPConnectTimeout.value = cfg.tcpConnectTimeout - networkTCPTimeout.value = cfg.tcpTimeout + smpWebPortServers.value = cfg.smpWebPortServers + networkTCPConnectTimeoutInteractive.value = cfg.tcpConnectTimeout.interactiveTimeout + networkTCPConnectTimeoutBackground.value = cfg.tcpConnectTimeout.backgroundTimeout + networkTCPTimeoutInteractive.value = cfg.tcpTimeout.interactiveTimeout + networkTCPTimeoutBackground.value = cfg.tcpTimeout.backgroundTimeout networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb networkRcvConcurrency.value = cfg.rcvConcurrency networkSMPPingInterval.value = cfg.smpPingInterval @@ -150,8 +165,11 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U sessionMode = sessionMode, smpProxyMode = smpProxyMode, smpProxyFallback = smpProxyFallback, - networkTCPConnectTimeout, - networkTCPTimeout, + smpWebPortServers, + networkTCPConnectTimeoutInteractive, + networkTCPConnectTimeoutBackground, + networkTCPTimeoutInteractive, + networkTCPTimeoutBackground, networkTCPTimeoutPerKb, networkRcvConcurrency, networkSMPPingInterval, @@ -182,8 +200,11 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U sessionMode: MutableState, smpProxyMode: MutableState, smpProxyFallback: MutableState, - networkTCPConnectTimeout: MutableState, - networkTCPTimeout: MutableState, + smpWebPortServers: MutableState, + networkTCPConnectTimeoutInteractive: MutableState, + networkTCPConnectTimeoutBackground: MutableState, + networkTCPTimeoutInteractive: MutableState, + networkTCPTimeoutBackground: MutableState, networkTCPTimeoutPerKb: MutableState, networkRcvConcurrency: MutableState, networkSMPPingInterval: MutableState, @@ -195,7 +216,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U updateSessionMode: (TransportSessionMode) -> Unit, updateSMPProxyMode: (SMPProxyMode) -> Unit, updateSMPProxyFallback: (SMPProxyFallback) -> Unit, - showModal: (ModalData.() -> Unit) -> Unit, + showModal: (@Composable ModalData.() -> Unit) -> Unit, resetDisabled: Boolean, reset: () -> Unit, saveDisabled: Boolean, @@ -214,75 +235,97 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U } SectionTextFooter(stringResource(MR.strings.private_routing_explanation)) SectionDividerSpaced(maxTopPadding = true) - } - if (currentRemoteHost == null) { 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) { + SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) { SectionItemView { - IntSettingRow("TCP_KEEPIDLE", networkTCPKeepIdle, listOf(15, 30, 60, 120, 180), secondsLabel) + TimeoutSettingRow( + stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeoutInteractive, + listOf(10_000000, 15_000000, 20_000000, 30_000000), secondsLabel + ) } SectionItemView { - IntSettingRow("TCP_KEEPINTVL", networkTCPKeepIntvl, listOf(5, 10, 15, 30, 60), secondsLabel) + TimeoutSettingRow( + stringResource(MR.strings.network_option_tcp_connection_timeout_background), networkTCPConnectTimeoutBackground, + listOf(30_000000, 45_000000, 60_000000, 90_000000), secondsLabel + ) } SectionItemView { - IntSettingRow("TCP_KEEPCNT", networkTCPKeepCnt, listOf(1, 2, 4, 6, 8), "") - } - } else { - SectionItemView { - Text("TCP_KEEPIDLE", color = MaterialTheme.colors.secondary) + TimeoutSettingRow( + stringResource(MR.strings.network_option_protocol_timeout), networkTCPTimeoutInteractive, + listOf(5_000000, 7_000000, 10_000000, 15_000000, 20_000_000), secondsLabel + ) } SectionItemView { - Text("TCP_KEEPINTVL", color = MaterialTheme.colors.secondary) + TimeoutSettingRow( + stringResource(MR.strings.network_option_protocol_timeout_background), networkTCPTimeoutBackground, + listOf(15_000000, 20_000000, 30_000000, 45_000000, 60_000_000), secondsLabel + ) } SectionItemView { - Text("TCP_KEEPCNT", color = MaterialTheme.colors.secondary) + // 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) + } } } } @@ -309,7 +352,7 @@ private fun SMPProxyModePicker( ) { val density = LocalDensity.current val values = remember { - SMPProxyMode.values().map { + 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)) @@ -344,7 +387,7 @@ private fun SMPProxyFallbackPicker( ) { val density = LocalDensity.current val values = remember { - SMPProxyFallback.values().map { + 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)) @@ -537,8 +580,11 @@ fun PreviewAdvancedNetworkSettingsLayout() { sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) }, - networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, - networkTCPTimeout = remember { mutableStateOf(10_000000) }, + smpWebPortServers = remember { mutableStateOf(SMPWebPortServers.Preset) }, + networkTCPConnectTimeoutInteractive = remember { mutableStateOf(15_000000) }, + networkTCPConnectTimeoutBackground = remember { mutableStateOf(45_000000) }, + networkTCPTimeoutInteractive = remember { mutableStateOf(10_000000) }, + networkTCPTimeoutBackground = remember { mutableStateOf(30_000000) }, networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) }, networkRcvConcurrency = remember { mutableStateOf(8) }, networkSMPPingInterval = remember { mutableStateOf(10_000000) }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index 835e01ec27..26ecf151ff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -22,8 +22,12 @@ 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.* @@ -180,7 +184,14 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { @Composable fun ConditionsButton(conditionsAction: UsageConditionsAction, rhId: Long?) { SectionItemView( - click = { ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> UsageConditionsView(currUserServers, userServers, close, rhId) } }, + 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), @@ -583,7 +594,7 @@ fun UseOnionHosts( onSelected = {} ) } - SectionTextFooter(values.first { it.value == onionHosts.value }.description) + SectionTextFooter(values.firstOrNull { it.value == onionHosts.value }?.description ?: AnnotatedString("")) } } @@ -733,17 +744,43 @@ fun UsageConditionsView( } } + @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)) { - AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false, bottomPadding = 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, if (conditionsAction.deadline != null) DEFAULT_PADDING_HALF else DEFAULT_PADDING * 2) + 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))), @@ -751,9 +788,12 @@ fun UsageConditionsView( ) 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_ }) } @@ -763,6 +803,7 @@ fun UsageConditionsView( } 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) } @@ -771,6 +812,18 @@ fun UsageConditionsView( } } +@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( 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 index 1ec2534ab1..6a999aa89d 100644 --- 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 @@ -43,6 +43,13 @@ fun ModalData.NewServerView( if (isActive) { newServer.value = res.first testing.value = false + val failure = res.second + if (failure != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_test_failed), + text = failure.localizedDescription + ) + } } } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index cc72387875..c619ae6ebc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -612,13 +612,14 @@ private fun SingleOperatorUsageConditionsView( } } +val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" + @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 } 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 index bebc96a28c..ccad962313 100644 --- 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 @@ -93,6 +93,13 @@ fun ProtocolServerView( if (isActive) { draftServer.value = res.first testing.value = false + val failure = res.second + if (failure != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_test_failed), + text = failure.localizedDescription + ) + } } } }, @@ -189,7 +196,7 @@ fun CustomServer( if (valid.value) { SectionDividerSpaced() SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) { - QRCode(serverAddress.value) + QRCode(serverAddress.value, small = true) } } } 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 4c781f0aab..41f454b2dd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -30,33 +30,33 @@ أضِف خوادم مُعدة مسبقًا أضِف إلى جهاز آخر سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا! - الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تفعيل هذا الخيار. - أضِف خادم + الوصول إلى الخوادم عبر وسيط SOCKS على المنفذ %d؟ يجب بدء تشغيل الوسيط قبل تفعيل هذا الخيار. + أضف خادم إعدادات الشبكة المتقدمة سيبقى جميع أعضاء المجموعة على اتصال. السماح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك. السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة) - المشرف - أضِف ملف التعريف + المُدير + أضف ملف التعريف السماح بإرسال رسائل مباشرة إلى الأعضاء. - قبول التخفي + اقبل التخفي أضِف رسالة ترحيب أضف الخوادم عن طريق مسح رموز QR. - يمكن للمشرفين إنشاء روابط للانضمام إلى المجموعات. + يمكن للمُدراء إنشاء روابط للانضمام إلى المجموعات. قبول طلب الاتصال؟ سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك. - مكالمة مقبولة + قُبلت المكالمة السماح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك. اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك. يتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن - فهو يسمح لخدمة الإشعارات بالعمل. يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم، ويفتح التطبيق كالمعتاد. أجب الاتصال - دائِماً + دائمًا السماح بإرسال رسائل تختفي. السماح بإرسال رسائل صوتية. تطبيق ثانوي إضافي - السماح لجهات اتصالك بإضافة ردود الفعل الرسالة. + السماح لجهات اتصالك بإضافة ردود الفعل للرسالة. السماح لجهات اتصالك بالاتصال بك. السماح بردود الفعل على الرسائل. يتم مسح جميع البيانات عند إدخالها. @@ -64,11 +64,11 @@ السماح لجهات اتصالك بإرسال رسائل تختفي. اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك. رمز مرور التطبيق - دائِماً مُتاح + يعمل دائمًا يمكن للتطبيق استلام الإشعارات فقط عند تشغيله، ولن يتم بدء تشغيل أي خدمة في الخلفية السماح بالرسائل الصوتية؟ ستبقى جميع جهات اتصالك متصلة. - استخدم التتابع دائمًا + استخدم الموجه دائمًا النسخ الاحتياطي لبيانات التطبيق حُذفت جميع بيانات التطبيق. السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) @@ -110,7 +110,7 @@ لون إضافي ثانوي " \nمتوفر في v5.1" - مكالمات الصوت/الفيديو محظورة. + مكالمات الصوت/الفيديو ممنوعة. عن طريق ملف تعريف الدردشة (افتراضي) أو عن طريق الاتصال (تجريبي). يمكن تعطيله عبر الإعدادات - سيستمر عرض الإشعارات أثناء تشغيل التطبيق.]]> مكالمات الصوت والفيديو @@ -121,7 +121,7 @@ مصادقة 1 دقيقة 30 ثانية - إلغاء الرسالة المباشرة + ألغِ الرسالة الحيّة إلغاء لكل جهة اتصال وعضو في المجموعة\n. الرجاء ملاحظة: إذا كان لديك العديد من الاتصالات، فقد يكون استهلاك البطارية وحركة المرور أعلى بكثير وقد تفشل بعض الاتصالات.]]> جارٍ الاتصال… @@ -143,7 +143,7 @@ قبول تلقائي المكالمات لا يمكن دعوة جهات الاتصال! - تم تغيير العنوان من أجلك + غُيِّر العنوان من أجلك طلب لاستلام الفيديو مكالمتك تحت الإجراء تغيير عبارة مرور قاعدة البيانات؟ @@ -174,14 +174,14 @@ تغيير عنوان الاستلام؟ نٌسخت إلى الحافظة مسح - مسح الدردشة - إنشاء عنوان + امسح الدردشة + أنشئ عنوان الدردشات تأكيد عبارة المرور الجديدة… تعمية قاعدة البيانات؟ قاعدة البيانات مُعمّاة غيرت دور %s إلى %s - تغيير عنوان الاستلام + غيّر عنوان الاستلام خطأ في إنشاء ملف التعريف! خطأ في الإتصال انتهت مهلة الاتصال @@ -189,14 +189,14 @@ خطأ في حذف طلب جهة الاتصال خطأ في حذف المجموعة اتصل - إنشاء ملف - إنشاء قائمة انتظار + أنشئ ملف + أنشئ قائمة انتظار قارن الملف خطأ - إنشاء مجموعة سرية + أنشئ مجموعة سرية خطأ في إحباط تغيير العنوان - تفعيل قفل SimpleX - تأكد من بيانات الاعتماد الخاصة بك + فعّل قفل SimpleX + تأكد من بيانات اعتمادك أنشئ عنوان SimpleX متابعة تحدث مع المطورين @@ -210,12 +210,12 @@ الاتصال ملف تعريف الدردشة الإصدار الأساسي: v%s - إنشاء ملف تعريف + أنشئ ملف تعريف جار الاتصال… انتهى متصل %1$d تخطت الرسائل - تفعيل القفل + فعّل القفل تأكيد رمز المرور خطأ في حذف قاعدة بيانات الدردشة خطأ في تعمية قاعدة البيانات @@ -224,7 +224,7 @@ جاري الاتصال (أعلن) الاتصال إحباط تغيير العنوان - إنشاء مجموعة سرية + أنشئ مجموعة سرية قارن رموز الأمان مع جهات اتصالك. الواجهة الصينية والاسبانية مسح @@ -244,7 +244,7 @@ مسح خطأ في إنشاء رابط المجموعة (حاضِر) - تفعيل أبقِ TCP على قيد الحياة + فعّل أبقِ TCP على قيد الحياة جار الاتصال… جار الاتصال… أرسلت طلب الاتصال! @@ -253,8 +253,8 @@ جار الاتصال (قُبِل) فُحصت جهة الاتصال %1$s أعضاء - إنشاء رابط المجموعة - إنشاء رابط + أنشئ رابط المجموعة + أنشئ رابط تأكيد كلمة المرور توقفت الدردشة قاعدة بيانات الدردشة @@ -263,7 +263,7 @@ خطأ في تغيير العنوان %1$d رسائل تخطت تغيير وضع القفل - تفعيل رمز التدمير الذاتي + فعّل رمز التدمير الذاتي تغيير دور المجموعة؟ تفضيلات الدردشة أدخل عبارة المرور الصحيحة. @@ -286,7 +286,7 @@ الاتصال %1$d انشأت الاتصال مكالمة جارية… - عَمِّ + تشفير أدخل عبارة المرور… المنشئ خطأ في إضافة الأعضاء @@ -306,17 +306,17 @@ سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا! الحد الأقصى لحجم الملف المدعوم حاليًا هو %1$s. تواصل عبر الرابط / رمز QR - إنشاء رابط دعوة لمرة واحدة + أنشئ رابط دعوة لمرة واحدة تحقق من عنوان الخادم وحاول مرة أخرى. - مسح التَحَقُّق + امسح التحقُّق أنشئ عنوانًا للسماح للأشخاص بالتواصل معك. أدخل الخادم يدويًا ملون لدى جهة الاتصال التعمية بين الطريفين - إنشاء + أنشئ أنشئ ملف تعريفك مكالمة جارية... - تفعيل التدمير الذاتي + فعّل التدمير الذاتي الموافقة على التعمية… الموافقة على التعمية لـ%s… متصل (مقدم) @@ -332,27 +332,27 @@ سمة داكنة حُذفت عبارة مرور قاعدة البيانات وتصديرها - حذف جميع الملفات - حذف بعد - حذف الملف + احذف جميع الملفات + احذف بعد + احذف الملف حذف حذف رسالة العضو؟ - حذف - حذف الرسائل - حذف الرسائل بعد + احذف + احذف الرسائل + احذف الرسائل بعد قاعدة البيانات مُعمّاة! تختلف عبارة مرور قاعدة البيانات عن تلك المحفوظة في Keystore. خطأ في قاعدة البيانات ترقية قاعدة البيانات حُددت %d جهة اتصال - حذف المجموعة + احذف المجموعة حذف المجموعة؟ - حذف الرابط + احذف الرابط حُذفت في: %s المجموعة حُذفت - حذف الصورة + احذف الصورة تخصيص السمات - حذف قاعدة البيانات + احذف قاعدة البيانات حذف ملف تعريف الدردشة؟ حذف الملفات لجميع ملفات تعريف الدردشة قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية، يمكنك تغييرها. @@ -361,31 +361,31 @@ إصدار قاعدة البيانات أحدث من التطبيق، ولكن لا يوجد ترحيل لأسفل ل%s معرّف قاعدة البيانات: %d حذف ملف تعريف الدردشة؟ - حذف ملف تعريف الدردشة ل - حذف للجميع + احذف ملف تعريف الدردشة ل + احذف للجميع أيام - حذف العنوان + احذف العنوان سيتم تحديث عبارة مرور تعمية قاعدة البيانات. حذف الرابط؟ الرجوع إلى إصدار سابق من قاعدة البيانات قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية. يُرجى تغييره قبل التصدير. %d يوم سيتم تعمية قاعدة البيانات. - حذف + احذف حذف الملفات والوسائط؟ - حذف جهة الاتصال + احذف جهة الاتصال تخصيص السمة داكن الافتراضي %s حذف الاتصال قيد الانتظار؟ - حذف ملف تعريف الدردشة + احذف ملف تعريف الدردشة خطأ في فك التعمية حذف الرسالة؟ معرفات قاعدة البيانات وخيار عزل النقل. حذف العنوان؟ خطأ في فك الترميز حذف جهة الاتصال؟ - حذف بالنسبة لي + احذف بالنسبة لي وقت مخصّص لامركزي عبارة مرور قاعدة البيانات @@ -399,11 +399,11 @@ تخصيص ومشاركة سمات الألوان. الخروج بدون حفظ أدوات المطور - حذف قائمة الانتظار + احذف قائمة الانتظار خطأ في تحديث خصوصية المستخدم مسح رمز QR.]]> - حذف ملف التعريف - حذف الخادم + احذف ملف التعريف + احذف الخادم خطأ في تحديث رابط المجموعة الوصف توسيع تحديد الدور @@ -428,7 +428,7 @@ مزيد من تقليل استخدام البطارية المجموعة للجميع - لم يتم العثور على الملف + لم يُعثر على الملف من المعرض الواجهة الفرنسية المساعدة @@ -464,8 +464,8 @@ سيتم استلام الملف عندما يكتمل جهة اتصالك من رفعِها. المساعدة الملف: %s - إصلاح - إصلاح الاتصال + أصلح + أصلح الاتصال إصلاح الاتصال؟ الإصلاح غير مدعوم من قبل أعضاء المجموعة يمكن للأعضاء إرسال الملفات والوسائط. @@ -486,7 +486,7 @@ سيتم استلام الصورة عندما يكتمل جهة اتصالك من رفعِها. اعرض رمز QR في مكالمة الفيديو، أو شارك الرابط.]]> ثبّت SimpleX Chat لطرفية - إذا قمت بالتأكيد، فستتمكن خوادم المراسلة من رؤية عنوان IP الخاص بك ومزود الخدمة الخاص بك - أي الخوادم التي تتصل بها. + إذا أكّدت، فستتمكن خوادم المُراسلة من رؤية عنوان IP الخاص بك ومزود خدمتك - أي الخوادم التي تتصل بها. إخفاء: إذا أدخلت رمز المرور هذا عند فتح التطبيق، فستتم إزالة جميع بيانات التطبيق نهائيًا! استيراد قاعدة بيانات الدردشة؟ @@ -547,18 +547,18 @@ \n3. اُخترق الاتصال. واجهة أستخدام يابانية وبرتغالية يمكن أن يحدث ذلك عندما تستخدم أنت أو اتصالك النُسخة الاحتياطية القديمة لقاعدة البيانات. - انضمام ك%s + انضم ك%s رمز QR غير صالح الواجهة الإيطالية الرابط غير صالح! دعوة الأصدقاء خطأ في Keychain دعوة للمجموعة - يٌمنع حذف الرسائل بشكل لا رجعة فيه. + يُمنع حذف الرسائل بشكل لا رجعة فيه. تنسيق الرسالة غير صالح البيانات غير صالحة بيانات ملف التعريف المحلية فقط - يٌمنع حذف الرسائل بشكل لا رجعة فيه في هذه الدردشة. + يُمنع حذف الرسائل بشكل لا رجعة فيه في هذه الدردشة. دعوة الأعضاء غادِر المجموعة الاسم المحلي: @@ -567,7 +567,7 @@ مدعو %1$s مدعو عبر رابط المجموعة الدردشة غير صالحة - حي + حيّ رابط اتصال غير صالح الملف كبير! معرفة المزيد @@ -577,7 +577,7 @@ تأكيد الترحيل غير صالح مدعو رابط معاينة الصورة - رسالة مباشرة! + رسالة حيّة! مائل لنتحدث في SimpleX Chat قفل بعد @@ -585,14 +585,14 @@ انتهت صلاحية الدعوة! انضمام إلى المجموعة؟ الانضمام المتخفي - الانضمام إلى المجموعة + جارِ الانضمام إلى المجموعة غادِر مغادرة المجموعة؟ غادر - إيصالات التسليم معطلة! - تعطيل + إيصالات التسليم مُعطَّلة! + عطّل رسائل تختفي - عٌطل مصادقة الجهاز. جاري إيقاف تشغيل قفل SimpleX. + استيثاق الجهاز مُعطَّل. جارِ إيقاف تشغيل قفل SimpleX. %d شهر %d أسابيع %d أسبوع @@ -601,7 +601,7 @@ قطع الاتصال مصادقة الجهاز غير مفعّلة. يمكنك تشغيل قفل SimpleX عبر الإعدادات، بمجرد تفعيل مصادقة الجهاز. نزّل الملف - تعطيل قفل SimpleX + عطّل قفل SimpleX تحرير اسم ملف التعريف: البريد الإلكتروني @@ -610,27 +610,27 @@ يختفي في: %s الرسائل المختفية ممنوعة في هذه الدردشة. مُعمّى بين الطريفين - حُرر + حُرّر الرجوع إلى إصدار سابق وفتح الدردشة رسائل مباشرة الرسائل المختفية ممنوعة. - تحرير ملف تعريف المجموعة + حرّر ملف تعريف المجموعة لا تُظهر مرة أخرى الجهاز %d أسبوع لا يمكن أن يحتوي اسم العرض على مسافة فارغة. مكالمة فيديو مُعمّاة بين الطريفين - الرسائل المباشرة بين الأعضاء ممنوعة في هذه المجموعة. + يُمنع إرسال الرسائل المباشرة بين الأعضاء في هذه المجموعة. %d ساعة %d ساعة %d ساعات %d شهر - تحرير الصورة + حرّر الصورة %d ملف/ات بإجمالي الحجم %s %d ثانية جهات الاتصال - تعطيل للجميع - تعطيل (الاحتفاظ بالتجاوزات) + عطّل للجميع + عطّل (الاحتفاظ بالتجاوزات) تعطيل الإيصالات؟ الهجرة المختلفة في التطبيق / قاعدة البيانات: %s / %s يختفي في @@ -669,16 +669,16 @@ تأكد من أن الملف يحتوي على بناء جملة YAML الصحيح. تصدير النسق للحصول على مثال لهيكل ملف النسق. 40 ثانية كحد أقصى، يتم استلامها على الفور. خطأ في حفظ خوادم ICE - إجراء اتصال خاص + أجري اتصال خاص خطأ في إيقاف الدردشة خطأ في تحميل التفاصيل - تفعيل المكالمات من شاشة القفل عبر الإعدادات. + فعّل المكالمات من شاشة القفل عبر الإعدادات. تفعيل الحذف التلقائي للرسائل؟ خطأ في إزالة العضو خطأ في تحديد العنوان علّم مقروءة - تفعيل للجميع - تفعيل (الاحتفاظ بالتجاوزات) + فعِّل للجميع + فعّل (الاحتفاظ بالتجاوزات) تفعيل الإيصالات؟ خطأ في بدء الدردشة خطأ في تصدير قاعدة بيانات الدردشة @@ -686,7 +686,7 @@ اجعل ملف التعريف خاصًا! تصفية الدردشات غير المقروءة والمفضلة. البحث عن الدردشات بشكل أسرع - تفعيل + فعّل حتى عندما يتم تعطيله في المحادثة. إصلاح التعمية بعد استعادة النُسخ الاحتياطية. اجعل رسالة واحدة تختفي @@ -695,7 +695,7 @@ خطأ في إرسال الرسالة خطأ في الانضمام إلى المجموعة خطأ في مزامنة الاتصال - تسجيل الدخول باستخدام بيانات الاعتماد الخاصة بك + سجّل الدخول باستخدام بيانات اعتمادك خطأ في حفظ الملف علّم غير مقروءة تأكد من أن عناوين خادم WebRTC ICE بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة. @@ -704,7 +704,7 @@ خطأ في حفظ ملف تعريف المجموعة رسالة نصية ردود فعل الرسائل - سيتم وضع علامة على الرسالة للحذف. سيتمكن المستلم/مون من الكشف عن هذه الرسالة. + سيتم وضع علامة على الرسالة للحذف. سيتمكن المُستلم/ون من الكشف عن هذه الرسالة. سيتم حذف الرسالة - لا يمكن التراجع عن هذا! خطأ في تسليم الرسالة الشبكة والخوادم @@ -712,7 +712,7 @@ فتح في تطبيق الجوال، ثم انقر فوق اتصال في التطبيق.]]> تحت الإشراف في: %s ردود الفعل الرسائل ممنوعة في هذه الدردشة. - مُشرف بواسطة %s + أُشرف بواسطة %s من المرجح أن جهة الاتصال هذه قد حذفت الاتصال بك. ردود فعل الرسائل مكالمة فائتة @@ -729,9 +729,7 @@ إيصالات تسليم الرسائل! دقائق شهور - - توصيل رسائل أكثر استقرارًا. -\n- مجموعات أفضل قليلاً. -\n- و اكثر! + - توصيل رسائل أكثر استقرارًا.\n- مجموعات أفضل قليلاً.\n- و اكثر! حالة الشبكة كتم ردود الفعل الرسائل ممنوعة. @@ -757,7 +755,7 @@ سيتم استخدام مضيفات البصل عند توفرها. لن يتم استخدام مضيفات البصل. لم تٌحدد جهات اتصال - يمكن للمشرف الآن:\n- حذف رسائل الأعضاء.\n- تعطيل الأعضاء (دور المراقب) + يمكن للمُدراء الآن:\n- حذف رسائل الأعضاء.\n- تعطيل الأعضاء (دور المراقب) خدمة الإشعار غير مفعّل` مفعل @@ -777,18 +775,17 @@ يمكن إرسال 10 فيديوهات فقط في نفس الوقت رابط دعوة لمرة واحدة لا - سوف تكون مضيفات البصل مطلوبة للاتصال. -\nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion. + ستكون مضيفات البصل مطلوبة للاتصال. \nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion. اسم عرض جديد: عبارة مرور جديدة… قيد الانتظار - كلمة المرور مطلوبة + عبارة المرور مطلوبة ألصِق الرابط الذي استلمته فقط مالكي المجموعة يمكنهم تفعيل الملفات والوسائط. فقط مالكي المجموعة يمكنهم تفعيل الرسائل الصوتية. (يخزن فقط بواسطة أعضاء المجموعة) - كلمة المرور - تم تعيين كلمة المرور! + رمز المرور + عيّنت رمز المرور! المالك فقط جهة اتصالك يمكنها إرسال رسائل تختفي. جهة اتصالك فقط يمكنها إضافة ردود الفعل على الرسالة @@ -796,26 +793,26 @@ جهة اتصالك فقط يمكنها حذف الرسائل بشكل لا رجعة فيه (يمكنك تعليم الرسالة للحذف). (24 ساعة) أنت فقط يمكنك إرسال رسائل صوتية. افتح - لم يتم تغيير كلمة المرور! - تم تغيير كلمة المرور + لم يُغيّر رمز المرور! + غُيّر رمز المرور! جارِ فتح قاعدة البيانات… جهة اتصالك فقط يمكنها إرسال رسائل صوتية. ألصق - كلمة المرور غير موجودة في مخزن المفاتيح، يرجى إدخالها يدوياً. قد يحدث هذا إذا قمت باستعادة ملفات التطبيق باستخدام أداة استرجاع بيانات. إذا لم يكن الأمر كذلك، تواصل مع المبرمجين رجاء + لم يُعثر على عبارة المرور في Keystore، يُرجى إدخالها يدويًا. ربما حدث هذا إذا استعدت بيانات التطبيق باستخدام أداة النسخ الاحتياطي. إذا لم يكن الأمر كذلك، يُرجى التواصل مع المطورين. افتح الدردشة - فتح الرابط في المتصفح قد يقلل خصوصية وحماية اتصالك. الروابط غير الموثوقة من SimpleX ستكون باللون الأحمر + قد يؤدي فتح الرابط في المتصفح إلى تقليل خصوصية الاتصال وأمانه. ستظهر روابط SimpleX غير الموثوقة باللون الأحمر. أنت فقط يمكنك إضافة ردود الفعل على الرسالة. أنت فقط يمكنك حذف الرسائل بشكل لا رجعة فيه (يمكن للمستلم تعليمها للحذف). (24 ساعة) أنت فقط يمكنك إرسال رسائل تختفي أنت فقط يمكنك إجراء المكالمات. فقط جهة اتصالك يمكنها إجراء المكالمات. افتح وحدة تحكم الدردشة - إدخال كلمة المرور - افتح SimpleX Chat للرد على المكالمة + إدخال رمز المرور + افتح SimpleX Chat لقبول المكالمة يمكن لأي شخص استضافة الخوادم. كلمة المرور للإظهار ندّ لِندّ - أنت تقرر من يمكنه الاتصال. + أنت تقرر مَن يمكنه الاتصال. مكالمة قيد الانتظار تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل. صفّر الألوان @@ -829,7 +826,7 @@ الإشعارات الدورية مُعطَّلة صورة ملف التعريف الإشعارات خاصة - يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. + يُرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. يُرجى تحديث التطبيق والتواصل مع المطورين. دليل المستخدم.]]> غيّر ملفات تعريف الدردشة @@ -839,20 +836,20 @@ رفض قيم التطبيق منفذ - حفظ إعدادات القبول التلقائي + احفظ إعدادات عنوان SimpleX إعادة تعريف الخصوصية الرجاء الإبلاغ للمطورين. الخصوصية والأمان - إزالة + أزل إزالة عبارة المرور من Keystore؟ الرجاء إدخال عبارة المرور الحالية الصحيحة. - يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. - منع مكالمات الصوت/الفيديو. - منع حذف الرسائل التي لا رجعة فيها. + يُرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. + امنع مكالمات الصوت/الفيديو. + امنع حذف الرسائل التي لا رجعة فيها. استلام الملفات غير معتمد حتى الآن الرجاء التحقق من اتصالك بالشبكة مع %1$s وحاول مرة أخرى. ستتم إزالة خوادم WebRTC ICE المحفوظة. - منع إرسال الملفات والوسائط. + امنع إرسال الملفات والوسائط. استلمت إجابة… مستودع GitHub.]]> رفض @@ -861,9 +858,9 @@ استعادة النسخة الاحتياطية لقاعدة البيانات؟ حفظ اتصالات ملف التعريف والخادم - منع ردود فعل الرسالة. - منع إرسال الرسائل الصوتية. - منع ردود فعل الرسائل. + امنع ردود فعل الرسالة. + امنع إرسال الرسائل الصوتية. + امنع ردود فعل الرسائل. قراءة المزيد انخفاض استخدام البطارية سجل رسالة صوتية @@ -879,7 +876,7 @@ حٌديثت السجل في حٌديثت السجل في: %s استعادة - يرى المستلمون التحديثات أثناء كتابتها. + يرى المُستلمون التحديثات أثناء كتابتها. استلمت، ممنوع حفظ سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك. @@ -891,37 +888,37 @@ تشغيل الدردشة استعادة النسخة الاحتياطية لقاعدة البيانات خطأ في استعادة قاعدة البيانات - أُزيلت %1$s - أزالتك + أُزيل %1$s + أزالك استلمت في - إزالة العضو - إزالة + أزل العضو + أزل صفّر إلى الإعدادات الافتراضية بينج الفاصل الزمني كلمة مرور ملف التعريف - منع إرسال الرسائل التي تختفي. + امنع إرسال الرسائل التي تختفي. مهلة البروتوكول مهلة البروتوكول لكل كيلوبايت - منع إرسال الرسائل الصوتية. - منع إرسال رسائل مباشرة إلى الأعضاء. - منع إرسال الرسائل التي تختفي. + امنع إرسال الرسائل الصوتية. + امنع إرسال رسائل مباشرة إلى الأعضاء. + امنع إرسال الرسائل التي تختفي. الاحتفاظ بمسودة الرسالة الأخيرة، مع المرفقات. أسماء ملفات خاصة - حماية ملفات تعريف الدردشة الخاصة بك بكلمة مرور! + احمِ ملفات تعريف دردشتك بكلمة مرور! رُفضت المكالمة حماية شاشة التطبيق - أُزيلت - يرجى تذكرها أو تخزينها بأمان - لا توجد طريقة لاستعادة كلمة المرور المفقودة! + أُزيل + يُرجى تذكرها أو تخزينها بأمان - لا توجد طريقة لاستعادة كلمة المرور المفقودة! معاينة - من المحتمل أن الملف المرجعي للشهادة في عنوان الخادم غير صحيح + البصمة في عنوان الخادم لا تتطابق مع الشهادة. يتم استلام الرسائل… - يُرجى الاتصال بمشرف المجموعة. + يُرجى الاتصال بمُدير المجموعة. أعد التفاوض إعادة تفاوض التعمية سحب وصول الملف سحب وصول الملف؟ رٌفض الإذن! - يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية. + يُرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية. العنصر النائب لصورة ملف التعريف رمز QR صفّر @@ -935,12 +932,12 @@ %s في %s حفظ ملف المجموعة ثانوي - كلمة مرور التدمير الذاتي + رمز المرور للتدمير الذاتي إرسال الملفات غير مدعوم بعد - قام المرسل بإلغاء إرسال الملف + أُلغيَ المرسل نقل الملف. (امسح أو ألصق من الحافظة) ثانية - قد يكون المرسل قد ألغى طلب الاتصال + ربما حذف المرسل طلب الاتصال. مسح رمز QR أرسل لنا بريداً مسح رمز الأمان من تطبيق جهة الاتصال @@ -948,66 +945,66 @@ سيتم إيقاف إرسال الملف. إرسال رسالة إرسال - إرسال رسالة حية + أرسل رسالة حيّة فشلت تجربة الخادم! - حفظ كلمة المرور في مخزن المفاتيح + احفظ عبارة المرور في Keystore أرسل رسالة مباشرة إرسال عبر الخوادم تقييم الأمان الرسائل المرسلة سيتم حذفها بعد المدة المحدّدة. تعيين رسالة تظهر للأعضاء الجدد! - تعيين كلمة المرور - تم إرساله في: %s + عيّن رمز المرور + أُرسلت في: %s %s (الحالي) رسالة مرسلة عيّن تفضيلات المجموعة - عيينها بدلا من توثيق النظام + عيّنها بدلاً من استيثاق النظام. مشاركة إرسال - حفظ كلمة المرور وفتح الدردشة - حدد جهات اتصال + احفظ عبارة المرور وافتح الدردشة + حدد جهات الاتصال تعيين يوم واحد ثواني رسالة مرسلة أرسل رسالة تختفي - حفظ كلمة مرور الحساب + حفظ كلمة مرور ملف التعريف تدمير ذاتي - مسح الكود - إرسال أسئلة وأفكار + مسح الرمز + أرسل أسئلة وأفكار مشاركة العنوان مع جهات الاتصال؟ - مشاركة العنوان + شارك العنوان حفظ رسالة الترحيب؟ - حفظ السيرفرات - أرسل تقارير الاستلام إلى - إرسال تقارير الاستلام معطل لـ %d جهة اتصال. - إرسال تقارير الاستلام مفعل لـ %d جهة اتصال - تعيين كلمة المرور للتصدير + احفظ الخوادم + أرسل إيصالات التسليم إلى + إرسال الإيصالات مُعطَّل لـ %d جهة اتصال. + إرسال الإيصالات مفعّل لـ %d جهة اتصال + عيّن عبارة المرور للتصدير تم تغيير رمز الأمان - تقارير الارسال - تم إرساله في + إيصالات الإرسال + أُرسلت في حدد - إرسال تقارير الاستلام سيتم تفعيله لجميع جهات الاتصال. - سيتم تفعيل إرسال تقارير الاستلام لجميع جهات الاتصال ذات حسابات دردشة ظاهرة + سيتم تفعيل إرسال إيصالات التسليم لجميع جهات الاتصال. + سيتم تفعيل إرسال إيصالات التسليم لجميع جهات الاتصال في جميع ملفات تعريف الدردشة المرئية. قائمة انتظار آمنة فشل الإرسال - تم الإرسال + أُرسلت تعيين اسم جهة الاتصال… - إرسال رسالة حية - سيتم تحديثها للمستلم مع كتابتك لها + أرسل رسالة حيّة - سيتم تحديثها للمُستلم مع كتابتك لها تعيين اسم جهة الاتصال الإعدادات حفظ الخوادم؟ مسح رمز QR الخادم رمز الأمان - حفظ الإعدادات؟ + حفظ التفضيلات؟ حفظ الإعدادات؟ سري - كلمة مرور التدمير الذاتي - تم تغيير كلمة مرور التدمير الذاتي! - تم تفعيل كلمة مرور التدمير الذاتي + رمز المرور للتدمير الذاتي + تغيّرت رمز المرور للتدمير الذاتي! + فعّلت رمز المرور للتدمير الذاتي! الإعدادات - دعوة لمرة واحدة SimpleX - عرض جهة الاتصال والرسالة + دعوة SimpleX لمرة واحدة + أظهر جهة الاتصال والرسالة قفل SimpleX لم يتحقق من %s قفل SimpleX @@ -1020,20 +1017,20 @@ مشاركة الملف… شعار SimpleX فريق SimpleX - عرض رمز QR + أظهر رمز QR تم التحقق %s فشلت بعض الخوادم في الاختبار: إرسال معاينات الرابط تخطي دعوة الأعضاء إيقاف الدردشة؟ - عرض + أظهر حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد: - وكيل SOCKS + وسيط SOCKS تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits. إيقاف - عرض المعاينة + أظهر المعاينة السماعة متوقفة - وضع قفل SimpleX + SimpleX وضع القفل مشاركة الرابط الرسائل التي تم تخطيها عنوان SimpleX @@ -1049,13 +1046,13 @@ أوقف الدردشة لتصدير أو استيراد أو حذف قاعدة بيانات الدردشة. لن تتمكّن من استلام الرسائل وإرسالها أثناء إيقاف الدردشة. %s ثانية/ثواني يبدأ… - تم تشغيل القفل SimpleX - عرض: - عرض خيارات المطور + شُغّل قفل SimpleX + أظهر: + أظهر خيارات المطور simplexmq: v%s (%2s) - يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور - يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور - عرض جهة الاتصال فقط + يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور. + يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور. + أظهر جهة الاتصال فقط مكالمات SimpleX Chat خدمة SimpleX Chat يبدأ بشكل دوري @@ -1063,13 +1060,13 @@ إيقاف الملف التوقف عن إرسال الملف؟ عنوان SimpleX - استخدم مضيفي .onion إلى "لا" إذا كان وكيل SOCKS لا يدعمها.]]> + استخدم مضيفي .onion إلى "لا" إذا كان وسيط SOCKS لا يدعمها.]]> مشاركة مع جهات الاتصال إيقاف التشغيل؟ - إعدادات وكيل SOCKS + إعدادات وسيط SOCKS إيقاف التشغيل السماعة قيد التشغيل - إرسال + أرسل النظام السمة لبدء محادثة جديدة @@ -1077,7 +1074,7 @@ لامركزية بالكامل – مرئية للأعضاء فقط. النظام فشل الاختبار في الخطوة %s. - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! يجلب التطبيق الرسائل الجديدة بشكل دوري - يستخدم نسبة قليلة من البطارية يوميًا. لا يستخدم التطبيق إشعارات الدفع - لا يتم إرسال البيانات من جهازك إلى الخوادم. سيتم إلغاء الاتصال الذي قبلته! لن تتمكن جهة الاتصال التي شاركت هذا الرابط معها من الاتصال! @@ -1085,37 +1082,37 @@ لحماية خصوصيتك، يستخدم SimpleX معرّفات منفصلة لكل جهة اتصال لديك. لحماية معلوماتك، فعّل قفل SimpleX \nسيُطلب منك إكمال المصادقة قبل تفعيل هذه الميزة. عزل النقل - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! دعم البلوتوث وتحسينات أخرى. - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! يتم تشغيل SimpleX في الخلفية بدلاً من استخدام إشعارات push.]]> انقر لبدء محادثة جديدة (للمشاركة مع جهة اتصالك) للتواصل عبر الرابط للاتصال، يمكن لجهة الاتصال مسح رمز QR أو استخدام الرابط في التطبيق. - خوادم الاختبار + اختبر الخوادم لا معرّفات مُستخدم دعم SIMPLEX CHAT تبديل العنوان الرئيسي سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء. انقر للانضمام - للكشف عن ملف تعريفك المخفي، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك. + للكشف عن ملف تعريفك المخفي، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف دردشتك. انقر للانضمام إلى وضع التخفي النظام السمات - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! قاعدة البيانات لا تعمل بشكل صحيح. انقر لمعرفة المزيد ألوان الواجهة انقر لتنشيط ملف التعريف. عزل النقل هذه السلسلة ليست رابط اتصال! هذه الإعدادات لملف تعريفك الحالي - يمكن تجاوزها في إعدادات الاتصال و المجموعة. + يمكن تجاوزها في إعدادات الاتصال والمجموعة. انتهت مهلة اتصال TCP لحماية المنطقة الزمنية، تستخدم ملفات الصور / الصوت التوقيت العالمي المنسق (UTC). فقدنا القراد الثاني! ✅ - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! لم تكتمل محاولة تغيير عبارة مرور قاعدة البيانات. لاستلام الإشعارات، يُرجى إدخال عبارة مرور قاعدة البيانات مصادقة النظام @@ -1126,26 +1123,26 @@ مقاطع الفيديو كثيرة! زر النقر شكرًا لك على تثبيت SimpleX Chat! - خادم الاختبار + اختبر الخادم تجزئة الرسالة السابقة مختلفة. معرف الرسالة التالية غير صحيح (أقل أو يساوي السابق). \nيمكن أن يحدث ذلك بسبب بعض العلل أو عندما يُخترق الاتصال. - إزالة من المفضلة - محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه. + أزل من المفضلة + محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من هذا الاتصال. اختيار ملف إرسال غير مصرح به - محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s). + محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s). تشغيل خوادم WebRTC ICE أنت تستخدم ملف تعريف متخفي لهذه المجموعة - لمنع مشاركة ملفك التعريفي الرئيسي الذي يدعو جهات الاتصال غير مسموح به غيّرتَ دور %s إلى %s نعم - أنت متصل بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه. + أنت متصل بالخادم المستخدم لاستلام الرسائل من هذا الاتصال. أنت لقد شاركت رابط لمرة واحدة سيتم إرسال ملف التعريفك إلى جهة الاتصال التي استلمت منها هذا الرابط. - سوف تتصل بجميع أعضاء المجموعة. - ملفات تعريف الدردشة الخاصة بك + ستتصل بجميع أعضاء المجموعة. + ملفات تعريف دردشتك عنوان SimpleX الخاص بك خوادم SMP الخاصة بك عندما يكون التطبيق قيد التشغيل @@ -1160,12 +1157,12 @@ تحقق من رمز الأمان رسائل صوتية عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه. - سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! - سوف تكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا! تستخدم خوادم SimpleX Chat. - استخدم وكيل SOCKS + استخدم وسيط SOCKS استخدم مضيفي onion. - استخدام وكيل SOCKS؟ + استخدام وسيط SOCKS؟ عندما تكون متاحة ستبقى جهات اتصالك متصلة. لا نقوم بتخزين أي من جهات اتصالك أو رسائلك (بمجرد تسليمها) على الخوادم. @@ -1203,21 +1200,21 @@ عبر رابط لمرة واحدة مكالمة الفيديو ليست مُعمّاة بين الطريفين غيّرتَ العنوان - سوف تكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! غادرت - يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال. + يجب عليك استخدام أحدث إصدار من قاعدة بيانات دردشتك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال. سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s. - أُزيلت %1$s - تحديث - قاعدة بيانات الدردشة الخاصة بك غير مُعمّاة - عيّن عبارة مرور لحمايتها. + أزلت %1$s + حدّث + قاعدة بيانات دردشتك غير مُعمّاة - عيّن عبارة مرور لحمايتها. عبارة مرور قاعدة بيانات خاطئة سيتم إرسال ملف تعريف الدردشة الخاص بك إلى أعضاء المجموعة مرحبًا! %1$s يريد الاتصال بك! خوادم ICE الخاصة بك خصوصيتك - حدثت ملف تعريف المجموعة + حُدثت ملف تعريف المجموعة أنت: %1$s تحديث تحديث إعدادات الشبكة؟ @@ -1235,7 +1232,7 @@ سيتم حذف قاعدة بيانات الدردشة الحالية واستبدالها بالقاعدة المستوردة. \nلا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي. تحديث عبارة مرور قاعدة البيانات - سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ بسجل الدردشة. + ستتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ بسجل الدردشة. أسابيع يمكنك إخفاء أو كتم ملف تعريف المستخدم - اضغط مطولاً للقائمة. ما هو الجديد @@ -1260,29 +1257,26 @@ استخدم للاتصالات الجديدة استخدم الخادم عنوان خادمك - قاعدة بيانات الدردشة الخاصة بك + قاعدة بيانات دردشتك أنت مدعو إلى المجموعة. انضم للتواصل مع أعضاء المجموعة. - لقد انضممت إلى هذه المجموعة. الاتصال بدعوة عضو المجموعة. + لقد انضممت إلى هذه المجموعة. جارِ الاتصال بدعوة عضو المجموعة. غيّرتَ العنوان ل%s إلغاء إخفاء ملف تعريف الدردشة الرسائل الصوتية ممنوعة في هذه الدردشة. مقاطع فيديو وملفات تصل إلى 1 جيجا بايت - - رسائل صوتية تصل إلى 5 دقائق. -\n- الوقت المخصص لتختفي. -\n- تحرير التاريخ. + - رسائل صوتية تصل إلى 5 دقائق.\n- الوقت المخصص لتختفي.\n- تحرير التاريخ. يمكنك تفعيلة لاحقًا عبر الإعدادات يمكنك تفعيلها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق. عبر رابط المجموعة لقد شاركت رابط لمرة واحدة متخفي عبر المتصفح يجب عليك إدخال عبارة المرور في كل مرة يبدأ فيها التطبيق - لا يتم تخزينها على الجهاز. - قم بالترقية وفتح الدردشة + رقِّ وافتح الدردشة رسالة الترحيب عبر رابط عنوان الاتصال ما لم يحذف جهة الاتصال الاتصال أو استُخدم هذا الرابط بالفعل، فقد يكون خطأ - الرجاء الإبلاغ عنه. \nللاتصال، يُرجى مطالبة جهة اتصالك بإنشاء رابط اتصال آخر والتحقق من أن لديك اتصال شبكة ثابت. - سيتم إرسال ملف تعريف الدردشة الخاص بك -\nإلى جهة اتصالك + سيتم إرسال ملف تعريف دردشتك\nإلى جهة اتصالك إلغاء الإخفاء ملفك التعريفي العشوائي ستستمر في استلام المكالمات والإشعارات من الملفات التعريفية المكتومة عندما تكون نشطة. @@ -1295,7 +1289,7 @@ رسالة صوتية رسالة صوتية… أنت مدعو إلى المجموعة - لا يمكنك إرسال رسائل! + أنت المراقب! تحتاج إلى السماح لجهة اتصالك بإرسال رسائل صوتية لتتمكن من إرسالها. أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s). الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات.]]> @@ -1306,17 +1300,17 @@ مع رسالة ترحيب اختيارية. قريباً! هذه الميزة ليست مدعومة بعد. جرب الإصدار القادم. - تعطيل (الاحتفاظ بتجاوزات المجموعة) - تفعيل لجميع المجموعات + عطّل (الاحتفاظ بتجاوزات المجموعة) + فعِّل لجميع المجموعات إرسال الإيصالات مفعّلة لـ%d مجموعات - تعطيل لجميع المجموعات + عطّل لجميع المجموعات الإيصالات مُعطَّلة %s: %s تضم هذه المجموعة أكثر من %1$d عضو، ولا يتم إرسال إيصالات التسليم. التوصيل مُعطَّل تعطيل الإيصالات للمجموعات؟ - تفعيل (الاحتفاظ بتجاوزات المجموعة) + فعّل (الاحتفاظ بتجاوزات المجموعة) تفعيل الإيصالات للمجموعات؟ لا توجد معلومات عن التسليم لا توجد دردشة محدّدة @@ -1326,7 +1320,7 @@ سيتم إرسال طلب الاتصال لعضو المجموعة هذا. اتصال متخفي استخدم ملف التعريف الحالي - تعطيل الإشعارات + عطّل الإشعارات افتح إعدادات التطبيق لا يمكن تشغيل SimpleX في الخلفية. ستستلم الإشعارات فقط عندما يكون التطبيق قيد التشغيل. سيتم مشاركة ملف تعريف عشوائي جديد. @@ -1341,40 +1335,37 @@ أنت دعوت جهة اتصال مسودة الرسالة %s و %s متصل - إظهار الرسائل الأخيرة + أظهر الرسائل الأخيرة %s، %s و %d أعضاء آخرين متصلون %s، %s و %s متصل سيتم تعمية قاعدة البيانات وتخزين عبارة المرور في الإعدادات. - يُخزين عبارة المرور العشوائية في الإعدادات كنص عادي. -\nيمكنك تغييره لاحقا. + يُخزين عبارة المرور العشوائية في الإعدادات كنص عادي.\nيمكنك تغييره لاحقًا. سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في الإعدادات. هل تريد إزالة عبارة المرور من الإعدادات؟ استخدم عبارة مرور عشوائية - حفظ عبارة المرور في الإعدادات - إعداد كلمة المرور لقاعدة البيانات - تعيين عبارة مرور قاعدة البيانات + احفظ عبارة المرور في الإعدادات + إعداد عبارة المرور لقاعدة البيانات + عيّن عبارة مرور قاعدة البيانات افتح مجلد قاعدة البيانات سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق. - يُخزين عبارة المرور في الإعدادات كنص عادي. - يُرجى الملاحظة: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]> + عبارة المرور مخزنة في الإعدادات كنص عادي. + يُرجى الملاحظة: يتم توصيل مُرحلات الرسائل والملفات عبر وسيط SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]> عَمِّ الملفات المحلية - عَمِّ الملفات والوسائط المخزنة + عمِّ الملفات والوسائط المخزنة تطبيق سطح المكتب الجديد! 6 لغات واجهة جديدة يُعمِّي الملفات المحلية الجديدة (باستثناء مقاطع الفيديو). اكتشاف والانضمام إلى المجموعات العربية والبلغارية والفنلندية والعبرية والتايلاندية والأوكرانية - شكرًا للمستخدمين و Weblate. إنشاء ملف تعريف جديد في تطبيق سطح المكتب. 💻 - - الاتصال بخدمة الدليل (تجريبي)! -\n- إيصالات التسليم (ما يصل إلى 20 عضوا). -\n- أسرع وأكثر استقرارًا. + - الاتصال بخدمة الدليل (تجريبي)!\n- إيصالات التسليم (ما يصل إلى 20 عضوا).\n- أسرع وأكثر استقرارًا. افتح حدث خطأ أثناء إنشاء جهة اتصال للعضو أرسل رسالة مباشرة للاتصال وضع التخفي اصبح أسهل فعّل وضع التخفي عند الاتصال. - أرسل رسالة مباشرة - متصل مباشرةً + أرسل لاتصال + طُلب اتصال جارٍ الاتصال بالفعل! مجموعات أفضل و%d أحداث أخرى @@ -1405,7 +1396,7 @@ %d رسالة محظورة حظر العضو الجوّال متصل - حذف وإشعار جهة الاتصال + احذف وإشعار جهة الاتصال انتهى الاتصال اتصل بسطح المكتب قطع الاتصال @@ -1444,8 +1435,8 @@ مجموعات التخفي %s و%s و%d عضو هذا الجهاز - %1$d من الرسائل يُشرف عليها بواسطة %2$s - إلغاء حظر العضو + %1$d من الرسائل أُشرف عليها بواسطة %2$s + ألغِ حظر العضو %s قُطع اتصاله]]> في انتظار سطح المكتب… انضمام أسرع ورسائل أكثر موثوقية. @@ -1459,7 +1450,7 @@ فشلت إعادة التفاوض على التعمية. غير متوافق! ربط الجوّال - إزالة العضو + أزل العضو إلغاء حظر العضو؟ استخدم من سطح المكتب رمز الجلسة @@ -1477,15 +1468,15 @@ خطأ لقد شاركت مسار ملف غير صالح. أبلغ عن المشكلة لمطوري التطبيق. اسم غير صالح! - لصق عنوان سطح المكتب + ألصق عنوان سطح المكتب %1$s!]]> تحقق من الرمز مع سطح المكتب مسح رمز QR من سطح المكتب - إلغاء الحظر - - إشعار اختياريًا جهات الاتصال المحذوفة. \n- أسماء الملفات التعريفية بمسافات. \n- و اكثر! + ألغِ الحظر + - إشعار اختياريًا جهات الاتصال المحذوفة.\n- أسماء الملفات التعريفية بمسافات.\n- و اكثر! مسار الملف غير صالح لقد طلبت بالفعل الاتصال عبر هذا العنوان! - إظهار وحدة التحكم في نافذة جديدة + أظهر وحدة التحكم في نافذة جديدة المسح من الجوّال تحقق من الاتصالات من فضلك، انتظر حتى يتم تحميل الملف من الجوّال المرتبط @@ -1532,7 +1523,7 @@ ابحث أو ألصِق رابط SimpleX بدء الدردشة؟ توقفت الدردشة. إذا كنت قد استخدمت قاعدة البيانات هذه بالفعل على جهاز آخر، فيجب عليك نقلها مرة أخرى قبل بدء الدردشة. - اعرض الأخطاء الداخلية + أظهر الأخطاء الداخلية خطأ فادح خطأ داخلي يُرجى إبلاغ المطورين بذلك: @@ -1565,7 +1556,7 @@ يحتوي سطح المكتب على رمز دعوة خاطئ سطح المكتب مشغول يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين - العضو السابق %1$s + العضو %1$s وظيفة بطيئة خيارات المطور تغيّر العضو %1$s إلى %2$s @@ -1594,18 +1585,18 @@ أُنشئ في: %s رسالة محفوظة إلغاء حظر العضو للجميع؟ - إلغاء الحظر للجميع + ألغِ الحظر للجميع حدث خطأ أثناء حظر العضو للجميع - حُظر %d رسالة من قبل المشرف + حُظرت %d رسالة من قبل المُدير محظور %s - أُلغيت حظر %s + أُلغيَ حظر %s حظرت %s أُلغيت حظر %s محظور حظر للجميع حظر العضو للجميع؟ - محظور من قبل المشرف - محظور من قبل المشرف + محظور من قبل المُدير + محظور من قبل المُدير الرسالة كبيرة جدًا رسالة الترحيب طويلة جدًا ترحيل قاعدة البيانات قيد التقدم. @@ -1615,16 +1606,16 @@ أنهيّ المكالمة متصفح الويب الافتراضي مطلوب للمكالمات. يُرجى تضبيط المتصفح الافتراضي في النظام، ومشاركة المزيد من المعلومات مع المطورين. حدث خطأ أثناء فتح المتصفح - أرشفة و رفع - يمكن للمشرفين حظر عضو للجميع. + أرشف وأرفع + يمكن للمُدراء حظر عضو للجميع. ترحيل بيانات التطبيق جارِ أرشفة قاعدة البيانات - سيتم تعمية جميع جهات الاتصال والمحادثات والملفات الخاصة بك بشكل آمن ورفعها في أجزاء إلى مُرحلات XFTP التي ضُبطت. + جميع جهات الاتصال، المحادثات والملفات الخاصة بك سيتم تعميتها بأمان ورفعها على شكل أجزاء إلى موجهات XFTP المُعدة. طبّق يُرجى ملاحظة: استخدام نفس قاعدة البيانات على جهازين سيؤدي إلى كسر فك تعمية الرسائل من اتصالاتك، كحماية أمنية.]]> تحذير: سيتم حذف الأرشيف.]]> - التعمية بين الطرفين مع توفير السرية التامة لإعادة التوجيه والرفض واستعادة عمليات الاقتحام.]]> - التعمية بين الطرفين المقاوم للكم مع توفير السرية التامة لإعادة التوجيه والرفض واستعادة عمليات الاقتحام.]]> + التعمية بين الطرفين مع توفير السرية التامة لإعادة التوجيه والرفض واستعادة عمليات الاقتحام.]]> + التعمية بين الطرفين المقاوم للكم مع توفير السرية التامة لإعادة التوجيه والرفض واستعادة عمليات الاقتحام.]]> هذه الدردشة محمية بالتعمية بين الطرفين. هذه الدردشة محمية بالتعمية بين الطرفين المقاوم للكم. افتح شاشة الترحيل @@ -1701,7 +1692,7 @@ رحّل من جهاز آخر على الجهاز الجديد و امسح رمز QR ضوئيًا.]]> تحذير: بدء الدردشة على أجهزة متعددة غير مدعوم وسيؤدي إلى فشل تسليم الرسائل إتصال شبكة - محوّلة + مُحوّلة خلوي إيثرنت سلكية لا إتصال شبكة @@ -1711,15 +1702,15 @@ الرسائل الصوتية غير مسموح بها روابط SimpleX السماح بإرسال روابط SimpleX. - منع إرسال روابط SimpleX + امنع إرسال روابط SimpleX كل الأعضاء يمكن للأعضاء إرسال روابط SimpleX. - روابط SimpleX محظورة. - المشرفين + روابط SimpleX ممنوعة. + المُدراء مفعّل لـ المالكون الملفات والوسائط غير مسموح بها - محوّلة + مُحوّلة حوّل الرسالة… تلقي التزامن لا يستطيع المُستلم/ون معرفة مَن أرسل هذه الرسالة. @@ -1729,7 +1720,7 @@ السماعة سماعة الأذن سماعات الرأس - محوّلة مِن + مُحوّلة مِن حُفظت نزّل حوّل @@ -1742,19 +1733,17 @@ إدارة الشبكة اتصال شبكة أكثر موثوقية. صور ملف التعريف - شكل الصور التعريفية + شكّل الصور التعريفية واجهة المستخدم الليتوانية مربع أو دائرة أو أي شيء بينهما. عنوان الخادم غير متوافق مع إعدادات الشبكة. إصدار الخادم غير متوافق مع إعدادات الشبكة. مفتاح خاطئ أو اتصال غير معروف - على الأرجح حُذف هذا الاتصال. - تم تجاوز السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا. + تجاوزت السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا. خطأ في خادم الوجهة: %1$s خطأ: %1$s - خادم التحويل: %1$s -\nخطأ في الخادم الوجهة: %2$s - خادم التحويل: %1$s -\nخطأ: %2$s + خادم التحويل: %1$s\nخطأ في خادم الوجهة: %2$s + خادم التحويل: %1$s\nخطأ: %2$s تحذير تسليم الرسالة مشكلات الشبكة - انتهت صلاحية الرسالة بعد عِدة محاولات لإرسالها. نعم @@ -1779,7 +1768,7 @@ استخدم دائمًا التوجيه الخاص. الملفات مطلقًا - سيطلب التطبيق تأكيد التنزيلات من خوادم ملفات غير معروفة (باستثناء .onion أو عند تفعيل وكيل SOCKS). + سيطلب التطبيق تأكيد التنزيلات من خوادم ملفات غير معروفة (باستثناء .onion أو عند تفعيل وسيط SOCKS). أرسل الرسائل مباشرة عندما يكون عنوان IP محميًا ولا يدعم الخادم الوجهة لديك التوجيه الخاص. خوادم غير معروفة خوادم غير معروفة! @@ -1839,36 +1828,35 @@ خطأ في الملف خطأ في الملف مؤقت حالة الرسالة - لم يتم العثور على الملف - على الأرجح حُذف الملف أو إلغاؤه. + لم يُعثر على الملف - على الأرجح حُذف الملف أو أُلغيَ. حالة الملف حالة الملف: %s حالة الرسالة: %s خطأ في النسخ - تم استخدام هذا الرابط مع جهاز محمول آخر، يُرجى إنشاء رابط جديد على سطح المكتب. + استُخدم هذا الرابط مع جوّال آخر، يُرجى إنشاء رابط جديد على سطح المكتب. يُرجى التحقق من اتصال الهاتف المحمول وسطح المكتب بنفس الشبكة المحلية، وأن جدار حماية سطح المكتب يسمح بالاتصال. \nيُرجى مشاركة أي مشاكل أُخرى مع المطورين. لا يمكن إرسال الرسالة تفضيلات الدردشة المحدّدة تحظر هذه الرسالة. التفاصيل - بدءًا من %s. -\nجميع البيانات خاصة بجهازك. + بدءًا من %s.\nجميع البيانات خاصة على جهازك. أرسلت الإجمالي الحجم الملفات المرفوعة - يُرجى المحاولة لاحقا. + يُرجى المحاولة لاحقًا. خطأ في التوجيه الخاص عنوان الخادم غير متوافق مع إعدادات الشبكة: %1$s. إصدار الخادم غير متوافق مع تطبيقك: %1$s. العضو غير نشط - رسالة محوّلة - لا يوجد اتصال مباشر حتى الآن، يتم تحويل من قِبل المشرف. + رسالة مُحوّلة + لا يوجد اتصال مباشر حتى الآن، الرسالة مُحوّلة بواسطة المُدير. امسح / ألصِق الرابط خوادم SMP المهيأة خوادم SMP أخرى خوادم XFTP المهيأة خوادم XFTP أخرى أظهِر النسبة المئوية - مُعطّل + مُعطَّل مستقرّ يتوفر تحديث: %s التمس التحديثات @@ -1876,13 +1864,13 @@ ثُبّت بنجاح افتح مكان الملف يُرجى إعادة تشغيل التطبيق. - تذكر لاحقا + تذكر لاحقًا تخطي هذه النسخة أُلغيت تنزيل التحديث - مُعطّل + مُعطَّل غير نشط معلومات الخوادم - عرض المعلومات ل + يُظهر معلومات ل الأخطاء الرسائل المُرسلة الإجمالي @@ -1916,9 +1904,9 @@ أخرى موّكل مؤمن - أرسِل الأخطاء + أرسل الأخطاء أُرسلت مباشرةً - مُرسَل عبر الوكيل + مُرسَل عبر الوسيط مشترك أخطاء الاشتراك رفع الأخطاء @@ -1960,25 +1948,25 @@ أعِد توصيل الخوادم؟ عنوان الخادم الإحصائيات - تم تجاهل الاشتراكات + تجاهلت الاشتراكات جلسات النقل لكي يتم إعلامك بالإصدارات الجديدة، شغّل الفحص الدوري للإصدارات المستقرة أو التجريبية. أنت غير متصل بهذه الخوادم. يتم استخدام التوجيه الخاص لتسليم الرسائل إليهم. قرّب - حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقا. + حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقًا. عنوان خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. عنوان خادم الوجهة %1$s غير متوافق مع إعدادات خادم التحويل %2$s. إصدار الخادم الوجهة %1$s غير متوافق مع خادم التحويل %2$s. - فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقا. + فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقًا. إصدار خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. مطفي قوي تمويه الوسائط متوسط ناعم - المكالمة + مكالمة اتصل - الرسالة + مراسلة افتح بحث الإعدادات @@ -2009,7 +1997,7 @@ يجب عليك السماح لجهات اتصالك بالاتصال حتى تتمكن من الاتصال بها. يُرجى الطلب من جهة اتصالك تفعيل المكالمات. حذف %d رسائل الأعضاء؟ - سيتم وضع علامة على الرسائل للحذف. سيتمكن المستلم/(المستلمون) من الكشف عن هذه الرسائل. + سيتم وضع علامة على الرسائل للحذف. سيتمكن المُستلم/(المُستلمون) من الكشف عن هذه الرسائل. حدد سيتم حذف الرسائل لجميع الأعضاء. سيتم وضع علامة على الرسائل على أنها تحت الإشراف لجميع الأعضاء. @@ -2019,7 +2007,7 @@ خوادم الوسائط والملفات خوادم الرسائل متابعة - وكيل SOCKS + وسيط SOCKS يمكنك ترحيل قاعدة البيانات المُصدرة. يمكنك حفظ الأرشيف المُصدر. حالة الاتصال والخوادم. @@ -2049,41 +2037,40 @@ صفّر كافة التلميحات يُرجى التأكد من أن رابط SimpleX صحيح. الرابط غير صالح - %1$d خطأ في الملف: -\n%2$s + %1$d خطأ في الملف:\n%2$s فشل تنزيل %1$d ملف/ات. لم يتم تنزيل %1$d ملف/ات. نزّل شارك ملف التعريف - استخدم بيانات اعتماد الوكيل المختلفة لكل اتصال. + استخدم بيانات اعتماد الوسيط المختلفة لكل اتصال. اسم المستخدم - قد يتم إرسال بيانات الاعتماد الخاصة بك غير مُعمَّاة. - خطأ في حفظ الوكيل + قد يتم إرسال بيانات اعتمادك غير مُعمَّاة. + خطأ في حفظ الوسيط إزالة الأرشيف؟ وضع النظام سيتم إزالة أرشيف قاعدة البيانات المرفوعة نهائيًا من الخوادم. - استخدم بيانات اعتماد الوكيل المختلفة لكل ملف تعريف. + استخدم بيانات اعتماد الوسيط المختلفة لكل ملف تعريف. استخدم بيانات اعتماد عشوائية قاعدة بيانات الدردشة حُذف %1$d ملف/ات. - لا يزال يتم تنزيل %1$d ملفًا. - لا تستخدم بيانات الاعتماد مع الوكيل. + لا يزال يتم تنزيل %1$d ملف/ات. + لا تستخدم بيانات الاعتماد مع الوسيط. خطأ في تحويل الرسائل خطأ في تبديل ملف التعريف حدد ملف تعريف الدردشة - لقد تم نقل اتصالك إلى %s ولكن حدث خطأ غير متوقع أثناء إعادة توجيهك إلى ملف التعريف. + نُقل اتصالك إلى %s ولكن حدث خطأ عند تبديل ملف التعريف. تحويل %1$s رسالة؟ لم يحوّل %1$s من الرسائل جارِ تحويل %1$s رسالة حوّل الرسائل… تحويل الرسائل بدون ملفات؟ جارِ حفظ %1$s رسالة - تأكد من صحة تضبيط الوكيل. + تأكد من صحة تضبيط الوسيط. %1$d خطأ في ملف آخر. حُذفت الرسائل بعد تحديدها. لا يوجد شيء لتحويله! كلمة المرور - استيثاق الوكيل + استيثاق الوسيط سيتم حذف الرسائل - لا يمكن التراجع عن هذا! الصوت مكتوم حدث خطأ أثناء تهيئة WebView. تأكد من تثبيت WebView وأن بنيته المدعومة هي arm64.\nالخطأ: %s @@ -2128,7 +2115,7 @@ يمكنك تضبيط المُشغلين في إعدادات الشبكة والخوادم. حدّث تابع - قُبل الشروط + الشروط المتفق عليها راجع الشروط الخوادم المُعدة مسبقًا سيتم قبول الشروط تلقائيًا للمُشغلين المفعّلين في: %s. @@ -2154,7 +2141,7 @@ %s.]]> أُضيفت خوادم الوسائط والملفات الشروط المفتوحة - الخوادم الخاصة بالملفات الجديدة لملف الدردشة الحالي الخاص بك + الخوادم الخاصة بالملفات الجديدة لملف دردشتك الحالي لإرسال خطأ في إضافة الخادم خطأ في تحديث الخادم @@ -2165,7 +2152,7 @@ أشرطة أدوات التطبيق تمويه الشفافية - فعّل flux + فعّل flux في إعدادات الشبكة والخوادم لتحسين خصوصية البيانات الوصفية. اللامركزية الشبكية المُشغل المُعد مسبقًا الثاني في التطبيق! لتحسين خصوصية البيانات الوصفية. @@ -2180,7 +2167,7 @@ لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط: خطأ في قبول الشروط خطأ في حفظ الخوادم - على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسوف يقوم تطبيقك بتسليمها عبر خادم Flux. + على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسيقوم تطبيقك بتسليمها عبر خادم Flux. لا يوجد خوادم لتوجيه الرسائل الخاصة. لا يوجد خوادم رسائل. لا يوجد خوادم لاستقبال الملفات. @@ -2194,7 +2181,7 @@ شارك عنوان SimpleX على وسائل التواصل الاجتماعي. عنوان SimpleX والروابط لمرة واحدة آمنة للمشاركة عبر أي برنامج مُراسلة. انقر فوق أنشئ عنوان SimpleX في القائمة لإنشائه لاحقًا. - حُذفت هذه الرسالة أو لم يتم استلامها بعد. + حُذفت هذه الرسالة أو لم تُستلم بعد. استخدم للرسائل يحمي التطبيق خصوصيتك من خلال استخدام مُشغلين مختلفين في كل محادثة. %s.]]> @@ -2206,7 +2193,7 @@ أضف أعضاء الفريق أضف أصدقاء أضف أعضاء فريقك إلى المحادثات. - يُحظر إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة. + يُمنع إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة. أجهزة Xiaomi: يُرجى تفعيل التشغيل التلقائي (Autostart) في إعدادات النظام لكي تعمل الإشعارات.]]> مُعمَّاة بين الطرفين، مع أمان ما بعد الكم في الرسائل المباشرة.]]> تحقق من الرسائل كل 10 دقائق @@ -2222,7 +2209,7 @@ أو استورد ملف الأرشيف لا توجد خدمة خلفية الإشعارات والبطارية - يمكن فقط لأصحاب الدردشة تغيير التفضيلات. + فقط مالكي الدردشة يمكنهم تغيير التفضيلات. الخصوصية لعملائك. الجوالات عن بُعد ادعُ للدردشة @@ -2236,9 +2223,303 @@ طلبت الاتصال يُرجى تقليل حجم الرسالة أو إزالة الوسائط ثم إرسالها مرة أخرى. يمكنك نسخ الرسالة وتقليل حجمها لإرسالها. - عندما يتم تفعيل أكثر من مُشغل واحد، لن يكون لدى أي منهم بيانات تعريفية لمعرفة من يتواصل مع من. + عندما يتم تفعيل أكثر من مُشغل واحد، لن يكون لدى أي منهم بيانات تعريفية لمعرفة مَن يتواصل مع مَن. سيتم تغيير الدور إلى %s. وسيتم إشعار الجميع في الدردشة. سيتم إرسال ملف تعريفك للدردشة إلى أعضاء الدردشة - سوف تتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة. + ستتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة. عن المُشغلين - \ No newline at end of file + توصلت 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 للخوادم المُعدة مسبقًا فقط. + إيقاف التشغيل + الخوادم المُعدة مسبقًا + جميع الخوادم + خطأ في قبول العضو + دردشة واحدة مع عضو + %d دردشة/ات + %d دردشات مع الأعضاء + %d رسائل + أُرسِل البلاغ للمشرفين + يمكنك عرض تقاريرك في \"دردش مع المُدراء\". + اقبل كمراقب + حُذفت جهة الاتصال + عُطِّلت جهة الاتصال + جهة الاتصال غير جاهزة + لا يمكنك إرسال الرسائل! + غير متزامن + قبلت %1$s + قبِلك + لقد قبلت هذا العضو. + الرجاء الانتظار ريثما يراجع مشرفو المجموعة طلبك للانضمام إليها. + دردش مع المُدراء + راجع الأعضاء + غير مفعّل + اقبل + عضو جديد يريد الانضمام للمجموعة. + الكل + دخول العضو + راجع الأعضاء قبل القبول (الطرق). + خطأ في حذف الدردشة + لا يمكن إرسال الرسائل + رُفض طلب الانضمام + غادرت + حُذفت المجموعة + أُزيل من المجموعة + حفظ إعدادات القبول؟ + قيد المراجعة + مراجعة + دردش مع عضو + حدّد دخول العضو + دردش مع المُدراء + احذف الدردشة + حذف الدردشة مع العضو؟ + ارفض + اقبل كعضو + اقبل العضو + سينضم العضو إلى المجموعة، هل تقبل العضو؟ + رفض العضو؟ + دردشات مع الأعضاء + رُجع من قِبل المُدراء + لا دردشات مع الأعضاء + العضو لديه نُسخة قديمة + رقِّ العنوان + اقبل طلب جهة الاتصال + أضف رسالة + التعمية بين الطرفين.]]> + إلا بعد قبول طلبك.]]> + اتصل + يجب أن يقبل جهة الاتصال… + خطأ في تغيير مستخدم جهة الاتصال + خطأ في فتح الدردشة + خطأ في فتح المجموعة + خطأ في رفض طلب جهة الاتصال + انضم للمجموعة + افتح الدردشة + افتح دردشة جديدة + افتح مجموعة جديدة + افتح للقبول + افتح لاتصال + افتح لانضمام + سيكون العنوان قصيراً، وسيتم مشاركة ملف تعريفك عبر العنوان. + ارفض طلب جهة الاتصال + أُرسل الطلب + إرسال طلب جهة اتصال؟ + أرسل طلب + أرسل طلب دون رسالة + أُرسل إلى جهة اتصالك بعد الاتصال. + ترقية رابط المجموعة؟ + رقِّ + ترقية العنوان؟ + لن يتم إشعار المرسل. + رسالة الترحيب + ملف تعريفك + لا يمكن تغيير ملف التعريف + لاستخدام ملف تعريف آخر بعد محاولة الاتصال، احذف الدردشة واستخدم الرابط مرة أخرى. + الدردشة مع الالمُدراء + الدردشة مع الأعضاء قبل انضمامهم. + اتصل بشكل أسرع! 🚀 + تقليل حركة البيانات على شبكات الجوّال. + راسل فورًا بمجرد النقر على \"اتصل\". + دور جديد للمجموعة: مشرف + لا توجد جلسة توجيه خاصة + انتهت مهلة التوجيه الخاص + انتهت مهلة خلفية البروتوكول + يزيل الرسائل ويحظر الأعضاء. + مراجعة أعضاء المجموعة + أرسل ملاحظاتك الخاصة إلى المجموعات. + مهلة اتصال TCP في الخلفية + جارِ تحميل ملف التعريف… + السيرة الذاتية: + وصف قصير: + نبذتك: + النبذة كبيرة جدًا + الوصف كبير جدًا + اقبل طلب جهة الاتصال + اتصال عمل + مجموعة + انقر على \"اتصل\" للدردشة + انقر على \"اتصل\" لإرسال الطلب + انقر على \"انضم للمجموعة\" + جهة عمل اتصالك + جهة اتصالك + مجموعتك + وقت الاختفاء يتم تعيينه للجهات الاتصال الجديدة فقط. + استخدم ملف تعريف متخفي + 4 لغات واجهة جديدة + الكتالانية والإندونيسية والرومانية والفيتنامية - بفضل مستخدمينا! + أنشئ عنوانك + فعّل الرسائل ذاتية الاختفاء افتراضيًا. + أبقِ محادثاتك نظيفة + عيّن نبذة عن الملف التعريف ورسالة الترحيب. + شارك عنوانك + عنوان SimpleX قصير + حدّث عنوانك + رحب بجهات اتصالك 👋 + شارك العنوان القديم + شارك الرابط القديم + سيكون الرابط قصيراً، وسيتم مشاركة الملف التعريفي للمجموعة عبر الرابط. + رقِّ رابط المجموعة + طلبات الاتصال من المجموعات + حُذف العضو - لا يمكن قبول الطلب + طُلب اتصال من المجموعة %1$s + هذا الإعداد لملف تعريفك الحالي + اسمح بالملفات والوسائط فقط إذا سمح جهة اتصالك بذلك. + اسمح لجهات اتصالك بإرسال الملفات والوسائط. + بوت + يمكنك أنت وجهة اتصالك إرسال الملفات والوسائط. + يُمنع إرسال الملفات والوسائط في هذه الدردشة. + يمكنك أنت فقط إرسال الملفات والوسائط. + يمكن لجهة اتصالك فقط إرسال الملفات والوسائط. + افتح لاستخدام البوت + امنع إرسال الملفات والوسائط. + انقر على \"اتصل\" لاستخدام البوت + لإرسال الأوامر يجب أن تكون متصلاً. + خيارات مهملة + افتح الرابط النظيف + افتح الرابط الكامل + أزل تتبع الروابط + رابط مُرحل SimpleX + خطأ في وضع علامة \"مقروءة\" + البصمة في عنوان الخادم الوجهة لا تتطابق مع الشهادة: %1$s. + البصمة في عنوان خادم التحويل لا تتطابق مع الشهادة: %1$s. + البصمة في عنوان الخادم لا تتطابق مع الشهادة: %1$s. + لا اشتراك + أنت غير متصل بالخادم المستخدم لاستقبال الرسائل من هذا الاتصال (لا يوجد اشتراك). + 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 ded58ac9b3..371f0e076f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -9,10 +9,15 @@ Join group? Use current profile Use new incognito profile + Use incognito profile Your profile will be sent to the contact that you received this link from. You will connect to all group members. Connect Connect incognito + Open chat + Open new chat + Open group + Open new group Invalid link Please check that SimpleX link is correct. @@ -27,9 +32,11 @@ connected error connecting - You are connected to the server used to receive messages from this contact. - Trying to connect to the server used to receive messages from this contact (error: %1$s). - Trying to connect to the server used to receive messages from this contact. + no subscription + You are connected to the server used to receive messages from this connection. + Trying to connect to the server used to receive messages from this connection. + Error connecting to the server used to receive messages from this connection: %1$s. + You are not connected to the server used to receive messages from this connection (no subscription). deleted @@ -63,6 +70,7 @@ Decryption error Encryption re-negotiation error + end-to-end encryption.]]> end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery.]]> quantum resistant e2e encryption with perfect forward secrecy, repudiation and break-in recovery.]]> This chat is protected by end-to-end encryption. @@ -91,6 +99,8 @@ SimpleX contact address SimpleX one-time invitation SimpleX group link + SimpleX channel link + SimpleX relay link via %1$s SimpleX links Description @@ -138,13 +148,18 @@ Connection timeout Connection error + Fingerprint in server address does not match certificate: %1$s. 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 timeout Private routing error + No private routing session + Fingerprint in forwarding server address does not match certificate: %1$s. 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. + Fingerprint in destination server address does not match certificate: %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. @@ -156,6 +171,9 @@ Error loading details Error adding member(s) Error joining group + Error accepting member + Error marking read + Error deleting chat Cannot receive file Sender cancelled file transfer. Unknown servers! @@ -168,6 +186,8 @@ 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 @@ -175,6 +195,7 @@ Undelivered messages The connection reached the limit of undelivered messages, your contact may be offline. Error accepting contact request + Error rejecting contact request Sender may have deleted the connection request. Error deleting contact Error deleting group @@ -185,9 +206,9 @@ Error aborting address change Error synchronizing connection Test failed at step %s. - Server requires authorization to create queues, check password - Server requires authorization to upload, check password - Possibly, certificate fingerprint in server address is incorrect + Server requires authorization to create queues, check password. + Server requires authorization to upload, check password. + Fingerprint in server address does not match certificate. Error setting address Error Connect @@ -207,6 +228,9 @@ Error updating chat list Error creating chat list Error loading chat lists + Error opening chat + Error opening group + Error changing profile Instant notifications @@ -312,7 +336,12 @@ 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 @@ -341,6 +370,7 @@ Search Archive Archive report + Archive reports Delete report Sent message Received message @@ -409,9 +439,11 @@ Chats Settings connecting… - send direct message - you are invited to group - join as %s + send to connect + Open to join + You are invited to group + Join as %s + rejected connecting… Tap to start a new chat Chat with the developers @@ -423,6 +455,10 @@ No chats No chats found Tap to Connect + Open to connect + Open to use bot + Open to accept + contact should accept… Connect with %1$s? Search or paste SimpleX link Tap Create SimpleX address in the menu to create it later. @@ -447,12 +483,29 @@ Businesses Notes Reports + Report: %s All Add list 1 report %d reports Member reports - Archived member reports + %d messages + %d chats with members + 1 chat with a member + %d chat(s) + + + Tap Connect to chat + Tap Connect to send request + Tap Connect to use bot + Accept contact request + Your contact + Bot + Tap Join group + Your group + Group + Business connection + Your business contact Share message… @@ -475,9 +528,6 @@ Decoding error 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 @@ -496,6 +546,33 @@ Report violation: only group moderators will see it. Report content: only group moderators will see it. Report other: only group moderators will see it. + Report sent to moderators + You can view your reports in Chat with admins. + Join group + Add message + Connect + Send contact request? + only after your request is accepted.]]> + Send request without message + Send request + + You can\'t send messages! + contact not ready + request is sent + 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 + you are observer + reviewed by admins + member has old version + To send commands you must be connected. Image @@ -662,6 +739,12 @@ Accept Accept incognito Reject + Accept contact request + Reject contact request + The sender will NOT be notified. + + + Member is deleted - can\'t accept request Clear chat? @@ -679,9 +762,11 @@ Mute + Mute all Unmute Favorite Unfavorite + Unread mentions Create list @@ -786,10 +871,12 @@ 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. + Your connection was moved to %s but an error happened when switchig profile. Or scan QR code Keep unused invitation? You can view invitation link again in connection details. @@ -800,6 +887,7 @@ Paste the link you received The text you pasted is not a SimpleX link. Tap to paste link + Loading profile… Invalid QR code The code you scanned is not a SimpleX link QR code. @@ -808,6 +896,11 @@ No filtered contacts Your contacts + + Your profile + Can\'t change profile + To use another profile after connection attempt, delete the chat and use the link again. + Scan code Incorrect security code! @@ -946,6 +1039,13 @@ 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 INTERFACE COLORS @@ -979,6 +1079,7 @@ Enable logs Database IDs and Transport isolation option. Developer options + Deprecated options Show internal errors Show slow API calls Shutdown? @@ -1001,9 +1102,11 @@ Stop sharing address? Stop sharing Auto-accept + Sent to your contact after connection. + Welcome message Enter welcome message… (optional) Save settings? - Save auto-accept settings + Save SimpleX address settings Delete address Invite friends Let\'s talk in SimpleX Chat @@ -1015,6 +1118,15 @@ Address settings Business address Add your team members to the conversations. + Upgrade address + Upgrade address? + The address will be short, and your profile will be shared via the address. + Upgrade + Upgrade group link + Upgrade group link? + The link will be short, and group profile will be shared via the link. + Share old address + Share old link Continue @@ -1026,10 +1138,13 @@ Profile name: Full name: + Bio: + Bio too large Your current profile Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. Edit image Delete image + Save admission settings? Save preferences? Save and notify contact Save and notify contacts @@ -1054,6 +1169,7 @@ The profile is only shared with your contacts. Display name cannot contain whitespace. Enter your name: + Your bio: Create Create profile Create @@ -1150,6 +1266,11 @@ 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. @@ -1255,6 +1376,7 @@ The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled). Without Tor or VPN, your IP address will be visible to file servers. Send link previews + Remove link tracking Show last messages Message draft App data backup @@ -1289,6 +1411,7 @@ An empty chat profile with the provided name is created, and the app opens as usual. If you enter this passcode when opening the app, all app data will be irreversibly removed! Set passcode + This setting is for your current profile These settings are for your current profile They can be overridden in contact and group settings. Contacts @@ -1320,6 +1443,8 @@ Ask Open web link? Open link + Open full link + Open clean link YOU @@ -1332,6 +1457,7 @@ CHATS FILES SEND DELIVERY RECEIPTS TO + CONTACT REQUESTS FROM GROUPS Restart Shutdown Developer tools @@ -1458,6 +1584,7 @@ Wrong database passphrase + Error reading database passphrase Encrypted database Database error Keychain error @@ -1479,7 +1606,9 @@ 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 @@ -1537,10 +1666,13 @@ deleted contact + requested connection from group %1$s invited %1$s connected + accepted %1$s + accepted you left changed role of %s to %s blocked %s @@ -1551,7 +1683,8 @@ deleted group updated group profile invited via your group link - connected directly + requested connection + New member wants to join the group. you changed role of %s to %s you changed role for yourself to %s you blocked %s @@ -1559,6 +1692,8 @@ you removed %1$s you left group profile updated + you accepted this member + Please wait for group moderators to review your request to join the group. %s connected %s and %s connected @@ -1610,11 +1745,16 @@ owner + rejected removed left group deleted unknown status invited + pending approval + pending + pending review + review connecting (introduced) connecting (introduction invitation) connecting (accepted) @@ -1626,7 +1766,7 @@ connecting unknown - Past member %1$s + Member %1$s No contacts to add @@ -1685,6 +1825,7 @@ Receipts are disabled This group has over %1$d members, delivery receipts are not sent. Invite + Chat with admins FOR CONSOLE @@ -1719,25 +1860,37 @@ Remove member? + Remove members? + Delete member messages? Remove member - + Delete member messages + Chat with 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! + Member messages will be deleted - this cannot be undone! Remove + Remove and delete messages + Delete messages 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 @@ -1805,6 +1958,8 @@ Fully decentralized – visible only to members. Enter group name: Group full name: + Short description: + Description too large Your chat profile will be sent to group members Your chat profile will be sent to chat members Create group @@ -1830,7 +1985,6 @@ Website Conditions accepted on: %s. Conditions will be accepted on: %s. - Operator Use servers Use %s Current conditions text couldn\'t be loaded, you can review conditions via this link: @@ -1843,6 +1997,7 @@ View conditions Accept conditions Conditions of use + Updated conditions %s, accept conditions of use.]]> Use for messages To receive @@ -1870,7 +2025,9 @@ Reset to defaults sec TCP connection timeout + TCP connection bg timeout Protocol timeout + Protocol background timeout Protocol timeout per KB Receiving concurrency PING interval @@ -1991,6 +2148,7 @@ Contact preferences Group preferences Set group preferences + Set member admission Your preferences Disappearing messages Direct messages @@ -2011,6 +2169,7 @@ Set 1 day Allow your contacts to send disappearing messages. Allow disappearing messages only if your contact allows them. + Time to disappear is set only for new contacts. Prohibit sending disappearing messages. Allow your contacts to irreversibly delete sent messages. (24 hours) Allow irreversible message deletion only if your contact allows it to you. (24 hours) @@ -2018,6 +2177,9 @@ Allow your contacts to send voice messages. Allow voice messages only if your contact allows them. Prohibit sending voice messages. + Allow your contacts to send files and media. + Allow files and media only if your contact allows them. + Prohibit sending files and media. Allow your contacts adding message reactions. Allow message reactions only if your contact allows them. Prohibit message reactions. @@ -2036,6 +2198,10 @@ Only you can send voice messages. Only your contact can send voice messages. Voice messages are prohibited in this chat. + Both you and your contact can send files and media. + Only you can send files and media. + Only your contact can send files and media. + Files and media are prohibited in this chat. Both you and your contact can add message reactions. Only you can add message reactions. Only your contact can add message reactions. @@ -2060,6 +2226,8 @@ Prohibit sending SimpleX links Send up to 100 last messages to new members. Do not send history to new members. + 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. @@ -2078,6 +2246,8 @@ 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 @@ -2099,10 +2269,34 @@ offered %s: %2s cancelled %s all members + moderators admins owners Enabled for + + Member admission + Review members + Review members before admitting ("knocking"). + off + all + + + Chats with members + No chats with members + Delete chat + Delete chat with member? + + + Chat with admins + Reject + Reject member? + Accept + Accept member + Member will join the group, accept member? + Accept as member + Accept as observer + What\'s new New in %s @@ -2261,12 +2455,43 @@ Delete or moderate up to 200 messages. Network decentralization The second preset operator in the app! - Enable flux + 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. + Connect faster! 🚀 + Message instantly once you tap Connect. + Review group members + Chat with members before they join. + Chat with admins + Send your private feedback to groups. + New group role: Moderator + Removes messages and blocks members. + Less traffic on mobile networks. + Welcome your contacts 👋 + Set profile bio and welcome message. + Keep your chats clean + Enable disappearing messages by default. + Short SimpleX address + Create your address + Update your address + Share your address + 4 new interface languages + Catalan, Indonesian, Romanian and Vietnamese - thanks to our users! View updated conditions @@ -2382,7 +2607,6 @@ You have already requested connection via this address! Join your group? %1$s!]]> - Open group Repeat join request? Group already exists! Chat already exists! @@ -2488,7 +2712,7 @@ Messages sent Messages received Details - Starting from %s.\nAll data is kept private on your device.. + Starting from %s.\nAll data is kept private on your device. Message reception Active connections Pending @@ -2553,4 +2777,7 @@ 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 c51b33e456..03a8bcfdc5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -57,7 +57,7 @@ Спри визуализацията на файла променена е вашата ролята на %s Спри визуализацията на изображението - присъединяване като %s + Присъединяване като %s Показване само на контакт Не може да се осъществи достъп до Keystore, за да се запази паролата на базата данни Файлът не може да бъде получен @@ -67,7 +67,7 @@ ново съобщение Визуализация Показване на контакт и съобщение - вие сте поканени в групата + Вие сте поканени в групата Показване на визуализация Услуга за известията Позволи @@ -220,11 +220,11 @@ Грешка при свързване (AUTH) свързване… Промени адреса за получаване - Базата данни на чата е изтрита + Базата данни е изтрита Чатът работи Чатът е спрян - БАЗА ДАННИ НА ЧАТА - Базата данни на чат е импортирана + БАЗА ДАННИ + Базата данни е импортирана Потвърди новата парола… Потвърди актуализаациите на базата данни свързан @@ -338,7 +338,7 @@ Понижаване на версията на базата данни Актуализация на базата данни версията на базата данни е по-нова от приложението, но няма миграция надолу за: %s - групата изтрита + групата е изтрита Контактът е проверен създател Създай групов линк @@ -406,7 +406,7 @@ Идентификатори в базата данни и опция за изолация на транспорта. Изтрий адрес Изтрий адрес\? - ЦВЕТОВЕ НА ТЕМАТА + ЦВЕТОВЕ НА ИНТЕРФЕЙСА Създай Създай профил Изтрий изображение @@ -430,7 +430,7 @@ Избери файл Ако сте получили линк за покана за SimpleX Chat, можете да го отворите във вашия браузър: Сканирай QR код.]]> - Изтрий линк за предстоящата връзка? + Изтрий линк за чакаща връзка? Електронна поща Сподели еднократен линк Линк за еднократна покана @@ -440,7 +440,7 @@ Изтрий сървър Не създавай адрес Име на профила: - двуслойно криптиране от край до край.]]> + Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения. Деактивирай потвърждениeто\? Активиране (запазване на промените) Активирай потвърждениeто\? @@ -491,7 +491,7 @@ Изтрий съобщенията Грешка при криптиране на базата данни Активиране на автоматично изтриване на съобщения\? - Вашата чат база данни не е криптирана - задайте парола, за да я защитите. + Вашата база данни не е криптирана - задайте парола, за да я защитите. Криптиране на база данни\? покана за група %1$s Изпратихте покана за групата @@ -567,13 +567,13 @@ Криптирана база данни Активирай TCP keep-alive Изчезващите съобщения са забранени в този чат. - Изчезващите съобщения са забранени в тази група. + Изчезващите съобщения са забранени. Изчезващи съобщения Въведи съобщение при посрещане…(незадължително) Съобщение при посрещане Грешка при свързване със сървъра С незадължително съобщение при посрещане. - Грешка при изтриване на чат базата данни + Грешка при изтриване на базата данни Грешка при промяна на настройката Грешка при създаване на групов линк Грешка при изтриване на групов линк @@ -592,15 +592,15 @@ Грешка при изтриване на контакт Грешка при изтриване на заявка за контакт Грешка при изтриване на група - Грешка при изтриване на предстоящата контактна връзка + Грешка при изтриване на чакащата контактна връзка Грешка при запазване на ICE сървърите - Предстояща връзка със сървъра + Чакаща връзка със сървъра Вашият контакт трябва да бъде онлайн, за да осъществите връзката. \nМожете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк). - Грешка при експортиране на чат базата данни + Грешка при експортиране на базата данни Грешка: %s Грешка при запазване на файл - Грешка при импортиране на чат базата данни + Грешка при импортиране на базата данни Грешка при отстраняване на член Грешка при запазване на профила на групата Грешка при зареждане на SMP сървъри @@ -641,14 +641,14 @@ Файлът ще бъде получен, когато вашият контакт завърши качването му. Файлът ще бъде получен, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно! Филтрирайте непрочетените и любимите чатове. - Членовете на групата могат да изпращат лични съобщения. + Членовете могат да изпращат лични съобщения. помощ ПОМОЩ Здравей, \nСвържи се с мен през SimpleX Chat: %s - Членовете на групата могат да добавят реакции към съобщенията. - Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) - Членовете на групата могат да изпращат гласови съобщения. + Членовете могат да добавят реакции към съобщенията. + Членовете могат необратимо да изтриват изпратените съобщения. (24 часа) + Членовете могат да изпращат гласови съобщения. Дори когато е деактивиран в разговора. Бързо и без чакане, докато подателят е онлайн! Френски интерфейс @@ -668,8 +668,8 @@ Груповият профил се съхранява на устройствата на членовете, а не на сървърите. Скрий Забрани изпращането на изчезващи съобщения. - Файловете и медията са забранени в тази група. - Членовете на групата могат да изпращат файлове и медия. + Файловете и медията са забранени. + Членовете могат да изпращат файлове и медия. Скрити чат профили Допълнително намален разход на батерията Групово модериране @@ -693,7 +693,7 @@ Само вие можете да изпращате изчезващи съобщения. Само вашият контакт може да изпраща изчезващи съобщения. Забрани изпращането на изчезващи съобщения. - Членовете на групата могат да изпращат изчезващи съобщения. + Членовете могат да изпращат изчезващи съобщения. Невалиден QR код Невалиден линк! Неправилен код за сигурност! @@ -705,7 +705,7 @@ Покани приятели Скрий профила Как се използва форматирането - Може да се промени по-късно през настройките. + Как се отразява на батерията Незабавно Режим инкогнито Покани членове @@ -716,14 +716,14 @@ Покажи профила Как се използва курсив - Импортиране на чат база данни\? + Импортиране на база данни? Ако изберете да отхвърлите, подателят НЯМА да бъде уведомен. Изображение Изображението ще бъде получено, когато вашият контакт завърши качването му. Изображението е запазено в Галерия Изображението е изпратено Изображението ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно! - Необратимото изтриване на съобщения е забранено в тази група. + Необратимото изтриване на съобщения е забранено. Подобрена конфигурация на сървъра Това може да се случи, когато: \n1. Времето за пазене на съобщенията е изтекло - в изпращащия клиент е 2 дена а на сървъра е 30. @@ -752,7 +752,7 @@ невалиден формат на съобщението Невалиден линк за връзка Скриване на контакт и съобщение - разрешете на SimpleX да работи във фонов режим в следващия диалогов прозорец. В противен случай известията ще бъдат деактивирани.]]> + Разрешете в следващия диалогов прозорец, за да получавате незабавно известия.]]> Незабавни известия Незабавни известия! Незабавните известия са деактивирани! @@ -765,7 +765,7 @@ Инсталирай SimpleX Chat за терминал Как работи Как работи SimpleX - Защитен от спам и злоупотреби + Защитен от спам Игнорирай Покани членове Необратимото изтриване на съобщения е забранено в този чат. @@ -788,10 +788,10 @@ Маркирай като проверено %s не е потвърдено %s е потвърдено - Оценете приложението + Оцени приложението Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани. Мрежа и сървъри - Мрежови настройки + Разширени настройки Порт порт %d Задължително @@ -852,7 +852,7 @@ Забрани реакциите на съобщенията. Моля, въведете предишната парола след възстановяване на резервното копие на базата данни. Това действие не може да бъде отменено. Голям файл! - Реакциите на съобщения са забранени в тази група. + Реакциите на съобщения са забранени. Ново в %s предлага %s предлага %s: %2s @@ -884,7 +884,7 @@ Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн. Нов архив на база данни Стар архив на база данни - Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности. + Някои не-фатални грешки са възникнали по време на импортиране: Съобщения Моля, въведете правилната текуща парола. Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите. @@ -895,14 +895,14 @@ Поканени сте в групата. Присъединете се, за да се свържете с членове на групата. Вие се присъединихте към тази група напусна - отстранен %1$s + премахнат %1$s ви острани напусна член наблюдател собственик кодът за сигурност е променен - отстранен + премахнат Нова членска роля Няма контакти за добавяне Напусни групата @@ -955,7 +955,7 @@ Получаване на съобщения… Работи, когато приложението е отворено Simplex Chat услуга - SimpleX фонова услуга – използва няколко процента от батерията на ден.]]> + SimpleX работи във фонов режим вместо да използва push известия.]]> За да получавате известия, моля, въведете паролата на базата данни Влезте с вашите идентификационни данни Грешка при доставката на съобщението @@ -963,7 +963,7 @@ Най-вероятно този контакт е изтрил връзката с вас. Няма история Отвори конзолата - Отвори чат профилите + Промени чат профилите Моля, запомнете я или я съхранявайте на сигурно място - няма начин да възстановите загубена парола! Получено съобщение Отговори @@ -978,8 +978,8 @@ Разрешение е отказано! профилно изображение запазено място за профилно изображение - Протокол и код с отворен код – всеки може да оперира собствени сървъри. - Хората могат да се свържат с вас само чрез ликовете, които споделяте. + Всеки може да оперира сървъри. + Вие решавате кой може да се свърже с вас. Поверителността преосмислена Добави поверителна връзка Отвори @@ -1006,13 +1006,13 @@ Сканирай код Сканирайте кода за сигурност от приложението на вашия контакт. Код за сигурност - Изпращайте въпроси и идеи + Изпрати въпроси и идеи Запази сървърите\? Запазените WebRTC ICE сървъри ще бъдат премахнати. Запази SOCKS прокси настройки Покажи опциите за разработчици - Запази настройките за автоматично приемане + Запази настройките за SimpleX адрес Запази настройките\? Запази паролата на профила Спри чата\? @@ -1056,7 +1056,7 @@ Нулиране Изпрати Започни нов чат - Изпратете ни имейл + Изпрати ни мейл Някои сървъри не минаха теста: Изключване\? таен @@ -1066,7 +1066,7 @@ Деактивиране на потвърждениe за доставка за групи\? Активиране за всички групи Изпращането на потвърждениe за доставка е разрешено за %d групи - Рестартирайте приложението, за да използвате импортирана чат база данни. + Рестартирайте приложението, за да използвате импортирана база данни. Тази група има над %1$d членове, потвърждениeто за доставка няма да се изпраща. СЪРВЪРИ %s: %s @@ -1083,7 +1083,7 @@ Отзови файл Изпращането на файла ще бъде спряно. Запази сървърите - Изпратете съобщение на живо - то ще се актуализира за получателя(ите), докато го пишете + Изпрати съобщение на живо - то ще се актуализира за получателя(ите), докато го пишете Тестът на сървъра е неуспешен! Оценка на сигурността Сподели файл… @@ -1180,17 +1180,17 @@ Този текст не е линк за връзка! Твърде много видеоклипове! Тази настройка се прилага за съобщения в текущия ви профил - За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти. + За да се защити поверителността, SimpleX използва идентификатори за опашки от съобщения, отделни за всеки от вашите контакти. Опит за свързване със сървъра, използван за получаване на съобщения от този контакт. - Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s). + Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s). Тестът е неуспешен на стъпка %s. Базата данни не работи правилно. Докоснете, за да научите повече Изображението не може да бъде декодирано. Моля, опитайте с друго изображение или се свържете с разработчиците. Докосни бутона Благодарим Ви, че инсталирахте SimpleX Chat! Запази и уведоми контактите - Ново поколение поверителни съобщения - Първата платформа без никакви потребителски идентификатори – поверителна по дизайн. + Бъдещето на комуникацията + Няма потребителски идентификатори. Системна Неправилно ID на следващото съобщение (по-малко или еднакво с предишното). \nТова може да се случи поради някаква грешка или когато връзката е компрометирана. @@ -1201,8 +1201,8 @@ Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно! Ще бъдете свързани, когато устройството на вашия контакт е онлайн, моля, изчакайте или проверете по-късно! Няма да загубите контактите си, ако по-късно изтриете адреса си. - Вашите настройки - Вашият SimpleX адрес + Настройки + SimpleX адрес Използвай за нови връзки Вашите XFTP сървъри Използвай сървърите на SimpleX Chat\? @@ -1233,7 +1233,7 @@ Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с %s. Премахни от любимите ВИЕ - Вашата чат база данни + Вашата база данни Изчаква се получаването на изображението Изчаква се получаването на изображението Изчаква се получаването на видеото @@ -1245,12 +1245,12 @@ Гласово съобщение Гласово съобщение (%1$s) Гласово съобщение… - Гласовите съобщения са забранени в тази група. + Гласовите съобщения са забранени. непрочетено Добре дошли! Добре дошли %1$s! Нямате чатове - Не може да изпращате съобщения! + вие сте наблюдател Вашият сървър Използвай сървър Вашият адрес на сървъра @@ -1260,7 +1260,7 @@ Вие контролирате своя чат! Грешна парола! Гласови съобщения - Вашите настройки + Настройки Какво е новото Вашите контакти могат да позволят пълното изтриване на съобщението. Актуализирай паролата на базата данни @@ -1275,8 +1275,7 @@ Вашият чат профил ще бъде изпратен на членовете на групата Вашият чат профил ще бъде изпратен \nдо вашия контакт - Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. -\nТова действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени. + Вашата текуща база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. \nТова действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени. Актуализирането на настройките ще свърже отново клиента към всички сървъри. актуализиран профил на групата Видео @@ -1376,8 +1375,8 @@ Отвори Грешка при създаване на контакт с член Изпрати лично съобщение за свързване - изпрати лично съобщение - свързан директно + изпрати за свързване + заявка за връзка Разшири Блокиране на членове на групата Изпрати отново заявката за свързване? @@ -1444,7 +1443,7 @@ Несъвместим! Изчаква се мобилното устройство да се свърже: Изтрий %d съобщения? - Свържи мобилно устройство + Свързване на мобилно устройство Свързване с %1$s? Премахни член Блокирай @@ -1533,7 +1532,7 @@ Докосни за сканиране Запази Докосни за поставяне на линк за връзка - Търсене или поставяне на SimpleX линк + Търси или постави SimpleX линк Стартирай чата? Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново. Настолното устройство има грешен код за връзка @@ -1560,7 +1559,7 @@ Настолното устройство е заето Връзката с настолното устройство бе прекъсната Критична грешка - Бивш член %1$s + Член %1$s неизвестен статус неизвестен Вътрешна грешка @@ -1630,7 +1629,7 @@ Потвърди мрежовите настройки Грешка при изтеглянето на архива Подготвя се качване - Грешка при експортиране на чат базата данни + Грешка при експортиране на базата данни Стартиране на чата Квантово устойчиво криптиране Миграция на данните от приложението @@ -1711,8 +1710,8 @@ Мрежова връзка SimpleX линкове Забранете изпращането на SimpleX линкове - Членовете на групата могат да изпращат SimpleX линкове. - SimpleX линкове са забранени в тази група. + Членовете могат да изпращат SimpleX линкове. + SimpleX линкове са забранени. Активирано за собственици Камера @@ -1749,4 +1748,781 @@ Профилни изображения Променете формата на профилните изображения Квадрат, кръг или нещо между тях. - \ 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.]]> + Отвори условията + Превключете чат профила за еднократен линк за връзка. + Поправи връзката? + Връзката изисква предоговаряне на криптирането. + Сподели еднократен линк за връзка с приятел + Настройки на адреса + Изберете мрежови оператори за използване. + Как помага за поверителността + Ще спрете да получавате съобщения от този чат. Историята на чата ще бъде запазена. + Добави членове на екипа + Заглушаване на всички + Използвай инкогнито профил + Отвори чат + Отвори нов чат + Отвори нова група + Само вие и модераторите го виждат + Само подателят и модераторите го виждат + архивиран доклад + архивиран доклад от %s + 1 чат с член + 1 доклад за нарушения + 1 година + 4 нови езика на интерфейса + Приеми + Приеми + Приеми като член + Приеми като наблюдател + Приеми заявка за контакт + Приеми заявка за контакт + %1$s приет + бяхте приети + Приеми член + Добави съобщение + Обнови адрес + всички + Всички нови съобщения от тези членове ще бъдат скрити! + Позволи докладването на съобщения на модераторите. + Всички доклади за нарушения ще бъдат архивирани за вас. + Всички сървъри + Друга причина + Архивирай + Архивиране на всички доклади за нарушения? + Архивиране на %d доклада? + Архивирай доклад за нарушения + Архивирай доклад за нарушения? + Архивирай докладите за нарушения + Попитай + По-добра производителност на групите + По-добра поверителност и сигурност + Биография: + Биографията е твърде дълга + Блокиране на членовете за всички? + Бизнес връзка + С използването на SimpleX Chat вие се съгласявате със:\n- изпращане само на легално съдържание в публични групи.\n- уважение към другите потребители – без спам. + Промяната на профила е невъзможна + съобщенията не могат да бъдат изпратени + Каталонски, индонезийски, румънски и виетнамски - благодарение на нашите потребители! + криптиране от край до край.]]> + само след като заявката ви бъде приета.]]> + Промяна на автоматичното изтриване на съобщения? + Промени списъка + Промени подредбата + Чатове с членовете + Чат с администраторите + Чат с администраторите + Чат с администраторите + Чат с член + Разговаряйте с членовете, преди да се присъединят. + Нарушение на правилата на общността + Конфигуриране на сървърни оператори + Свързване + Свържете се по-бързо! 🚀 + Връзката е блокирана + Връзката е блокирана от оператора на сървъра:\n%1$s. + контактът е изтрит + контактът е деактивиран + контактът не е готов + контактът трябва да приеме… + Съдържанието нарушава условията за ползване + Създайте вашия адрес + %d чат(а) + %d чата с членовете + по подразбиране (%s) + Изтрий чата + Изтрийте чат съобщенията от вашето устройство. + Изтриване на чата с члена? + Изтриване на доклад за нарушения + Описанието е твърде дълго + Деактивиране на автоматичното изтриване на съобщения? + Деактивиране на изтриването на съобщения + %d съобщения + Не пропускайте важни съобщения. + %d доклада за нарушения + Активирайте изчезващите съобщения по подразбиране. + Грешка при приемането на член + Грешка при промяна на профила + Грешка при създаването на доклад за нарушения + Грешка при изтриване на чата с член + Грешка при отваряне на чата + Грешка при отваряне на групата + Грешка при четене на паролата за базата данни + Грешка при отхвърляне на заявката за контакт + Грешка при запазване на настройките + По-бързо изтриване на групи. + По-бързо изпращане на съобщения. + Файлът е блокиран от оператора на сървъра:\n%1$s. + За всички модератори + За мен + Цял линк + Получавайте известия, когато бъдете споменати. + Група + групата е изтрита + Помага на администраторите да модерират своите групи. + Неприемливо съдържание + Неприемлив профил + Присъединяване към групата + Поддържайте чатовете си чисти + По-малко трафик през мобилните мрежи. + Зареждане на профил… + Приемане на членове + членът има стара версия + Доклади за нарушения от членовете + Членовете могат да докладват съобщения на модераторите. + Членовете ще бъдат премахнати от чата - това не може да бъде отменено! + Членовете ще бъдат премахнати от групата - това не може да бъде отменено! + Членът ще се присъедини към групата, приеми член? + Споменете членовете 👋 + Изпращайте съобщение веднага щом докоснете Свързване. + Съобщенията от тези членове ще бъдат показани! + Съобщенията в този чат никога няма да бъдат изтрити. + модератор + модератори + Нова групова роля: Модератор + Нов член иска да се присъедини към групата. + Не + Няма чатове с членове + Няма сесия за поверително рутиране + Бележки + несинхронизирано + изключено + Изключено + Отвори линк + Отваряне на линковете от чатовете в списъка + Отвори за приемане + Отвори за свързване + Отвори за присъединяване + Отвори уеб линк? + Организиране на чатовете в списъци + Паролата в хранилището за ключове не може да бъде прочетена, моля, въведете я ръчно. Това може да се е случило след системна актуализация, несъвместима с приложението. Ако случаят не е такъв, моля, свържете се с разработчиците. + Паролата в хранилището за ключове не може да бъде прочетена. Това може да се е случило след системна актуализация, несъвместима с приложението. Ако случаят не е такъв, моля, свържете се с разработчиците. + чакащо + чакащо одобрение + чакащо преглед + Моля, изчакайте модераторите на групата да прегледат заявката ви за присъединяване към групата. + Предварително зададени сървъри + Политика за поверителност и условия за ползване. + Личните чатове, групи и вашите контакти не са достъпни от операторите на сървърите. + Имена на лични медийни файлове. + Време за изчакване на поверително рутиране + Време за изчакване на протокола във фонов режим + Времето на изчакване за установяване на TCP връзка във фонов режим + Адресът ще бъде кратък и вашият профил ще бъде споделен чрез него. + Забранете докладването на съобщения на модератори. + Отхвърляне + Отхвърляне на заявка за контакт + отхвърлен + отхвърлен + Отхвърляне на член? + премахнат от групата + Премахване на членове? + Премахва съобщения и блокира членове. + Докладвай + Докладване на съдържание: само модераторите на групата ще го виждат. + Докладването на съобщения е забранено в тази група. + Докладване на профил на член: само модераторите на групата ще го виждат. + Докладвай друго: само модераторите на групата ще го виждат. + Причина за докладване? + Доклад: %s + Доклади + Докладът е изпратен до модераторите + Докладвай за спам: само модераторите на групата ще го виждат. + Докладване на нарушение: само модераторите на групата ще го виждат. + заявката е изпратена + заявката за присъединяване е отхвърлена + преглед + прегледано от администраторите + Преглед на членовете на групата + Преглед на членовете + Прегледайте членовете преди да ги приемете. + Запазване на настройките за достъп? + Изпращане на заявка за контакт? + Изпращане на лични доклади за нарушения + Изпрати заявка + Изпрати заявка без съобщение + Изпрати личната си обратна връзка до групи. + Изпратено до вашия контакт след осъществяване на връзка. + Задаване на име на чат… + Задаване на достъп за членове + Задаване на срок на валидност на съобщенията в чатовете. + Задайте биография на профила и съобщение при посрещанее. + Обнови групов линк? + Обнови + Обнови адрес? + Споделете адреса си + Кратко описание: + Кратък линк + Кратък SimpleX адрес + Линк за канала на SimpleX + Спам + Спам + Докосни Свързване за чат + Докосни Свързване за изпращане на заявка + Докосни Присъединяване към групата + TCP порт за съобщения + Докладът ще бъде архивиран за вас. + Подателят НЯМА да бъде уведомен. + Това действие не може да бъде отменено - съобщенията, изпратени и получени в този чат по-рано от избраното, ще бъдат изтрити. + Тази връзка изисква по-нова версия на приложението. Моля, актуализирайте приложението или помолете контакта да ви изпрати съвместим линк. + Времето за изчезване е зададено само за нови контакти. + За да използвате друг профил след опит за връзка, изтрийте чата и използвайте линка отново. + Отблокиране за всички членове? + Непрочетени споменавания + Неподдържан линк за връзка + Актуализирани условия + Актуализирайте адреса си + Използвай TCP порт %1$s, когато не е посочен порт. + Използвай TCP порт 443 само за предварително зададени сървъри. + Използвай уеб порт + Съобщение при посрещане + Посрещане на контактите 👋 + Да + Вие приехте този член + Можете да споменете до %1$s членове в съобщение! + Не можете да изпращате съобщения! + Можете да видите вашите доклади за нарушения в Чат с администратори. + вие напуснахте + Вашата биография: + Вашият бизнес контакт + Вашият контакт + Вашата група + Вашият профил + Сподели стар адрес + Сподели стар линк + Линкът ще бъде кратък и профилът на групата ще бъде споделен чрез него. + Обнови групов линк + ЗАЯВКИ ЗА КОНТАКТ ОТ ГРУПИ + Членът е изтрит - не може да се приеме заявката + заявка за връзка от група %1$s + Тази настройка е за текущия профил + Разреши файлове и медия само ако вашият контакт ги разрешава. + Позволи на вашите контактите да изпращат файлове и медия. + Бот + И вие, и вашият контакт можете да изпращате файлове и медия. + Отпадащи опции + Грешка при маркиране на чата като прочетен + Файлове и медията са забранени в този чат. + Само вие можете да изпращате файлове и медия. + Само вашият контакт може да изпраща файлове и медия. + Отвори почистен линк + Отвори пълен линк + Отвори за използване на бот + Забрани изпращането на файлове и медия. + Премахни параметри за проследяване от линковете + SimpleX реле линк + Натисни Свързване, за използване на бота + За да изпращате команди, трябва да сте свързани. + 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..a3b97f0be2 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -0,0 +1,2504 @@ + + + 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 + connexió sol·licitada + 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ç + Desar la configuració d\'adreça SimpleX + 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. + L\'empremta digital a l\'adreça del servidor no coincideix amb el certificat. + 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! + Entrar com a %s + enviar per connectar + 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 + ets observador(a) + 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 connexió s\'ha mogut a %s però s\'ha produït un error en canviar de 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 %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 + Estat convidats(des) 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 + Enllaç complet + Enllaç curt + Tots els servidors + Apagat + Feu servir el port TCP 443 només per a servidors predefinits. + Servidors predefinits + Error en acceptar membre + %d missatges + Informe enviat a la moderació + desactivat + Acceptar + Acceptar com a membre + Acceptar membre + El(la) membre s\'unirà al grup, l\'accepteu? + revisat per l\'administració + no es poden enviar missatges + contacte eliminat + contacte desactivat + el contacte no està a punt + el grup s\'ha suprimit + no sincronitzat + eliminat(da) del grup + sol·licitud d\'unió rebutjada + No podeu enviar missatges! + Podeu veure els vostres informes al xat amb administració. + Heu sortit + %1$s acceptat(da) + us ha acceptat + pendient de revisió + Establir l\'admissió de membres + Admissió de membre + Xat amb admins + No hi ha xats amb membres + Acceptar com a observador + Desar la configuració d\'admissió? + Error en suprimir el xat + %d xat(s) + el(la) membre té una versió antiga + Un(a) nou(va) membre vol unir-se al grup. + Si us plau, espereu que els moderadors del grup revisin la vostra sol·licitud per unir-vos-hi. + heu acceptat aquest(a) membre + per revisar + Xats amb admins + Xats amb membre + Eliminar xat + Rebutjar + Rebutjar membre? + Revisar membres + Revisar membres abans d\'admetre (trucar a la porta). + un xat amb un(a) membre + Xats amb membres + Suprimir el xat amb membre? + %d xats amb membres + tots(es) + Actualitzar l\'adreça + Acceptar la sol·licitud de contacte + Afegir un missatge + No es pot canviar el perfil + xifratge extrem a extrem.]]> + després que la sol·licitud sigui acceptada.]]> + Connectar + el contacte hauria d\'acceptar… + Error en canviar el perfil + Error en obrir el xat + Error en obrir el grup + Error en rebutjar la sol·licitud de contacte + Entrar al grup + Obrir xat + Obrir nou xat + Obrir nou grup + Obrir per acceptar + Obrir per connectar + Obrir per entrar + L\'adreça serà curta i el vostre perfil es compartirà a través d\'ella. + Rebutjar la sol·licitud de contacte + sol·licitud enviada + Enviar sol·licitud de contacte? + Enviar sol·licitud + Enviar sol·licitud sense missatge + Enviat al contacte després de la connexió. + Actualitzar l\'enllaç del grup? + Actualitzar + Actualitzar l\'adreça? + L\'emissor(a) NO serà notificat(da). + Per utilitzar un altre perfil després d\'un intent de connexió, suprimiu el xat i torneu a utilitzar l\'enllaç. + Missatge de benvinguda + El vostre perfil + Xatejar amb l\'administració + Xatejar amb membres abans que s\'uneixin. + Connexió més ràpida! 🚀 + Menys trànsit a les xarxes mòbils. + Enviar missatges a l\'instant en tocar Connectar. + Nou rol de grup: Moderador(a) + Sense sessió d\'encaminament privada + Temps d\'espera d\'encaminament privat + Temps d\'espera del protocol en segon pla + Elimina missatges i bloca membres. + Revisió de membres del grup + Envieu comentaris privats a grups. + Temps d\'espera de la connexió TCP en segon pla + Bio: + Bio massa llarga + Descripció massa llarga + Carregant el perfil… + Descripció breu: + La vostra bio: + Acceptar la sol·licitud de contacte + Connexió empresarial + Grup + Toqueu Connectar per xatejar + Toqueu Connectar per enviar la sol·licitud + Toqueu Unir-vos al grup + El vostre contacte empresarial + El vostre contacte + El vostre grup + El temps de desaparició només està definit per a contactes nous. + 4 idiomes nous + Català, indonesi, romanès i vietnamita: gràcies per les col·laboracions! + Creeu la vostra adreça + Activar els missatges que desapareixen per defecte. + Mantingueu els vostres xats nets + Definir la biografia del perfil i el missatge de benvinguda. + Compartir la vostra adreça + Adreça SimpleX curta + Actualitzar la vostra adreça + Utilitzar el perfil d\'incògnit + Doneu la benvinguda als vostres contactes 👋 + Compartir l\'adreça antiga + Compartir l\'enllaç antic + L\'enllaç serà curt i el perfil del grup es compartirà a través d\'ell. + Actualitzar l\'enllaç del grup + SOL·LICITUDS DE CONTACTE DE GRUPS + Membre eliminat(da); no es pot acceptar la sol·licitud. + connexió sol·licitada del grup %1$s + Aquesta configuració és per al perfil actual + Permetre fitxers i contingut multimèdia només si el vostre contacte ho permet. + Permetre que els contactes enviïn fitxers i contingut multimèdia. + Bot + Tant vos com el vostre contacte podeu enviar fitxers i contingut multimèdia. + Els fitxers i els continguts multimèdia estan prohibits en aquest xat. + Només vos podeu enviar fitxers i contingut multimèdia. + Només el contacte pot enviar fitxers i contingut multimèdia. + Obrir per utilitzar el bot + Prohibir l\'enviament de fitxers i contingut multimèdia. + Toqueu Connectar per utilitzar el bot + Per enviar ordres heu d\'estar connectat(da). + Obrir enllaç net + Obrir enllaç complet + Eliminar el seguiment de l\'enllaç + Opcions obsoletes + Enllaç de servidor SimpleX + Error marcant com a llegit + L\'empremta digital a l\'adreça del servidor de destinació no coincideix amb el certificat: %1$s. + L\'empremta digital a l\'adreça del servidor de reenviament no coincideix amb el certificat: %1$s. + L\'empremta digital a l\'adreça del servidor no coincideix amb el certificat: %1$s. + 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 c80d24f0bd..423f0e135f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -72,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 @@ -208,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 @@ -243,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 @@ -254,7 +254,7 @@ Hovory na uzamčené obrazovce: Otevřete SimpleX Chat pro přijetí hovoru Povolte volání ze zamčené obrazovky prostřednictvím Nastavení. - Otevřený + Otevřít Zvuk zapnut Reproduktor vypnut Reproduktor zapnut @@ -320,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. @@ -343,8 +343,8 @@ 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… @@ -356,7 +356,7 @@ Nastavit 1 den Chyba spojení (AUTH) Odesílatel možná smazal požadavek připojení - Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo + Server vyžaduje autorizaci pro vytvoření front, zkontrolujte heslo. Odstranit frontu Smazat Smazat čekající připojení\? @@ -384,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… @@ -446,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ů @@ -468,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 @@ -493,7 +493,7 @@ Připojtíte se ke všem členům skupiny. Připojení Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu. - Pokoušíte se připojit k serveru používaném pro příjem zpráv od tohoto kontaktu (chyba: %1$s). + Pokoušíte se připojit k serveru používaném pro příjem zpráv od tohoto kontaktu (chyba: %1$s). označeno jako smazáno Odesílání souborů zatím není podporováno přijímání souborů zatím není podporováno @@ -519,7 +519,7 @@ Chyba mazání žádosti kontaktu Chyba mazání probíhajícího připojení kontaktu Test selhal v kroku %s. - Je možné, že otisk certifikátu v adrese serveru je nesprávný. + Otisk adresy serveru neodpovídá certifikátu. Připojit Odpojit Chyba mazání uživatelského profilu @@ -548,8 +548,8 @@ Smazat pro mě upraveno neautorizované odeslání - jste pozváni do skupiny - připojit jako %s + Jste pozváni do skupiny + Připojit jako %s připojuji… Začněte nový chat Chat s vývojáři @@ -574,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 @@ -604,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 @@ -664,10 +664,10 @@ Nové vymezení soukromí Bez uživatelských identifikátorů Odolná vůči spamu - 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ů. + 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í).]]> @@ -678,7 +678,7 @@ 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é @@ -730,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 @@ -766,7 +766,7 @@ 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. pozvánka do skupiny %1$s Jste zváni do skupiny. Připojte se k členům skupiny. @@ -893,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. + Č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é 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 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 @@ -937,7 +937,7 @@ pozorovatel Zpráva bude smazána pro všechny členy. Zpráva bude pro všechny členy označena jako moderovaná. - Nemůžete posílat zprávy! + jste pozorovatel Chyba aktualizace odkazu skupiny Počáteční role Systém @@ -1016,7 +1016,7 @@ Chyba načítání serverů XFTP Chyba ukládání XFTP serverů Ujistěte se, že adresy XFTP serverů jsou ve správném formátu s oddělenými řádky a nejsou duplicitní. - Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo + Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo. Porovnat soubor Vytvořit soubor Smazat soubor @@ -1038,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! @@ -1063,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. @@ -1114,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 @@ -1141,7 +1141,7 @@ Posláno: %s Zmizí: %s Upraveno: %s - %s (aktuální) + %s (aktuálně) Tmavý motiv Import motivu SimpleX @@ -1154,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 @@ -1172,7 +1172,7 @@ Sdílet adresu s kontakty\? Přestat sdílet adresu\? Vytvořit adresu, aby se s vámi lidé mohli spojit. - Uložit nastavení automatického přijímání + Uložit nastavení SimpleX adresy Přestat sdílet Automaticky přijmout Můžete vytvořit později @@ -1192,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ů @@ -1228,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 @@ -1246,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. @@ -1267,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í) @@ -1345,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. @@ -1367,7 +1363,7 @@ 6 nových jazyků pro rozhraní Aplikace šifruje nové místní soubory (kromě videí) Arabština, bulharština, finština, hebrejština, thajština a ukrajinština - díky uživatelům a Weblate. - propojeno napřímo + požádáno o připojení Otevřít Šifrování uložených souborů a médií Chyba vytváření kontaktu @@ -1377,10 +1373,8 @@ 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ší. - odeslat přímou zprávu + - připojení k adresáři skupin (BETA)!\n- doručenky (až 20 členů).\n- rychlejší a stabilnější. + odeslat pro připojení smazaný kontakt Chyba Vytvořit profil @@ -1390,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í @@ -1429,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. @@ -1472,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 @@ -1489,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ě @@ -1543,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? @@ -1581,7 +1573,7 @@ Vyhledávání přijímá pozvánky. Zobrazit pomalé API volání odstraněna kontaktní adresa - Vložte člena %1$s + Člen %1$s Vložte odkaz na připojení! Nejnovější historie a vylepšený directory bot. Soukromé poznámky @@ -1600,7 +1592,7 @@ Chyba otevření prohlížeče odstraněn profilový obrázek nastavit novou kontaktní adresu - nastavit nový profilový obrázek + nastavil nový profilový obrázek Uložené zprávy Pomalá funkce Soukromé poznámky @@ -1735,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ě @@ -1751,7 +1743,7 @@ 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í.]]> Pokročilé nastavení Všechny barevné režimy @@ -1860,7 +1852,7 @@ Nastavené SMP servery Probíhá Části nahrány - %1$d chuba souboru(ů):\n%2$s + %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ě. @@ -1940,7 +1932,7 @@ 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) stále stahuji. + %1$d soubor(y) se stále stahují. Lepší datování zpráv. Lepší zabezpečení ✅ Části odstraněny @@ -2014,7 +2006,7 @@ Odeslané zprávy Odeslaných celkem Adresa serveru - Dosažitelný panel nástrojů chatu + Dosažitelné panely nástrojů Chyba potvrzení Připojení Vytvořen @@ -2041,4 +2033,499 @@ Pošlete zprávu pro povolení volání. Přijmout podmínky Přijaté podmínky - \ No newline at end of file + 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 nastala chyba při přepínání profilu. + 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í + 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. + Chyba přijmutí člena + %d chat(y) + %d chat se členy + %d zpráv + 1 chat se členem + Uložit nastavení vstupného? + Chat s adminy + Nastavit přijímání členů + Přihlášení člena + Schválit členy + Schválit členy před přijetím (zaklepat). + Chat se členy + Žádné chaty se členy + Chat s adminy + Přijmout + Přijmout člena + čeká na schválení adminy + Povýšit adresu + přijat %1$s + Vás přijal + schválení + Odstranit chat se členem? + Odmítnout + odešli jste + Chat se členem + Prosím, počkejte až moderátoři skupiny vaši žádost o připojení ke skupině schválí. + přijali jste tohoto člen + Hlášení odesláno moderátorům + kontakt smazán + kontakt vypnut + kontakt nepřipraven + nesynchronizováno + žádost o přihlášení zamítnuta + Nemůžete posílat zprávy! + Můžete zobrazit své hlášení v Chatu s adminy. + nemůže posílat zprávy + skupina smazána + člen má starou verzi + odstraněn ze skupiny + Nový člen chce připojit do skupiny. + čeká na posouzení + vypnuto + vše + Odstranit chat + Člen se připojí ke skupině, přijmout? + Přijmout jako člena + Přijmout jako pozorovatele + Odmítnout člena? + Chyba odstranění chatu + Použït profil inkognito + Otevřít chat + Otevřít nový chat + Otevřít novou skupinu + Připojit + Připojte se rychleji! 🚀 + POŽADAVKY NA PŘIPOJENÍ ZE SKUPIN + kontakt by měl přijmout… + Vytvořit vaši adresu + Popis příliš dlouhý + Povolení mizících zpráv ve výchozím nastavení. + Chyba změny profilu + Chyba otevření chatu + Chyba otevření skupiny + Chyba odmítnutí žádosti o kontakt + Skupina + Připojit ke skupině + Udržujte chat čistý + Méně provozu na mobilních sítích. + Načítání profilu… + Člen je smazán - nemůže přijmout žádost + Nová skupinová role: Moderátor + Zastaralé možnosti + Soubory a média jsou zakázány v tomto chatu. + Pouze vy můžete odesílat soubory a média. + Pouze vaše kontakty mohou posílat soubory a média. + Otevřít čistý odkaz + Otevřít celý odkaz + Otevřít pro přijetí + Otevřít pro připojení + Otevřít pro připojení + Otevřít pro použítí bota + Vypršel čas soukromého směrování + Odesílání souborů a médií zakázáno. + Vypršel čas protokolu na pozadí + Odmítnout žádost o kontakt + Odebrat sledování odkazů + Odstranit zprávy a blokovat členy. + koncovým šifrováním.]]> + SimpleX relé odkaz + Sezení bez soukromého směrování + 4 nové jazyky rozhraní + Přijmout žádost o kontakt + Přijmout žádost o kontakt + Přidat zprávu + Povolit soubory a média pouze pokud, je váš kontakt povolí. + Povolit vašim kontaktům odesílání souborů a médii. + Bio: + Bio příliš velké + Bot + Vy i vaše kontakty můžete posílat soubory a média. + Obchodní spojení + Nelze změnit profil + Katalánština, Indonéština, Rumunština a Vietnamština - díky našim uživatelům! + až bude váš požadavek přijat.]]> + Chat se správci + Chat se členy než se připojí. + Chyba při označení jako přečteno + Otisk adresy cílového serveru neodpovídá certifikátu: %1$s. + Otisk adresy přeposílacího serveru neodpovídá certifikátu: %1$s. + Otisk adresy serveru neodpovídá certifikátu: %1$s. + Chatujte okamžitě po připojení. + požadováno spojení ze skupiny %1$s + požadavek odeslán + Zkontrolovat členy skupiny + Odeslat žádost o kontakt? + Odeslat žádost + Odeslat žádost bez zprávy + Posílání soukromé zpětné vazby do skupin. + Pošlete kontaktu po připojení. + Nastavení bio profilu a uvítací zprávy. + Sdílení staré adresy + Sdílení starého odkazu + Sdílet vaši adresu + Stručný popis: + Krátké SimpleX adresy + Klepněte na Připojte se k chatu + Klepněte na Připojit k odeslání požadavku + Klepněte na Připojit k použití bota + Klepněte na Připojit skupinu + Vypršelo TCP připojení na pozadí + Adresa bude krátká a Váš profil bude sdílen prostřednictvím adresy. + Odkaz bude krátký a profil skupiny bude sdílen prostřednictvím odkazu. + Odesílatel NEBUDE informován. + Toto nastavení je pro váš aktuální profil + Čas mizení, je nastaven pouze pro nové kontakty. + Pro odeslání příkazů musíte být připojen. + Pro použití jiného profilu po pokusu o připojení, smažte chat a znovu použijte odkaz. + Aktualizovat vaši adresu + Povýšení + Povýšit adresu? + Povýšit odkaz skupiny + Povýšit odkaz skupiny? + Uvítací zpráva + Přivítejte vaše kontakty 👋 + Vaše bio: + Váš obchodní kontakt + Váš kontakt + Vaše skupina + Váš profil + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml new file mode 100644 index 0000000000..30e557a4e3 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml @@ -0,0 +1,862 @@ + + + %1$d file error(s):\n%2$s + %1$d fil(er) henter stadig + %1$d fil(er) kunne ikke hentes. + %1$d fil(er) blev slettet + %1$d fil(er) hentes ikke + %1$d beskeder kunne ikke dekrypteres. + %1$d beskeder modereret af %2$s + %1$d beskeder sprunget over. + %1$d andre filfejl. + %1$d sprungne beskeder + %1$s MEDLEMMER + %1$s beskeder blev ikke videresendt + %1$s ønsker at komme i kontakt med dig via + 1 chat med et medlem + 1 dag + 1 minut + 1 måned + 1 rapport + 1-gangs link + 1 uge + 1 år + 30 sekunder + 5 minutter + 6 nye grænsefladesprog + a + b + Afbryd + Afbryd adresseændring + Afbryd adresseændring? + Om operatører + Om SimpleX + Om SimpleX-adressen + Om SimpleX Chat + over, så: + Farveaccent + Acceptere + Acceptere + Acceptere + Modtage + Acceptere + Acceptere + Accepter som medlem + Accepter som observatør + Accepter betingelser + Accepter forbindelsesanmodning? + Accepter kontaktanmodning + accepterede %1$s + accepted call + Accepterede betingelser + accepteret invitation + accepterede dig + Accepter inkognito + Accepter medlem + Få adgang til serverne via SOCKS proxy på port %d? Proxyen skal startes, før denne indstilling aktiveres. + Anerkendt + Bekræftelsesfejl + Aktive forbindelser + Tilføj adresse til din profil, så dine kontakter kan dele den med andre. Profilopdateringen sendes til dine kontakter. + Tilføj kontakt + Tilføjede medie- og filservere + Tilføjede beskedservere + Tilføj venner + Ekstra farveaccent + Ekstra farveaccent 2 + Yderligere sekundær + Tilføj liste + Tilføj besked + Tilføj forudindstillede servere + Tilføj profil + Adresse + Adresseændringen bliver annulleret. Den gamle modtageradresse bliver brugt. + Adresse eller engangslink? + Adresseindstillinger + Tilføj server + Tilføj servere ved at scanne QR-koder. + Tilføj et kort link + Tilføj teammedlemmer + Tilføj til en anden enhed + Tilføj til liste + Tilføj velkomstbesked + Tilføj dine teammedlemmer til samtalerne. + administrator + administratorer + Administratorer kan blokere et medlem for alle. + Administratorer kan oprette links for at deltage i grupper. + Avancerede netværksindstillinger + Avancerede indstillinger + Avancerede indstillinger + Et par ting mere + accepterer kryptering… + accepterer kryptering for %s… + alle + Alle + Alle appdata slettes. + k + Opret forbindelse via kontaktadresse? + Opret forbindelse via engangslink? + Deltag i gruppe? + Brug den aktuelle profil + Brug ny inkognito -profil + Din profil sendes til den kontakt, du har modtaget dette link fra. + Du opretter forbindelse til alle gruppemedlemmer. + Forbinde + Tilslut inkognito + Alle chats og meddelelser slettes - dette kan ikke fortrydes! + Alle chats fjernes fra listen %s, og listen slettes + Alle farvetilstande + Alle data slettes, når det indtastes. + Alle gruppemedlemmer forbliver forbundet. + alle medlemmer + Alle meddelelser slettes - dette kan ikke fortrydes! + Alle meddelelser slettes - dette kan ikke fortrydes! Meddelelserne slettes KUN for dig. + Alle nye meddelelser fra %s vil være skjult! + Alle nye meddelelser fra disse medlemmer vil være skjult! + Tillade + Tillade + Tillade opkald? + Tillad kun opkald, hvis din kontakt tillader dem. + Tillad kun forsvindende meddelelser, hvis din kontakt tillader dem. + Tillad nedjustering + Tillad kun irreversibel meddelelsesudslettelse, hvis din kontakt giver den mulighed for dig. (24 timer) + Tillad meddelelsesreaktioner. + Tillad kun meddelelsesreaktioner, hvis din kontakt tillader dem. + Tillad at sende direkte beskeder til medlemmerne. + Tillad for irreversibelt at slette sendte beskeder. (24 timer) + Tillad at rapportere meddelelser til moderatorer. + Tillad at sende forsvindende beskeder. + Tillad at sende filer og medier. + Tillad at sende simplex -links. + Tillad at sende stemmemeddelelser. + Tillad stemmemeddelelser? + Tillad kun stemmemeddelelser, hvis din kontakt tillader dem. + Tillad dine kontakter at tilføje meddelelsesreaktioner. + Lad dine kontakter ringe til dig. + Lad dine kontakter irreversibelt slette sendte beskeder. (24 timer) + Lad dine kontakter sende forsvindende beskeder. + Lad dine kontakter sende stemmemeddelelser. + Alle profiler + Alle rapporter vil blive arkiveret for dig. + Alle servere + Alle dine kontakter, samtaler og filer vil blive krypteret og uploadet i bidder til konfigurerede XFTP -relæer. + Alle dine kontakter forbliver forbundet. + Alle dine kontakter forbliver forbundet. Profilopdatering sendes til dine kontakter. + Tilslut dig allerede! + Deltager allerede i gruppen! + altid + Altid + Altid på + Brug altid privat routing. + Brug altid relæ + og %d andre begivenheder + Android Keystore bruges til sikkert at opbevare adgangssæt - det giver Notification Service mulighed for at arbejde. + Android Keystore vil blive brugt til sikkert at gemme adgangssæt, når du genstarter appen eller skifter adgangssætning - det giver mulighed for at modtage meddelelser. + En tom chatprofil med det angivne navn oprettes, og appen åbnes som sædvanligt. + En ny tilfældig profil deles. + En anden grund + Svaropkald + Alle kan være vært for servere. + APP + App løber altid i baggrunden + App Build: %s + App kan kun modtage meddelelser, når den kører, ingen baggrundstjeneste startes + Backup af appdata + Migration af appdata + Åbn chat + Åbn ny chat + Åben gruppe + Åbn ny gruppe + Ugyldigt link + Kontroller at SimpleX-linket er korrekt. + Åbner databasen… + Databasemigrering er i gang.\nDet kan tage et par minutter. + Ugyldig filsti + Du delte en ugyldig filsti. Rapportér problemet til appudviklerne. + Visningen gik ned + forbundet + fejl + forbinder + Du har forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt. + Forsøger at oprette forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt (fejl: %1$s). + Forsøger at oprette forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt. + slettet + markeret som slettet + %d beskeder markeret som slettet + modereret af %s + Kun du og moderatorer ser det + Kun afsender og moderatorer ser det + arkiveret rapport + arkiveret rapport af %s + blokeret + blokeret af administrator + %d beskeder blokeret + %d beskeder blokeret af administrator + afsendelse af filer understøttes endnu ikke + Modtagelse af filer understøttes endnu ikke + du + ukendt beskedformat + ugyldigt beskedformat + LEVENDE + modereret + videresendt + gemt + gemt fra %s + ugyldig chat + ugyldige data + fejl ved visning af besked + fejl ved visning af indhold + Dekrypteringsfejl + Fejl ved genforhandling af kryptering + ende-til-ende-kryptering.]]> + ende-til-ende-kryptering med perfekt fremadrettet hemmeligholdelse, afvisning og gendannelse efter indbrud.]]> + kvanteresistent e2e-kryptering med perfekt fremadrettet hemmeligholdelse, afvisning og gendannelse efter indbrud.]]> + Denne chat er beskyttet af end-to-end-kryptering. + Denne chat er beskyttet af kvanteresistent end-to-end-kryptering. + Private noter + forbindelse %1$d + forbindelse etableret + inviteret til at oprette forbindelse + bedt om at oprette forbindelse + forbinder… + du delte et engangslink + Du delte et engangslink inkognito + via gruppelink + inkognito via gruppelink + via link til kontaktadresse + inkognito via link til kontaktadresse + via engangslink + inkognito via engangslink + SimpleX kontaktadresse + SimpleX engangsinvitation + SimpleX gruppe link + SimpleX-kanallink + via %1$s + SimpleX-links + Beskrivelse + Fuldt link + Via browseren + Åbning af linket i browseren kan reducere forbindelsens privatliv og sikkerhed. Upålidelige SimpleX-links vil være røde. + Spam + Upassende indhold + Overtrædelse af retningslinjer for fællesskabet + Upassende profil + Fejl under lagring af SMP-servere + Fejl ved lagring af XFTP-servere + Sørg for, at SMP-serveradresserne er i korrekt format, linjeseparerede og ikke duplikerede. + Sørg for, at XFTP-serveradresserne er i korrekt format, linjeseparerede og ikke duplikerede. + Fejl ved indlæsning af SMP-servere + Fejl ved indlæsning af XFTP-servere + Fejl ved opdatering af netværkskonfigurationen + Chatten kunne ikke indlæses + Kunne ikke indlæse chats + Opdater appen, og kontakt udviklerne. + Fejl ved oprettelse af profil! + Duplikeret visningsnavn! + Du har allerede en chatprofil med det samme visningsnavn. Vælg et andet navn. + Ugyldigt visningsnavn! + Dette visningsnavn er ugyldigt. Vælg et andet navn. + Fejl ved skift af profil! + Fejl ved lagring af servere + Ingen beskedservere. + Ingen servere til at modtage beskeder. + Ingen servere til routing af private beskeder. + Ingen medie- og filservere. + Ingen servere til at sende filer. + Ingen servere til at modtage filer. + For chatprofil %s: + Fejl i serverkonfigurationen. + Fejl ved accept af betingelser + Spam + Indholdet overtræder brugsbetingelserne + Forbindelsestimeout + Forbindelsesfejl + Tjek venligst din netværksforbindelse med %1$s og prøv igen. + Serveradressen er ikke kompatibel med netværksindstillingerne: %1$s. + Serverversionen er ikke kompatibel med din app: %1$s. + Timeout for privat routing + Fejl ved privat routing + Ingen privat routingsession + Fejl ved forbindelse til videresendelsesserveren %1$s. Prøv venligst senere. + Videresendelsesserveradressen er ikke kompatibel med netværksindstillingerne: %1$s. + Videresendelsesserverversionen er inkompatibel med netværksindstillingerne: %1$s. + Videresendelsesserveren %1$s kunne ikke oprette forbindelse til destinationsserveren %2$s. Prøv senere. + Destinationsserveradressen for %1$s er ikke kompatibel med indstillingerne for videresendelsesserveren %2$s. + Destinationsserverversionen af %1$s er inkompatibel med videresendelsesserveren %2$s. + Prøv senere. + Fejl ved afsendelse af besked + Fejl ved videresendelse af beskeder + Fejl ved oprettelse af besked + Fejl ved oprettelse af rapport + Fejl ved indlæsning af oplysninger + Fejl ved tilføjelse af medlem(mer) + Fejl ved tilmelding til gruppen + Fejl ved accept af medlem + Fejl ved sletning af chat med medlem + Kan ikke modtage fil + Afsenderen annullerede filoverførslen. + Ukendte servere! + Uden Tor eller VPN vil din IP-adresse være synlig for disse XFTP-relæer:\n%1$s. + Fejl ved modtagelse af fil + Fejl ved oprettelse af adresse + Kontakten findes allerede + Du er allerede forbundet til %1$s. + Ugyldigt forbindelseslink + Kontroller venligst, at du har brugt det korrekte link, eller bed din kontaktperson om at sende dig et nyt. + Ikke-understøttet forbindelseslink + Dette link kræver en nyere appversion. Opgrader appen, eller bed din kontaktperson om at sende et kompatibelt link. + Forbindelsesfejl (AUTH) + Medmindre din kontaktperson har slettet forbindelsen, eller dette link allerede er i brug, kan det være en fejl - rapporter det.\nFor at oprette forbindelse skal du bede din kontaktperson om at oprette et nyt forbindelseslink og kontrollere, at du har en stabil netværksforbindelse. + Forbindelse blokeret + Forbindelsen er blokeret af serveroperatøren:\n%1$s. + Ikke-leverede beskeder + Forbindelsen har nået grænsen for antal ikke-leverede beskeder. Din kontaktperson er muligvis offline. + Fejl ved accept af kontaktanmodning + Fejl ved afvisning af kontaktanmodning + Afsenderen har muligvis slettet forbindelsesanmodningen. + Fejl ved sletning af kontakt + Fejl ved sletning af gruppe + Fejl ved sletning af private noter + Fejl ved sletning af kontaktanmodning + Fejl ved sletning af ventende kontaktforbindelse + Fejl ved ændring af adresse + Fejl ved afbrydelse af adresseændring + Fejl ved synkronisering af forbindelse + Testen mislykkedes ved trin %s. + Server kræver tilladelse til at oprette køer, tjek adgangskode + Server kræver tilladelse til at uploade, tjek adgangskode + Muligvis er certifikatets fingeraftryk i serveradressen forkert + Fejl ved angivelse af adresse + Fejl + Forbinde + Afbryde + Opret kø + Sikker kø + Slet kø + Opret fil + Upload fil + Hent fil + Sammenlign fil + Slet fil + Fejl ved sletning af brugerprofil + Der opstod en fejl ved opdatering af brugerens privatliv + Langsom funktion + Funktionens udførelse tager for lang tid: %1$d sekunder: %2$s + Der opstod en fejl ved opdatering af chatlisten. + Fejl ved oprettelse af chatliste + Fejl ved indlæsning af chatlister + Fejl ved åbning af chatten + Fejl ved åbning af gruppe + Fejl ved ændring af profil + Øjeblikkelige notifikationer + Øjeblikkelige notifikationer! + Øjeblikkelige notifikationer er deaktiveret! + løber SimpleX i baggrunden i stedet for at bruge push-notifikationer.]]> + Det kan deaktiveres via indstillinger – notifikationer vises stadig, mens appen løber.]]> + Tillad det i den næste dialogboks for at modtage beskeder med det samme.]]> + Batterioptimering er aktiv og deaktiverer baggrundstjenester og periodiske anmodninger om nye beskeder. Du kan genaktivere dem via indstillinger. + Periodiske meddelelser + Periodiske notifikationer er deaktiveret! + Appen henter nye beskeder med jævne mellemrum – den bruger et par procent af batteriet om dagen. Appen bruger ikke push-notifikationer – data fra din enhed sendes ikke til serverne. + Åbn appindstillinger + Deaktiver notifikationer + SimpleX kan ikke løbe i baggrunden. Du modtager kun notifikationer, når appen kører. + Appens batteriforbrug / Ubegrænset i appindstillingerne.]]> + Ingen baggrundsopkald + Appen kan lukkes i baggrunden efter 1 minut. + Appens batteriforbrug / Ubegrænset i appindstillingerne.]]> + Adgangsudtryk er nødvendig + For at modtage notifikationer, indtast databaseadgangskoden + Kan ikke initialisere databasen + Databasen fungerer ikke korrekt. Tryk for at få mere at vide. + Xiaomi-enheder: Aktiver venligst Autostart i systemindstillingerne for at notifikationer kan fungere.]]> + SimpleX Chat-tjeneste + Modtager beskeder… + Lydopkald + Videoopkald + Afslut opkald + Skjul + SimpleX Chat beskeder + SimpleX Chat opkalder + Notifikationstjeneste + Vis forhåndsvisning + Forhåndsvisning af notifikationer + Løber når appen er åben + Starter periodisk + Tjekker nye beskeder hvert 10. minut i op til 1 minut + Baggrundstjenesten løber altid – der vises meddelelser, så snart beskederne er tilgængelige. + Beskedtekst + Kontaktnavn + Skjult + Vis kontakt og besked + Vis kun kontakt + Skjul kontakt og besked + Kontakt skjult: + ny besked + Ny kontaktanmodning + Forbundet + Fejl ved visning af notifikation. Kontakt udviklerne. + SimpleX Lås + For at beskytte dine oplysninger skal du aktivere SimpleX Lås.\nDu vil blive bedt om at fuldføre godkendelse, før denne funktion aktiveres. + Tænd + SimpleX lås tilstand + Systemgodkendelse + Indtastning af adgangskode + Godkendelse mislykkedes + Du kunne ikke verificeres. Prøv igen. + Ingen app-adgangskode + Indtast adgangskode + Nuværende adgangskode + Skift adgangskode + Godkend + Straks + %d sekunder + %d minutter + Husk eller opbevar den sikkert - der er ingen måde at gendanne en mistet adgangskode på! + SimpleX Lås aktiveret + Du skal godkende, når du starter eller genoptager appen efter 30 sekunder i baggrunden. + Lås op + Log ind med dine legitimationsoplysninger + Aktivér SimpleX Lås + Deaktiver SimpleX Lås + Bekræft dine legitimationsoplysninger + Godkendelse ikke tilgængelig + Enhedsgodkendelse er ikke aktiveret. Du kan aktivere SimpleX Lås via Indstillinger, når du har aktiveret enhedsgodkendelse. + Enhedsgodkendelse er deaktiveret. SimpleX Lås slås fra. + Stop chatten + Åbn chatkonsol + Skift chatprofiler + Åbn migreringsskærmen + SimpleX Lås er ikke aktiveret! + Du kan aktivere SimpleX Lås via Indstillinger. + Fejl ved levering af besked + Advarsel om levering af besked + Denne kontakt har højst sandsynligt slettet forbindelsen med dig. + Ingen besked + Denne besked er blevet slettet eller ikke modtaget endnu. + Rapportér årsag? + Arkivér rapport? + Arkivér %d rapporter? + Arkivér alle rapporter? + Rapporten vil blive arkiveret for dig. + For mig + Til alle moderatorer + Fejl: %1$s + Forkert nøgle eller ukendt forbindelse - sandsynligvis er denne forbindelse slettet. + Kapacitet overskredet - modtageren modtog ikke tidligere sendte beskeder. + Netværksproblemer - beskeden udløb efter mange forsøg på at sende den. + Destinationsserverfejl: %1$s + Videresendelsesserver: %1$s\nFejl: %2$s + Videresendelsesserver: %1$s\nDestinationsserverfejl: %2$s + Serveradressen er ikke kompatibel med netværksindstillingerne. + Serverversionen er inkompatibel med netværksindstillingerne. + Forkert nøgle eller ukendt fil-chunk-adresse - filen er sandsynligvis slettet. + Filen er blokeret af serveroperatøren:\n%1$s. + Filen blev ikke fundet - filen blev sandsynligvis slettet eller annulleret. + Filserverfejl: %1$s + Svar + Dele + Kopi + Gem + Rediger + Info + Søg + Arkivé + Arkivér rapport + Arkiver rapporterne + Slet rapport + Sendt besked + Modtaget besked + Historie + Ingen historik + Som svar til + Gemt + Videresendt + Gemt fra + Videresendt fra + Modtager(e) kan ikke se, hvem denne besked er fra. + Levering + Ingen leveringsoplysninger + Slet + Afsløre + Skjul + Moderé + Anmeld + Vælg + Udvid + Slette besked? + Slet %d beskeder? + Beskeden vil blive slettet - dette kan ikke fortrydes! + Beskeder vil blive slettet - dette kan ikke fortrydes! + Beskeden vil blive markeret til sletning. Modtageren(e) vil kunne se denne besked. + Beskeder vil blive markeret til sletning. Modtageren(e) vil kunne se disse beskeder. + Slet medlemsbesked? + Slet %d beskeder fra medlemmer? + Beskeden vil blive slettet for alle medlemmer. + Beskederne vil blive slettet for alle medlemmer. + Beskeden vil blive markeret som modereret for alle medlemmer. + Beskederne vil blive markeret som modereret for alle medlemmer. + Slet for mig + For alle + Stop fil + Stoppe afsendelse af fil? + Afsendelse af filen vil blive stoppet. + Stoppe modtagelse af fil? + Modtagelsen af filen vil blive stoppet. + Stop + Tilbagekald fil + Tilbagekald fil? + Filen vil blive slettet fra serverne. + Tilbagekald + Videresend + Hent + Liste + Besked videresendt + Ingen direkte forbindelse endnu, beskeden er videresendt af administratoren. + Medlem inaktivt + Beskeden kan blive leveret senere, hvis medlemmet bliver aktivt. + redigeret + sendt + uautoriseret afsendelse + afsendelse mislykkedes + ulæst + Velkommen %1$s! + Velkomst! + Denne tekst er tilgængelig i indstillinger + Chats + Indstillinger + forbinder… + send for at oprette forbindelse + Åben for at deltage + Du er inviteret til gruppen + Deltag som %s + afvist + forbinder… + Tryk for at starte en ny chat + Chat med udviklerne + Du har ingen chats + Indlæser chats… + Ingen filtrerede chats + Ingen chats på listen %s. + Ingen ulæste chats + Ingen chats + Ingen chats fundet + Tryk for at oprette forbindelse + Åbn for at oprette forbindelse + Åben for at acceptere + kontakten skal acceptere… + Forbinde med %1$s? + Søg eller indsæt SimpleX-link + Tryk på Opret SimpleX-adresse i menuen for at oprette den senere. + Ingen chat valgt + Intet er valgt + Valgte %d + Videresend %1$s besked(er)? + Intet at videresende! + Videresende beskeder uden filer? + Beskeder blev slettet, efter du valgte dem. + Hent + Favoritter + Kontakter + Grupper + Virksomheder + Noter + Rapporter + Rapport: %s + %d rapporter + Medlemsrapporter + %d beskeder + %d chats med medlemmer + %d chat(er) + Tryk på Opret forbindelse for at chatte + Tryk på Opret forbindelse for at sende anmodningen + Accepter kontaktanmodning + Dit kontaktbanner + Tryk på Deltag i gruppen + Din gruppe + Gruppe + Forretningsforbindelse + Din forretningskontakt + Del besked… + Del medier… + Del fil… + Videresend besked… + Videresend beskeder… + Kan ikke sende besked + De valgte chatpræferencer forbyder denne besked. + Vedhæft + Kontekstikon + Annuller billedforhåndsvisning + Annuller filforhåndsvisning + For mange billeder! + For mange videoer! + Kun 10 billeder kan sendes ad gangen + Kun 10 videoer kan sendes ad gangen + Afkodningsfejl + Billedet kan ikke afkodes. Prøv et andet billede, eller kontakt udviklerne. + Videoen kan ikke afkodes. Prøv en anden video, eller kontakt udviklerne. + Filer og medier forbudt! + Kun gruppeejere kan aktivere filer og medier. + Send direkte besked for at oprette forbindelse + Videresender %1$s beskeder + Gemmer %1$s beskeder + SimpleX links er ikke tilladt + Filer og medier er ikke tilladt + Talebeskeder er ikke tilladt + Besked + Beskeden er for stor! + Reducer venligst beskedstørrelsen og send den igen. + Reducer venligst beskedstørrelsen, eller fjern mediet, og send igen. + Du kan kopiere og reducere beskedstørrelsen for at sende den. + Rapportér spam: Kun gruppemoderatorer vil se det. + Rapportér medlemsprofil: Kun gruppemoderatorer vil se den. + Rapportér overtrædelse: Kun gruppemoderatorer vil se det. + Rapportér indhold: Kun gruppemoderatorer vil se det. + Rapportér andet: Kun gruppemoderatorer vil se det. + Rapport sendt til moderatorer + Du kan se dine rapporter i Chat med administratorer. + Deltag i gruppen + Forbinde + Send kontaktanmodning? + efter din anmodning er accepteret.]]> + Send anmodning uden besked + Send anmodning + Du kan ikke sende beskeder! + kontakten er ikke klar + anmodningen er sendt + kontakt slettet + ikke synkroniseret + kontakt deaktiveret + du er observatør + Kontakt gruppeadministratoren. + anmodning om tilmelding afvist + gruppen er slettet + fjernet fra gruppen + du forlod + kan ikke sende beskeder + du er observatør + gennemgået af administratorer + medlemmet har en gammel version + Billede + Venter på billede + Bedt om at modtage billedet + Billede sendt + Venter på billede + Billedet modtages, når din kontaktperson har uploadet det. + Billedet modtages, når din kontakt er online. Vent eller tjek senere! + Billede gemt i Galleri + Video + Venter på video + Bedt om at modtage videoen + Video sendt + Venter på video + Videoen modtages, når din kontaktperson har uploadet den. + Video modtages, når din kontakt er online. Vent eller tjek senere! + Fil + Stor fil! + Din kontakt sendte en fil, der er større end den aktuelt understøttede maksimale størrelse (%1$s). + Den maksimalt understøttede filstørrelse er i øjeblikket %1$s. + Venter på fil + Filen modtages, når din kontaktperson har uploadet den. + Filen modtages, når din kontakt er online. Vent eller tjek senere! + Fil gemt + Filen blev ikke fundet + Fejl ved lagring af fil + Indlæser filen + Vent venligst, mens filen indlæses fra den tilknyttede mobil. + Filfejl + Midlertidig filfejl + Åbn med %s + Talebesked + Talebesked (%1$s) + Talebesked… + Notifikationer + Deaktiver automatisk sletning af beskeder? + Ændre automatisk sletning af beskeder? + Beskeder i denne chat vil aldrig blive slettet. + Denne handling kan ikke fortrydes - beskeder sendt og modtaget i denne chat tidligere end valgt vil blive slettet. + Deaktiver sletning af beskeder + Slet chatbeskeder fra din enhed. + forbinde + åbn + besked + opkald + søg + video + Slet kontakt? + Kontakt og alle beskeder vil blive slettet - dette kan ikke fortrydes! + Kontakten vil blive slettet - dette kan ikke fortrydes! + Fortsæt samtalen + Slet kun samtalen + Bekræft sletning af kontakt? + Slet og underret kontakt + Slet uden meddelelse + Slet kontakt + Samtalen er slettet! + Du kan sende beskeder til %1$s fra arkiverede kontakter. + Kontakt slettet! + Du kan stadig se samtalen med %1$s på listen over chats. + Angiv kontaktnavn… + Angiv chatnavn… + Forbundet + Afbrudt + Fejl + Indtil + Ændre modtageradresse? + Modtageradressen vil blive ændret til en anden server. Adresseændringen vil blive gennemført, når afsenderen er online. + Genforhandle kryptering? + Krypteringen fungerer, og den nye krypteringsaftale er ikke påkrævet. Det kan resultere i forbindelsesfejl! + Genforhandle + Rette forbindelse? + Forbindelsen kræver genforhandling af krypteringen. + Lav + Genforhandling af kryptering i gang. + Vis sikkerhedsnummer + Verificier sikkerhedsnummer + Brug inkognito -profil + Send besked + 4 nye interface -sprog + Tillad kun filer og medier, hvis din kontakt tillader dem. + Lad dine kontakter sende filer og medier. + Udseende + App krypterer nye lokale filer (undtagen videoer). + Appikon + Anvende + Ansøg på + App adgangskode + App adgangskode + App-adgangskode erstattes med selvdestruktionsskode. + App -session + App -tema + App værktøjslinjer + Appopdatering er downloadet + App version + App version: v%s + Arabisk, bulgarsk, finsk, hebraisk, thailandsk og ukrainsk - takket være brugerne og Weblate. + Arkiv og upload + Arkivkontakter for at chatte senere. + Arkiverede kontakter + Arkiveringsdatabase + Spørge + forsøg + Lyd- og videoopkald + lydopkald + lydopkald (ikke E2E krypteret) + Lyd fra + Lyd på + Audio & videoopkald + Audio/videoopkald + Audio/videoopkald er forbudt. + Autentificering annulleret + forfatter + Auto-accept + Auto-accept-kontaktanmodninger + Auto-accept-billeder + \nFås i v5.1 + Tilbage + Baggrund + Dårlig skrivebordsadresse + Dårlig besked hash + Dårlig besked hash + Dårligt besked -id + Dårligt besked -id + Beta + Bedre opkald + Bedre grupper + Bedre grupper ydeevne + Bedre meddelelsesdatoer. + Bedre beskeder + Bedre privatlivets fred og sikkerhed + Bedre sikkerhed ✅ + Bedre brugeroplevelse + Bio: + Bio for stor + Sort + Blok + blokeret + Blokeret af admin + blokeret %s + Blok for alle + Blokergruppemedlemmer + Blok medlem + Blok medlem? + Blokermedlem for alle? + Blokermedlem for alle? + Bluetooth + Sløret + Slør for bedre privatliv. + Slør medier + fed + Bot + Både dig og din kontakt kan tilføje meddelelsesreaktioner. + Både dig og din kontakt kan irreversibelt slette sendte beskeder. (24 timer) + Både dig og din kontakt kan foretage opkald. + Både dig og din kontakt kan sende forsvindende beskeder. + Både dig og din kontakt kan sende filer og medier. + Både dig og din kontakt kan sende stemmemeddelelser. + Forretningsadresse + Forretningschats + Med chatprofil (standard) eller ved forbindelse (beta). + Ved at bruge simplex chat accepterer du:\n- Send kun lovligt indhold i offentlige grupper.\n- Respekter andre brugere - ingen spam. + Opkald allerede afsluttet! + Opkald sluttede + Opkald sluttede %1$s + Opkaldsfejl + Ringer … + ring i gang + Ring i gang + Opkald + Opkald på låseskærmen: + Kalder forbudt! + Kamera + Kamera + Kamera og mikrofon + Kamera ikke tilgængeligt + Annuller + Annulleret %s + Annuller linkeksempel + Annuller live -meddelelsen + Annuller migration + Kan ikke få adgang til Keystore for at gemme databaseadgangskode + Kan ikke ringe til kontakten + Kan ikke ringe til gruppemedlem + Kan ikke ændre profil + Kan ikke invitere kontakt! + Kan ikke invitere kontakter! + Kan ikke sende besked til gruppemedlem + Katalansk, indonesisk, rumænsk og vietnamesisk - takket være vores brugere! + med kun en kontakt - Del personligt eller via enhver messenger.]]> + ende-til-ende krypteret med sikkerhed efter kvantet i direkte meddelelser.]]> + for hver chatprofil, du har i appen.]]> + til hvert kontakt- og gruppemedlem.\n Bemærk : Hvis du har mange forbindelser, kan dit batteri og trafikforbrug være væsentligt højere, og nogle forbindelser kan mislykkes.]]> + Tilføj kontakt: Sådan opretter du et nyt invitationslink eller opretter forbindelse via et link, du har modtaget.]]> + Bedst til batteri. Du modtager kun meddelelser, når appen løber (ingen baggrundstjeneste).]]> + Opret gruppe: At oprette en ny gruppe.]]> + godt til batteri . App kontrollerer meddelelser hvert 10. minut. Du kan gå glip af opkald eller presserende beskeder.]]> + Bemærk: Meddelelse og filrelæer er tilsluttet via SOCKS -proxy. Opkald og afsendelse af link -forhåndsvisninger Brug direkte forbindelse.]]> + Bemærk : Brug af den samme database på to enheder vil bryde dekryptering af meddelelser fra dine forbindelser som en sikkerhedsbeskyttelse.]]> + Bemærk : Du vil ikke være i stand til at gendanne eller ændre adgangskode, hvis du mister den.]]> + bruger mere batteri ! App løber altid i baggrunden - underretninger vises øjeblikkeligt.]]> + ADVARSEL : Arkivet slettes.]]> + Forbinde + Forbind automatisk + Opret forbindelse direkte? + tilsluttet + tilsluttet + tilsluttet + Tilsluttet + Forbundet desktop + Forbundet mobil + Forbundet servere + Forbundet til desktop + Forbundet til mobil + Forbind hurtigere! 🚀 + Tilslutning + Forbindelse… + Forbindelse + Forbindelse (accepteret) + Forbindelse (annonceret) + Tilslutning af opkald … + Tilslutning af opkald + Tilslutning (introduceret) + 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 be6896d932..e038207801 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -13,14 +13,14 @@ Verbunden Fehler Verbinde - Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird. - Beim Versuch, die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %1$s). - Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird. + Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten dieser Verbindung genutzt wird. + Beim Versuch, die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %1$s). + Versuche eine Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten dieser Verbindung genutzt wird. 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,9 +58,9 @@ 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 besteht bereits Sie sind bereits mit %1$s verbunden. @@ -69,7 +69,7 @@ Verbindungsfehler (AUTH) Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln – bitte melden Sie ihn uns. \nBitten Sie Ihren Kontakt darum, einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können, und stellen Sie sicher, dass Sie eine stabile Netzwerkverbindung haben. - Fehler beim Akzeptieren der Kontaktanfrage + Fehler beim Annehmen der Kontaktanfrage Der Absender hat möglicherweise die Verbindungsanfrage gelöscht. Fehler beim Löschen des Kontakts Fehler beim Löschen der Gruppe @@ -77,8 +77,8 @@ 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. - Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig. + Der Server erfordert zum Erstellen von Warteschlangen eine Autorisierung. Bitte überprüfen Sie das Passwort. + Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein. Verbinde Erzeuge Warteschlange Sichere Warteschlange @@ -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 @@ -271,7 +271,7 @@ QR-Code scannen.]]> In mobiler App öffnen“ und dann auf „Verbinden“.]]> - Die Verbindungsanfrage akzeptieren? + Die Verbindungsanfrage annehmen? Wenn Sie ablehnen, wird der Absender NICHT benachrichtigt. Akzeptieren Inkognito akzeptieren @@ -292,10 +292,10 @@ Stummschaltung aufheben Sie haben Ihren Kontakt eingeladen - Sie haben die Verbindung akzeptiert + Sie haben die Verbindung angenommen Ausstehende Verbindung löschen? Der Kontakt, mit dem Sie diesen Link geteilt haben, kann sich NICHT verbinden! - Die von Ihnen akzeptierte Verbindung wird abgebrochen! + Die von Ihnen angenommene Verbindung wird abgebrochen! Ihr Kontakt ist noch nicht verbunden! Ihr Kontakt muss online sein, damit die Verbindung hergestellt werden kann. @@ -326,7 +326,7 @@ Dieser Link ist kein gültiger Verbindungslink! Verbindungsanfrage gesendet! Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach! - Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach! + Sie werden verbunden, sobald Ihre Verbindungsanfrage angenommen wird. Bitte warten oder schauen Sie später nochmal nach! Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach! den QR-Code während eines Videoanrufs anzeigen oder einen Einladungslink über einen anderen Kanal mit Ihrem Kontakt teilen.]]> Ihr Chat-Profil wird @@ -355,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 @@ -544,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 @@ -585,30 +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. Dateien und Medien löschen? - Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien 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! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, 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 @@ -662,7 +661,7 @@ 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. @@ -728,7 +727,7 @@ Eingeladen Verbindung (erstellt) Verbinde (nach einer Einladung) - Verbindung (akzeptiert) + Verbindung (angenommen) Verbindung (angekündigt) Verbunden Vollständig @@ -842,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 @@ -913,8 +912,8 @@ 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. @@ -923,8 +922,8 @@ Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten. Das Senden von verschwindenden Nachrichten nicht erlauben. Verschwindende Nachrichten sind nicht erlaubt. - Mitglieder können verschwindende Nachrichten senden. - Fügen Sie Server durch Scannen der QR-Codes hinzu. + Mitglieder können verschwindende Nachrichten versenden. + Server durch Scannen von QR-Codes hinzufügen. Verschwindende Nachrichten Übernehmen Einen Tag festlegen @@ -937,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 @@ -1015,7 +1014,7 @@ Moderieren Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet. Sie sind Beobachter - Sie können keine Nachrichten versenden! + Sie sind Beobachter Beobachter Anfängliche Rolle Nachricht des Mitglieds löschen\? @@ -1078,27 +1077,27 @@ 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 Fehler beim Laden der SMP-Server - Bitte das Passwort überprüfen – für den Upload benötigt der Server eine Berechtigung + Der Server erfordert zum Hochladen eine Autorisierung. Bitte überprüfen Sie das Passwort. Datei herunterladen Datei vergleichen Datei löschen @@ -1160,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 @@ -1203,7 +1202,7 @@ Freunde einladen Lassen Sie uns über SimpleX Chat schreiben Profil-Aktualisierung wird an Ihre Kontakte gesendet. - Einstellungen von „automatisch akzeptieren“ speichern + SimpleX-Adress-Einstellungen speichern Einstellungen speichern\? Die Adresse mit Kontakten teilen\? Teilen beenden @@ -1247,7 +1246,7 @@ 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 + Zugangscode festlegen Reaktionen auf Nachrichten sind nicht erlaubt. Fehler beim Laden von Details Empfangene Nachricht @@ -1305,7 +1304,7 @@ 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 nicht schwerwiegende Fehler aufgetreten: @@ -1323,7 +1322,7 @@ Wechsel der Empfängeradresse beenden? Dateien und Medien sind nicht erlaubt! Nur Gruppenbesitzer können Dateien und Medien aktivieren. - Mitglieder können Dateien und Medien senden. + 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 @@ -1454,8 +1453,8 @@ 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. - Direktnachricht senden - Direkt miteinander verbunden + Zum Verbinden senden + Angefragte Verbindung Erweitern Verbindungsanfrage wiederholen? Gelöschter Kontakt @@ -1466,7 +1465,7 @@ Profil erstellen %s und %s Ihrer Gruppe beitreten? - %1$s.]]> + %1$s.]]> Das ist Ihr eigener Einmal-Link! %d Nachrichten als gelöscht markiert Gruppe besteht bereits! @@ -1484,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 @@ -1642,7 +1641,7 @@ Interner Fehler %s wird eine nicht unterstützte Version verwendet. Bitte stellen Sie sicher, dass beide Geräte die selbe Version nutzen]]> %s ist besetzt]]> - Ehemaliges Mitglied %1$s + Mitglied %1$s Die Ausführung dieser Funktion dauert zu lange: %1$d Sekunden: %2$s Langsame Funktion Langsame API-Aufrufe anzeigen @@ -1653,8 +1652,8 @@ Private Notizen Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Private Notizen entfernen? - %s wurde blockiert - %s wurde freigegeben + hat %s blockiert + hat %s freigegeben Sie haben %s blockiert Sie haben %s freigegeben Mitglied für Alle blockieren? @@ -1662,11 +1661,11 @@ Zum Verbinden den Link einfügen! Mit reduziertem Akkuverbrauch. Aktueller Nachrichtenverlauf und verbesserter Gruppenverzeichnis-Bot. - In der Suchleiste werden nun auch Einladungslinks akzeptiert. + In der Suchleiste werden nun auch Einladungslinks angenommen. Für Alle freigeben Mitglied für Alle freigeben? wurde blockiert - ist vom Administrator blockiert worden + wurde vom Administrator blockiert wurde vom Administrator blockiert Für Alle blockiert Erstellt um @@ -1715,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 @@ -1791,7 +1790,7 @@ SimpleX-Links sind nicht erlaubt Sprachnachrichten sind nicht erlaubt SimpleX-Links - Mitglieder können SimpleX-Links senden. + Mitglieder können SimpleX-Links versenden. Administratoren Alle Mitglieder Aktiviert für @@ -1880,7 +1879,7 @@ Bild entfernen Wiederholen Skalieren - Default-Design einstellen + Default-Design festlegen Wallpaper-Akzent Wallpaper-Hintergrund Anwenden auf @@ -1907,11 +1906,11 @@ 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 empfangen + Dateien sicher herunterladen Mit reduziertem Akkuverbrauch. Keine Information Debugging-Zustellung - Nachrichten-Warteschlangen-Information + Information Nachrichtenwarteschlange Server-Warteschlangen-Information: %1$s \n \nZuletzt empfangene Nachricht: %2$s @@ -2025,8 +2024,7 @@ Zoom SMP-Server Informationen zeigen für - Beginnend mit %s. -\nAlle Daten werden nur auf Ihrem Gerät gespeichert. + Beginnend mit %s.\nAlle Daten werden nur auf Ihrem Gerät gespeichert. Statistiken Transport-Sitzungen Hochgeladen @@ -2052,7 +2050,7 @@ 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 + Medien verpixeln Weich Stark Mittel @@ -2091,10 +2089,10 @@ Einstellungen Die Nachrichten werden für alle Gruppenmitglieder gelöscht. Die Nachrichten werden für alle Mitglieder als moderiert markiert. - %d Nachrichten der Mitglieder löschen? + 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. - %d ausgewählt + Es wurden %d ausgewählt Es wurde Nichts ausgewählt Auswählen Einladen @@ -2140,7 +2138,7 @@ 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. + Ihre Verbindung wurde auf %s verschoben, aber während des Profil-Wechsels trat ein Fehler auf. Stellen Sie sicher, dass die Proxy-Konfiguration richtig ist. Fehler beim Speichern des Proxys Passwort @@ -2156,7 +2154,7 @@ Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten. Es gibt nichts zum Weiterleiten! %1$d andere(r) Datei-Fehler. - %1$s Nachricht(en) weiterleiten? + 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? @@ -2193,7 +2191,7 @@ Fehler in der Server-Konfiguration. Für das Chat-Profil %s: Keine Medien- und Dateiserver. - Keine Server für den Empfang von Dateien. + 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. @@ -2248,7 +2246,7 @@ Der Server-Betreiber wurde geändert. Das Server-Protokoll wurde geändert. Transparenz - Flux aktivieren + 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 @@ -2259,7 +2257,7 @@ Adress- oder Einmal-Link? App-Symbolleiste Verpixeln - nur mit einem Kontakt genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger.]]> + nur mit einem Kontakt genutzt werden - teilen Sie ihn nur persönlich oder über einen beliebigen Messenger.]]> %s.]]> %s.]]> Die Nutzungsbedingungen wurden akzeptiert am: %s @@ -2286,11 +2284,11 @@ Ende-zu-Ende-verschlüsselt versendet. In Direktnachrichten sogar mit Post-Quantum-Security.]]> Team-Mitglieder aufnehmen Freunde aufnehmen - Einladung akzeptiert + Einladung angenommen Geschäftliche Adresse Geschäftliche Chats Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf. - Die App läuft immer im Hintergrund ab + App läuft dauerhaft im Hintergrund In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. Kein Hintergrund-Service Nachrichten alle 10 Minuten überprüfen @@ -2320,6 +2318,298 @@ Wenn mehr als ein Betreiber aktiviert ist, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert. Chat %1$s verbunden.]]> - Über Betreiber + Über die Betreiber SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen. - \ No newline at end of file + 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. + Annehmen + Server-Betreiber konfigurieren + Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich. + Verbindungs-Link wird nicht unterstützt + 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 + %d Chats mit Mitgliedern + %d Chat(s) + Meldung wurde an die Moderatoren gesendet + Sie haben dieses Mitglied angenommen + Überprüfung der Mitglieder vor der Aufnahme (\"Anklopfen\"). + Überprüfung der Mitglieder + alle + Aus + Als Beobachter übernehmen + Mitglied annehmen + Chats mit Mitgliedern + Chat mit Administratoren + Keine Chats mit Mitgliedern + Ablehnen + hat Sie angenommen + Chat mit einem Mitglied + %d Nachrichten + Mitglied wird der Gruppe beitreten. Annehmen? + Ein neues Mitglied will der Gruppe beitreten. + Überprüfung + Von Administratoren überprüft + Aufnahme von Mitgliedern festlegen + Speichern der Aufnahme-Einstellungen? + Sie können Ihre Meldungen im Chat mit den Administratoren sehen. + Chat mit Administratoren + %1$s angenommen + Als Mitglied übernehmen + Fehler beim Annehmen des Mitglieds + Aufnahme von Mitgliedern + Ausstehende Überprüfung + Chat mit einem Mitglied + Übernehmen + Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können. + Gruppe wurde gelöscht + Beitrittsanfrage abgelehnt + Aus der Gruppe entfernt + Sie haben die Gruppe verlassen + Kontakt deaktiviert + Nicht synchronisiert + Fehler beim Löschen des Chats + Kontakt nicht bereit + Sie können keine Nachrichten senden! + Chat löschen + Chat mit dem Mitglied löschen? + Mitglied ablehnen? + Es können keine Nachrichten gesendet werden + Kontakt gelöscht + Das Mitglied hat eine alte App-Version + Adresse aktualisieren + Kontaktanfrage annehmen + Nachricht hinzufügen + Ende-zu-Ende-Verschlüsselung geschützt.]]> + sobald Ihre Anfrage akzeptiert wurde.]]> + Verbinden + Kontakt sollte annehmen… + Fehler beim Ändern des Profils + Fehler beim Öffnen des Chats + Fehler beim Öffnen der Gruppe + Fehler bei der Ablehnung der Kontaktanfrage + Der Gruppe beitreten + Chat öffnen + Neuen Chat öffnen + Neue Gruppe öffnen + Zum Akzeptieren öffnen + Zum Verbinden öffnen + Zum Beitreten öffnen + Die Adresse wird gekürzt sein, und Ihr Profil wird über die Adresse geteilt. + Kontakt-Anfrage ablehnen + Anfrage wurde gesendet + Kontakt-Anfrage senden? + Anfrage senden + Anfrage ohne Nachricht senden + Wird nach der Verbindung an Ihren Kontakt gesendet. + Gruppen-Link aktualisieren? + Aktualisieren + Adresse aktualisieren? + Der Absender wird NICHT benachrichtigt. + Begrüßungsmeldung + Ihr Profil + Änderung des Profils nicht möglich + Wenn Sie nach dem Verbindungsversuch ein anderes Profil verwenden möchten, löschen Sie den Chat und verwenden Sie den Link erneut. + Chat mit Administratoren + Mit Mitgliedern chatten bevor sie beitreten. + Schneller miteinander verbinden! 🚀 + Weniger Datenverkehr in mobilen Netzen. + Sobald Sie auf Verbinden tippen, erhalten Sie sofort eine Nachricht. + Neue Gruppen-Rolle: Moderator + Keine private Routing-Sitzung + Zeitüberschreitung der privaten Routing-Sitzung + Protokoll Hintergrund-Zeitüberschreitung + Entfernt Nachrichten und blockiert Mitglieder. + Gruppenmitglieder überprüfen + Senden Sie Ihr privates Feedback an Gruppen. + TCP-Verbindung Hintergrund-Zeitüberschreitung + Profil wird geladen… + Bio: + Kurze Beschreibung: + Ihre Biografie: + Biografie zu lang + Beschreibung zu lang + Kontaktanfrage annehmen + Geschäftliche Verbindung + Gruppe + Verbinden tippen, um zu chatten + Verbinden tippen, um die Anfrage zu senden + Tippen, um der Gruppe beizutreten + Ihr geschäftlicher Kontakt + Ihr Kontakt + Ihre Gruppe + 4 neue Sprachen für die Bedienoberfläche + Katalanisch, Indonesisch, Rumänisch und Vietnamesisch - Dank unserer Nutzer! + Ihre Adresse erstellen + Verschwindende Nachrichten sind per Voreinstellung aktiviert. + Ihre Chats übersichtlich halten + Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben. + Ihre Adresse teilen + Verkürzte SimpleX-Adresse + Die Zeit bis zum Verschwinden wird nur für neue Kontakte eingestellt. + Ihre Adresse aktualisieren + Inkognito-Profil nutzen + Begrüßen Sie Ihre Kontakte 👋 + Alte Adresse teilen + Alten Link teilen + Der Link wird gekürzt sein, und das Gruppen-Profil wird über den Link geteilt. + Gruppen-Link aktualisieren + KONTAKTANFRAGEN VON GRUPPEN + Mitglied ist gelöscht - Anfrage kann nicht angenommen werden + Angefragte Verbindung von Gruppe %1$s + Diese Einstellung gilt für Ihr aktuelles Profil + Erlauben Sie Dateien und Medien nur dann, wenn es Ihr Kontakt ebenfalls erlaubt. + Erlauben Sie Ihren Kontakten Dateien und Medien zu senden. + Bot + Sowohl Sie, als auch Ihr Kontakt können Dateien und Medien senden. + Veraltete Optionen + In diesem Chat sind Dateien und Medien nicht erlaubt. + Nur Sie können Dateien und Medien senden. + Nur Ihr Kontakt kann Dateien und Medien senden. + Trackingfreien Link öffnen + Vollständigen Link öffnen + Bot für die Nutzung erlaubt + Das Senden von Dateien und Medien ist nicht erlaubt. + Link-Tracking entfernen + Verbinden tippen, um den Bot zu nutzen. + Um Befehle senden zu können, müssen Sie verbunden sein. + SimpleX Relais-Link + Fehler beim Markieren als gelesen + Fingerabdruck in der Zielserveradresse stimmt nicht mit dem Zertifikat überein: %1$s. + Fingerabdruck in der Weiterleitungsserveradresse stimmt nicht mit dem Zertifikat überein: %1$s. + Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein: %1$s. + Kein Abonnement + Sie sind nicht mit dem Server verbunden, der für den Empfang von Nachrichten dieser Verbindung genutzt wird (kein Abonnement). + 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 179c7fec52..9e38019c8b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -29,7 +29,7 @@ πάντα Αποδοχή Επιτρέψτε τα μηνύματα που εξαφανίζονται μόνο εάν το επιτρέπει η επαφή σας. - Επιτρέψτε στις επαφές σας να διαγράφουν μη αναστρέψιμα τα απεσταλμένα μηνύματα. + Επιτρέψτε στις επαφές σας να διαγράφουν μη αναστρέψιμα τα απεσταλμένα μηνύματα. (24 ώρες) Επιτρέψτε στις επαφές σας να στέλνουν μηνύματα που εξαφανίζονται. Επιτρέπονται τα φωνητικά μηνύματα μόνο εάν τα επιτρέπει η επαφή σας. Επιτρέψτε στις επαφές σας να σας καλέσουν. @@ -48,9 +48,9 @@ Οι διαχειριστές μπορούν να δημιουργήσουν τους συνδέσμους συμμετοχής σε ομάδες. Όλες οι συνομιλίες και τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί! Όλα τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί! Τα μηνύματα θα διαγραφούν ΜΟΝΟ για εσάς. - Επιτρέψτε τη μη αναστρέψιμη διαγραφή μηνυμάτων μόνο εάν σας το επιτρέπει η επαφή σας. + Επιτρέψτε τη μη αναστρέψιμη διαγραφή μηνυμάτων μόνο εάν το επιτρέπει η επαφή σας. (24 ώρες) Επιτρέπονται οι κλήσεις μόνο εάν η επαφή σας τις επιτρέπει. - Επιτρέψτε τη μη αναστρέψιμη διαγραφή των απεσταλμένων μηνυμάτων. + Επιτρέψτε τη μη αναστρέψιμη διαγραφή των απεσταλμένων μηνυμάτων. (24 ώρες) Να επιτρέπονται τα φωνητικά μηνύματα; Πάντα ενεργό Να χρησιμοποιείται πάντα αναμεταδότη @@ -230,7 +230,7 @@ Η αλλαγή διεύθυνσης θα ακυρωθεί. Θα χρησιμοποιηθεί η παλιά διεύθυνση παραλαβής. Ενεργές συνδέσεις Προχωρημένες ρυθμίσεις - Πρόσθετη προφορά + Πρόσθετος τόνος Προσθήκη επαφής Διακοπή αλλαγής διεύθυνσης Προχωρημένες ρυθμίσεις @@ -395,4 +395,46 @@ Καλύτερο για τη ζωή της μπαταρίας . Θα λαμβάνετε ειδοποιήσεις μόνο όταν εκτελείται η εφαρμογή (ΧΩΡΙΣ υπηρεσία παρασκηνίου).]]> Beta Καλύτερες κλήσεις - \ No newline at end of file + %1$d σφάλμα/τα αρχείου/ων:\n%2$s + 1 συζήτηση με ένα μέλος + 1 αναφορά + 1 χρόνος + Σχετικά με χειρηστές + Αποδοχή + Αποδοχή + Αποδοχή ως μέλος + Αποδοχή ως παρατηρητής + Αποδοχή όρων + Αποδοχή αιτήματος επαφής + Αποδοχή αιτήματος επαφής + αποδέχτηκε %1$s + Αποδεχούμενοι όροι + αποδέχτηκε τη πρόσκληση + σε αποδέχτηκε + Αποδοχή μέλους + Προστέθηκαν διακομιστές πολυμέσων και αρχείων + Προστέθηκε διακομιστής μυνημάτων + Προσθήκη φίλων + Πρόσθετος τόνος 2 + Προσθήκη λίστας + Προσθήκη μυνήματος + Διεύθυνση ή σύνδεσμος μιας χρήσης; + Ρυθμίσεις διεύθυνσης + Προσθήκη μέλη ομάδας + Προσθήκη στην λίστα + Πρόσθεσε τα μέλη της ομάδας σου στις συνομιλίες. + όλα + Όλα + Όλες οι συζητήσεις θα διαγραφτούν απο την λίστα %s, και η λίστα θα διαγραφτεί + Όλα τα καινούργια μυνήματα από αυτά τα μέλη θα είναι κρυμμένα! + Επιτρέψτε τα αρχεία και πολυμέσα μόνο αν η επαφή σου το επιτρέπει. + Επιτρέψτε την αναφορά μυνημάτων στους διαχειριστές. + Επιτρέψτε τις επαφές σας να σας στέλνουν αρχεία και πολυμέσα. + Όλες η αναφορές θα αρχειοθετηθούν για εσένα. + Όλοι οι διακομιστές + Άλλος λόγος + Η εφαρμογή πάντα να τρέχει στο παρασκήνιο + Αρχειοθέτηση + Αρχειοθέτηση όλων των αναφορών; + αρχειοθετημένη αναφορά + 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 6163d7e873..b5e756aaad 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -7,37 +7,36 @@ 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 Todos los miembros del grupo permanecerán conectados. - Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite para tí. (24 horas) - Android Keystore se usará para almacenar de forma segura la frase de contraseña después de cambiarla o reiniciar la aplicación - permitirá recibir notificaciones. - Permites a tus contactos enviar mensajes temporales - Permites a tus contactos enviar mensajes de voz. + Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas) + Android Keystore se usará para almacenar la frase de contraseña de forma segura después de cambiarla o reiniciar la aplicación - permitirá recibir notificaciones. + Permites que tus contactos envien mensajes temporales. + Permites que tus contactos envien 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.]]> + Se puede desactivar en la configuración. En ese caso las notificaciones se seguirán mostrando mientras la aplicación esté en funcionamiento.]]> Siempre activo Permitir y después: ¿Aceptar solicitud de conexión\? Aceptar incógnito - Se eliminarán todos los mensajes SOLO para tí. ¡No podrá deshacerse! + 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 @@ -45,18 +44,18 @@ Aceptar llamada (sin cifrar) llamada - Llamadas y videollamadas + Llamadas y Videollamadas Audio desactivado 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. + Android Keystore se usará para almacenar la frase de contraseña de forma segura - permite que el servicio de notificaciones funcione. Añadir perfil Color - Permites a tus contactos eliminar irreversiblemente los mensajes enviados. (24 horas) + Permites que tus contactos eliminan irreversiblemente los mensajes enviados. (24 horas) Se permiten los mensajes de voz pero sólo si tu contacto también los permite. Se permiten mensajes directos entre miembros. Se permite la eliminación irreversible de mensajes. (24 horas) @@ -73,7 +72,7 @@ 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 se pierde 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 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) @@ -88,7 +87,7 @@ ¿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. @@ -109,9 +108,9 @@ Llamada con cifrado de extremo a extremo cifrado de extremo a extremo mensaje duplicado - Herramientas desarrollo + Herramientas de desarrollo 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 en base de datos @@ -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 @@ -159,7 +158,7 @@ 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 @@ -209,7 +208,7 @@ Eliminar grupo Editar perfil de grupo Conexión - Eliminar el perfil de chat para + Eliminar el perfil Oscuro %dd %d días @@ -217,14 +216,14 @@ conectado directa El contacto permite - predeterminado (%s) + predefinido (%s) Eliminar para todos activado Tus contactos sólo pueden marcar los mensajes para eliminar. Tu podrás verlos. %ds eliminado ¿Conectar mediante dirección de contacto? - ¿Unirte al grupo? + ¿Te unes al grupo? ¿Conectar mediante enlace de invitación? Conectar conectado @@ -239,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 @@ -273,7 +271,7 @@ Cambiar rol Mediante perfil (predeterminado) o por conexión (BETA) cambiando de servidor… - Preferencias de Chat + Preferencias generales cancelado %s SimpleX está parado LLAMADAS @@ -306,19 +304,19 @@ Imagen guardada en la Galería El archivo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. Enlace de invitación de un uso - Pegar el enlace recibido + Pega el enlace recibido Error al guardar perfil de grupo Salir sin guardar Archivo guardado - 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 @@ -330,8 +328,8 @@ Error al cambiar configuración Archivo: %s ¡Error al cambiar perfil! - Introduce el servidor manualmente - Cómo usar los servidores + Añadir manualmente + Cómo usar tus servidores Error al parar SimpleX Introduce la contraseña correcta. Introduce la contraseña… @@ -344,7 +342,7 @@ 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: @@ -356,9 +354,9 @@ Ignorar Error al eliminar base de datos Base de datos cifrada - Error al eliminar miembro + Error al expulsar miembro Los miembros pueden enviar mensajes de voz. - en modo incógnito mediante enlace de dirección del contacto + en modo incógnito mediante dirección de contacto ¡Error al crear perfil! No se pudo cargar el chat Fallo en la carga de chats @@ -412,7 +410,7 @@ 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 @@ -429,7 +427,7 @@ 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. Da permiso en el siguiente diálogo para recibir notificaciones instantáneas.]]> Instalar terminal de SimpleX Chat @@ -440,13 +438,13 @@ Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos. 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 + Notificaciones instantáneas 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 + Unirme + Unirme en modo incógnito indirecta (%1$s) Claro Activado @@ -465,7 +463,7 @@ ¡Invitación caducada! ha salido Mensajes en vivo - Notificación instantánea + Notificaciones instantáneas Servicio El mensaje se marcará para eliminar. El destinatario o destinatarios podrán revelar este mensaje. ¡Mensaje en vivo! @@ -491,13 +489,13 @@ 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 Lo más probable es que este contacto haya eliminado la conexión contigo. Moderar - unirte como %s + Unirme como %s Sólo se pueden enviar 10 imágenes al mismo tiempo ¡Archivo grande! Silenciar @@ -532,7 +530,7 @@ Sin archivos recibidos o enviados Mensajes Contraseña nueva… - ¿Unirte al grupo? + ¿Te unes al grupo? Entrando al grupo Error en Keystore Invitar miembros @@ -565,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 @@ -573,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 del certificado en la dirección del servidor es incorrecta + La huella en la dirección del servidor no coincide con el certificado. 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. @@ -597,7 +595,7 @@ llamada rechazada secreto Abrir SimpleX Chat para aceptar llamada - Restablecer valores predetarminados + Reiniciar a valores predetarminados Pendiente Notificaciones periódicas Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes. @@ -606,7 +604,7 @@ Notificaciones privadas imagen del perfil No se permiten mensajes de voz. - Proteger la pantalla de la aplicación + Proteger pantalla de la aplicación repositorio GitHub .]]> Grabar mensaje de voz ha expulsado a %1$s @@ -681,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 el enlace haya sido 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. + 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 @@ -718,7 +715,7 @@ envío no autorizado Escribe un nombre para el contacto Error desconocido - El rol cambiará a %s. Todos serán notificados. + El rol cambiará a %s. Se notificará en el grupo. La seguridad de SimpleX Chat ha sido auditada por Trail of Bits. Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido. Mensajes de chat SimpleX @@ -741,21 +738,21 @@ 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! - SimpleX se ejecuta en segundo plano en lugar de usar notificaciones push.]]> + SimpleX se ejecuta en segundo plano.]]> Configuración Altavoz desactivado Inciar chat nuevo - Para exportar, importar o eliminar la base de datos debes parar SimpleX. Mientra tanto no podrás recibir o enviar mensajes. - Gracias por instalar SimpleX Chat! - Para proteger tu privacidad, 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. - ¿Usar servidores SimpleX Chat\? + Para actualizar la configuración el cliente se reconectará a todos los servidores. + ¿Usar servidores de 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 + El servidor requiere autorización para crear colas, comprueba la contraseña. Para recibir notificaciones, introduce la contraseña de la base de datos Llamadas de chat SimpleX Cíclico @@ -774,8 +771,8 @@ 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. - ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate. + ¡Agradecimiento a los colaboradores! Puedes contribuir a través de Weblate. + ¡Agradecimiento a los colaboradores! Puedes contribuir a través de Weblate. Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC. Aislamiento de transporte (para compartir con tu contacto) @@ -784,8 +781,8 @@ %s no está verificado Probar servidor Probar servidores - Estrella en GitHub - Lista de servidores para las conexiones nuevas del perfil + Califica en GitHub + Servidores para conexiones nuevas en tu perfil ¿Usar conexión directa a Internet\? El perfil sólo se comparte con tus contactos. inicializando… @@ -807,9 +804,9 @@ 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. + Intentando conectar con el servidor usado para recibir mensajes de esta conexión. formato de mensaje desconocido - Intentando conectar con el servidor para recibir mensajes de este contacto (error: %1$s). + Error al conectar con el servidor usado para recibir mensajes de esta conexión: (error: %1$s). Prueba no superada en el paso %s. Pulsa para iniciar chat nuevo Compartir mensaje… @@ -825,7 +822,7 @@ ¡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 @@ -858,7 +855,7 @@ Ya tienes un perfil con este nombre mostrado. Por favor, selecciona otro nombre. Abrir en aplicación móvil.]]> ponerte en contacto con los desarrolladores de SimpleX Chat para consultas y para recibir actualizaciones.]]> - ¡No puedes enviar mensajes! + eres observador Puedes usar la sintaxis markdown para dar formato a tus mensajes: 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. El contacto debe estar en línea para completar la conexión. @@ -867,14 +864,14 @@ \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 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. + Te conectarás cuando el dispositivo del 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 ! - has sido invitado al grupo + ¡Bienvenido %1$s! + Has sido invitado al grupo Esperando archivo Esperando imagen Mensaje de voz (%1$s ) @@ -883,7 +880,7 @@ Has sido invitado a un grupo. Únete para conectar con sus miembros. has expulsado a %1$s Tú: %1$s - Puedes compartir un enlace o código QR para que cualquiera pueda unirse al grupo. Si decides eliminarlo más tarde, los miembros del grupo se mantendrán. + Puedes compartir el enlace o el código QR para que cualquiera pueda unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo. Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten. Mis preferencias Con mensaje de bienvenida opcional. @@ -917,17 +914,17 @@ Permites SimpleX Tu perfil se enviará al contacto del que has recibido este enlace. - Te conectarás con todos los miembros del grupo. + Conectarás con todos los miembros del grupo. tu - Estás conectado al servidor usado para recibir mensajes de este contacto. - mediante enlace de dirección de contacto + Estás conectado al servidor usado para recibir mensajes de esta conexión. + mediante dirección de contacto mediante enlace de grupo enlace de un solo uso has compartido enlace de un solo uso en módo incógnito No tienes chats El contacto ha enviado un archivo mayor al máximo admitido (%1$s ). %1$d mensaje(s) omitido(s) - Dejarás de recibir mensajes de este grupo. El historial del chat se conservará. + Dejarás de recibir mensajes del grupo. El historial del chat se conservará. Mostrar código de seguridad Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos. ¡Mensajes de voz no permitidos! @@ -938,9 +935,9 @@ Mis perfiles Mi dirección SimpleX Tu servidor - Dirección del servidor + Dirección de tu servidor Tu perfil actual - Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil. + Tu perfil se almacena en tu dispositivo y se comparte sólo con tus contactos. Los servidores SimpleX no pueden ver tu perfil. Sistema Añadir mensaje de bienvenida Llamadas y videollamadas @@ -968,7 +965,7 @@ Ahora con soporte bluetooth y otras mejoras. ¡Guarda un mensaje para ser mostrado a los miembros nuevos! Interfaz en chino y español - ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate. + ¡Agradecimiento a los colaboradores! Puedes contribuir a través de Weblate. Error al actualizar privacidad de usuario Confirmar contraseña Reducción consumo de batería @@ -995,12 +992,12 @@ 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 - Ocultar: - Mostrar: + Mostrar opciones para desarrolladores + Oculta: + Muestra: Eliminar perfil Contraseña del perfil Mostrar perfil oculto @@ -1019,7 +1016,7 @@ Error al cargar servidores XFTP Error al cargar servidores SMP Asegúrate de que las direcciones del servidor XFTP tienen el formato correcto, están separadas por líneas y no están duplicadas. - El servidor requiere autorización para subir, comprueba la contraseña + El servidor requiere autorización para subir, comprueba la contraseña. Comparar archivo Crear archivo Eliminar archivo @@ -1049,7 +1046,7 @@ %d minutos Introduce código Inmediatamente - Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay forma de recuperar un código perdido! + Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay manera de recuperar un código perdido! ¡Bloqueo SimpleX no activado! Puedes activar el Bloqueo SimpleX a través de Configuración. Confirma código @@ -1084,7 +1081,7 @@ ¿Dejar de recibir el archivo\? ¿Dejar de enviar el archivo\? Se permiten las llamadas pero sólo si tu contacto también las permite. - Permites que tus contactos puedan llamarte. + Permites que tus contactos te llamen. Llamadas y videollamadas Las llamadas y videollamadas no están permitidas. " @@ -1096,7 +1093,7 @@ Código de acceso de la aplicación Interfaz en polaco Úsalo en lugar de la autenticación del sistema. - ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate. + ¡Agradecimiento a 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! Cambiar perfil @@ -1109,11 +1106,11 @@ Abriendo base de datos… Error al introducir dirección Guía de Usuario.]]> - Enlace de un uso + Enlace de un solo uso Dirección SimpleX Cuando alguien solicite conectarse podrás aceptar o rechazar su solicitud. Compartir dirección - Introduce mensaje de bienvenida… + Deja un mensaje de bienvenida… SimpleX Color adicional Secundario adicional @@ -1127,7 +1124,7 @@ Continuar Tema oscuro Personalizar tema - Introduce mensaje de bienvenida… (opcional) + Deja un mensaje de bienvenida… (opcional) Crea una dirección para que otras personas puedan conectar contigo. No crear dirección SimpleX ¡Hola! @@ -1139,14 +1136,14 @@ Asegúrate de que el archivo tiene la sintaxis YAML correcta. Exporta el tema para tener un ejemplo de la estructura del archivo de tema. La actualización del perfil se enviará a tus contactos. Mensaje recibido - Guardar configuración de auto aceptar + Guardar configuración de dirección SimpleX Puedes compartir tu dirección como enlace o código QR para que cualquiera pueda conectarse contigo. ¿Guardar configuración\? Secundario Mensaje enviado Dejar de compartir ¿Dejar de compartir la dirección\? - COLORES DEL INTERFAZ + COLORES DE LA INTERFAZ Puedes crearla más tarde ¿Compartir la dirección con los contactos\? Compartir con contactos @@ -1214,7 +1211,7 @@ Registro actualiz 30 segundos 5 minutos - Permitir que tus contactos añadan reacciones a los mensajes. + Permites que tus contactos añadan reacciones a los mensajes. Eliminado Se permiten las reacciones a los mensajes pero sólo si tu contacto también las permite. Temas personalizados @@ -1222,7 +1219,7 @@ - mensajes de voz de hasta 5 minutos. \n- tiempo personalizado para mensajes temporales. \n- historial de edición. - ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate. + ¡Agradecimiento a los colaboradores! Puedes contribuir a través de Weblate. Mensajes mejorados Al introducirlo todos los datos son eliminados. Personalizar y compartir temas de color. @@ -1232,11 +1229,11 @@ Interfaz en japonés y portugués sin texto Han ocurrido algunos errores no críticos durante la importación: - ¿Cerrar\? + ¿Salir de SimpleX? APLICACIÓN Reiniciar - Cerrar - Las notificaciones dejarán de funcionar hasta que reinicies la aplicación + Salir + 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 @@ -1368,9 +1365,9 @@ Cifra archivos almacenados y multimedia Error al establecer contacto con el miembro Recuerda: los servidores están conectados mediante proxy SOCKS, pero las llamadas y las previsualizaciones de enlaces usan conexión directa.]]> - Cifra archivos locales + 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). Envía un mensaje para conectar Descubre y únete a grupos @@ -1382,8 +1379,8 @@ - conexión al servicio de directorio (BETA)! \n- confirmaciones de entrega (hasta 20 miembros). \n- mayor rapidez y estabilidad. - Enviar mensaje directo - conectado directamente + envia para conectar + conexión solicitada Expandir Error de renegociación de cifrado contacto eliminado @@ -1427,7 +1424,7 @@ Grupo abierto Conexión finalizada (este dispositivo v%s)]]> - ¡Los mensajes de %s serán mostrados! + ¡Los mensajes nuevos de %s serán mostrados! Nombre de este dispositivo… Error Conectar con ordenador @@ -1476,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. @@ -1518,13 +1515,13 @@ 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 se envía el historial a los miembros nuevos. - O muestra este código QR + 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? + Puedes ver el enlace de invitación de nuevo en los detalles de la conexión. + ¿Guardar enlace no usado? Comparte este enlace de un solo uso Crear grupo: crea un grupo nuevo.]]> Historial visible @@ -1537,7 +1534,7 @@ Añadir contacto Pulsa para escanear Guardar - Pulsa para pegar el enlace + Pulsa aquí para pegar el enlace Buscar o pegar enlace SimpleX Con uso reducido de batería. bloqueado por administrador @@ -1545,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 @@ -1555,7 +1552,7 @@ ¿Bloqear miembro para todos? Creado: %s Bloquear para todos - ¿Desbloquear miembro para todos? + ¿Desbloquear al miembro para todos? Desbloquear para todos bloqueado bloqueado por administrador @@ -1573,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. + Migrando base de datos.\nPuede tardar varios minutos. El ordenador tiene una versión sin soporte. Por favor, asegúrate de usar la misma versión en ambos dispositivos el contacto %1$s ha cambiado a %2$s perfil actualizado @@ -1613,7 +1609,7 @@ Por favor, informa a los desarrolladores: \n%s Reiniciar chat - Miembro pasado %1$s + Miembro %1$s el miembro %1$s ha cambiado a %2$s dirección de contacto eliminada ha eliminado la imagen del perfil @@ -1687,8 +1683,8 @@ Error al guardar ajustes El archivo exportado no existe Para continuar, SimpleX 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.]]> + 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. @@ -1715,7 +1711,7 @@ Enlaces SimpleX no permitidos Mensajes de voz no permitidos Enlaces SimpleX - Los miembros pueden enviar enlaces SimpleX. + Los miembros pueden enviar enlaces de SimpleX. Enlaces SimpleX no permitidos. propietarios Móvil @@ -1761,7 +1757,7 @@ \nError del servidor de destino: %2$s Servidor de reenvío: %1$s \nError: %2$s - Problema en la red - el mensaje ha expirado tras muchos intentos de envío. + 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 @@ -1785,7 +1781,7 @@ 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 retransmisión desconocidos. + 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: @@ -1811,10 +1807,10 @@ Respuesta recibida Mosaico Quitar imagen - Restablecer color + Reiniciar color Escala Respuesta enviada - Establecer tema predeterminado + Establecer tema predefinido Sistema Color de fondo Encaje @@ -1826,7 +1822,7 @@ información cola del servidor: %1$s \n \núltimo mensaje recibido: %2$s - Restablecer al tema de la aplicación + Reiniciar al tema de la aplicación Enrutamiento privado de mensajes 🚀 Recibe archivos de forma segura Mejora del envío de mensajes @@ -1838,14 +1834,13 @@ Nuevos temas de chat Información cola de mensajes ninguno - Protege tu dirección IP de los servidores de retransmisión elegidos por tus contactos. -\nActívalo en ajustes de *Servidores y Redes*. - Restablecer al tema del usuario + 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 borrado o cancelado. + Archivo no encontrado, probablemente haya sido eliminado o cancelado. Error del servidor de archivos: %1$s Error de archivo Error en archivo temporal @@ -1859,10 +1854,10 @@ 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. - Info servidores + Estadísticas servidores Archivos Mostrando - Suscrito + Suscritas Errores de suscripción Suscripciones ignoradas Para ser notificado sobre versiones nuevas, activa el chequeo periódico para las versiones Estable o Beta. @@ -1883,7 +1878,7 @@ Total Sesiones de transporte Servidor XFTP - No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado. + No tienes conexión directa a estos servidores. Los mensajes destinados a estos usan enrutamiento privado. Todos los perfiles Conectadas Estadísticas detalladas @@ -1905,10 +1900,10 @@ 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 fuente - Error al restablecer las estadísticas - Restablecer - Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse! + 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. @@ -1945,16 +1940,16 @@ Reconectar todos los servidores ¿Reconectar servidor? ¿Reconectar servidores? - Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional. + 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. - Restablecer todas las estadísticas - ¿Restablecer todas las estadísticas? + Reiniciar estadísticas + ¿Reiniciar todas las estadísticas? Mensajes enviados Total enviados Archivos descargados Errores de descarga duplicados - expirados + caducados Abrir configuración del servidor otros otros errores @@ -1968,8 +1963,7 @@ Tamaño Conexiones activas Iniciado el %s. - Iniciado el %s -\nTodos los datos son privados a tu dispositivo + Iniciado el %s \nLos datos son privados en tu dispositivo. Bloques eliminados Bloques descargados Bloques subidos @@ -1977,7 +1971,7 @@ 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 + Conéctate más rápido con tus amigos Controla tu red Protege tu dirección IP y tus conexiones. ¿Permitir llamadas? @@ -2003,10 +1997,10 @@ Configuración ¿Confirmas la eliminación del contacto? ¡Contacto eliminado! - El contacto será eliminado. ¡No podrá deshacerse! + El contacto será eliminado. ¡No puede deshacerse! ¡Conversación eliminada! Elimina sin notificar - Sólo borrar la conversación + Eliminar sólo la conversación Conservar conversación buscar Contactos archivados @@ -2024,7 +2018,7 @@ Por favor, pide a tu contacto que active las llamadas. Enviar mensaje para activar llamadas. Elimina hasta 20 mensajes a la vez. - Barra de herramientas accesible + Barra de menú accesible Archiva contactos para charlar más tarde. Puedes guardar el archivo exportado. Fuerte @@ -2039,10 +2033,10 @@ 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 - Necesitas permitir que tus contacto llamen para poder llamarles. + 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. + El servidor de reenvío %1$s no ha podido conectarse al servidor de destino %2$s. Por favor, inténtalo 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 @@ -2069,17 +2063,16 @@ ¿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 + %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 borrados después de seleccionarlos. + Los mensajes han sido eliminados después de seleccionarlos. ¡Nada para reenviar! Guardando %1$s mensajes - No uses credenciales con proxy. + No se usan credenciales con proxy. Error guardando proxy Contraseña Autenticación proxy @@ -2090,23 +2083,23 @@ 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 podrá deshacerse! + Los mensajes serán eliminados. ¡No puede deshacerse! Error al cambiar perfil Selecciona perfil de chat - Comparte perfil - Tu conexión ha sido trasladada a %s pero ha ocurrido un error inesperado al redirigirte al perfil. + Perfil a compartir + Tu conexión ha sido trasladada a %s pero ha ocurrido un error al cambiar de 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. - Sesión de 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 por cada servidor. - Servidor + Se usarán credenciales SOCKS nuevas para cada servidor. + por servidor Llamadas mejoradas Sistema de fechas mejorado. Experiencia de usuario mejorada @@ -2115,21 +2108,21 @@ Protocolos de SimpleX auditados por Trail of Bits. Intercambia audio y video durante la llamada. Seguridad mejorada ✅ - Borra o modera hasta 200 mensajes a la vez. + 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: - Ningún servidor de mensajes. - Ningún servidor para recibir archivos. - Ningún servidor para enviar archivos. + 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. - Configuración de dirección - Crear enlace de un uso + Ajustes de dirección + Crear enlace de un solo uso Para redes sociales - ¿Dirección SimpleX o enlace de un uso? + ¿Dirección SimpleX o enlace de un solo uso? Operadores de servidores Operadores de red Las condiciones de los operadores habilitados serán aceptadas después de 30 días. @@ -2139,10 +2132,10 @@ Operador Servidores predefinidos Revisar condiciones - %s servidores + Servidores %s Las condiciones serán aceptadas el: %s. Condiciones de uso - Para el enrutamiento privado + Para enrutamiento privado Error al añadir servidor Abrir cambios Abrir condiciones @@ -2150,7 +2143,7 @@ El operador del servidor ha cambiado. El protocolo del servidor ha cambiado. Barras de herramientas - Difuminar + Difuminado Navegación en el chat mejorada Descentralización de la red - El chat abre en el primer mensaje no leído.\n- Desplazamiento hasta los mensajes citados. @@ -2158,49 +2151,49 @@ Condiciones aceptadas Servidores de archivos y multimedia añadidos Servidores de mensajes añadidos - ¿Dirección o enlace de un uso? + ¿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: - Habilita Flux + 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 - Ningún servidor de archivos y multimedia. - Ningún servidor para enrutamiento privado. - Ningún servidor para recibir mensajes. + 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 - Campartir dirección públicamente - Compartir los enlaces de un uso y las direcciones SimpleX es seguro a través de cualquier medio. + Selecciona operadores a usar. + 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 - Usar para mensajes + Uso para mensajes Ver condiciones Para recibir Para enviar - Usar para archivos + 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 Servidores y Redes. + Puedes configurar los operadores desde los ajustes de Servidores y Red. %s.]]> %s.]]> %s.]]> %s.]]> %s.]]> %s.]]> - %s, acepta las condiciones de uso.]]> - Los servidores para archivos nuevos en tu perfil actual + %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. @@ -2209,7 +2202,7 @@ 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 borrado o aún no se ha recibido. + 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. @@ -2217,20 +2210,20 @@ 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 los miembros de tu equipo a las conversaciones. + 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 podrá deshacerse! + 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 podrá deshacerse! + 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 podrá deshacerse! - El rol cambiará a %s. Todos serán notificados. - Dejarás de recibir mensajes de este chat. El historial del chat se conserva. + El miembro será eliminado del chat. ¡No puede deshacerse! + El rol cambiará a %s. Se notificará en el chat. + Dejarás de recibir mensajes del chat. El historial del chat se conserva. 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 @@ -2243,7 +2236,7 @@ Comprobar mensajes cada 10 min. Sin servicio en segundo plano Chat - Barra de herramientas accesible + Barra de chat accesible Mensajes directos entre miembros no permitidos. %1$s.]]> ¡El chat ya existe! @@ -2251,5 +2244,297 @@ 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 servidores operados por Flux en la aplicación. - \ No newline at end of file + 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 nuevos de estos miembros serán mostrados! + ¿Desbloquear los miembros para todos? + ¡Todos los mensajes nuevos de estos miembros estarán ocultos! + ¿Bloquear miembros para todos? + 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 puerto TCP 443 solo en servidores predefinidos. + Todos los servidores + Servidores predefinidos + No + Admisión miembro + Aceptar como observador + Error al aceptar el miembro + ¿Guardar configuración? + Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo. + has aceptado al miembro + pendiente de revisión + por revisar + Chat con administradores + Chat con miembro + en revisión por los administradores + %1$s aceptado + desactivado + Admisión de miembros + el miembro usa una versión antigua + Sin chats + Error al eliminar el chat + %d chats con miembros + %d mensajes + un chat con miembro + %d chat(s) + Informe enviado a los moderadores + no se pueden enviar mensajes + contacto eliminado + contacto desactivado + en espera de ser aceptado + el grupo ha sido eliminado + no sincronizado + expulsado del grupo + petición para unirse rechazada + ¡No puedes enviar mensajes! + Puedes ver tus informes en Chat con administradores + has salido + te ha aceptado + Un miembro nuevo desea unirse al grupo. + todos + Chat con miembros + Chat con administradores + Eliminar chat + ¿Eliminar chat con miembro? + Rechazar + ¿Rechazar al miembro? + Revisar miembros + Revisa a los miembros antes de admitirles en el grupo. + Aceptar + Aceptar miembro + Aceptar como miembro + El miembro se unirá al grupo, ¿aceptas al miembro? + Actualizar dirección + Aceptar petición de contacto + Añadir mensaje + después de que tu petición sea aceptada.]]> + Chatea con los administradores + Chatea con el miembro antes de unirse. + Conectar + ¡Conéctate más rápido! 🚀 + el contacto debe aceptarte… + Error al cambiar el perfil + Error al abrir el chat + Error al abrir el grupo + Error al rechazar la solicitud del contacto + Unirme al grupo + Menos tráfico en redes móviles. + Tras pulsar Contactar, mensajea ya. + Nuevo rol de grupo: Moderador + Ninguna sesión con enrutamiento privado + Abrir chat + Abrir chat nuevo + Abrir grupo nuevo + Abrir para aceptar + Abre para conectar + Abre para unirte + Timeout enrutamiento privado + La dirección pasará a ser corta y tu perfil será compartido mediante la dirección. + Timeout protocolo en segundo plano + Rechazar solicitud del contacto + Elimina mensajes y bloquea miembros. + petición enviada + Revisa los miembros del grupo + ¿Enviar solicitud de contacto? + Enviar solicitud + Enviar solicitud sin mensaje + Envía tu comentario privado a los grupos. + Enviadp a tu contacto tras la conexión. + ¿Actualizar enlace de grupo? + Actualizar + ¿Actualizar la dirección? + Timeout conexión TCP sp + El remitente NO será notificado. + Para usar otro perfil tras el intento de conexión, elimina el chat y usa el enlace de nuevo. + Mensaje de bienvenida + Tu perfil + cifrado de extremo a extremo.]]> + No se puede cambiar el perfil + Biografía: + Cargando perfil. . . + Descripción corta: + Tu biografía: + Biografía demasiado larga + Descripción demasiado larga + Aceptar solicitud de conexión + Conexión empresarial + Grupo + Pulsa Conectar para chatear + Pulsa Conectar para enviar solicitud + Pulsa Unirme al grupo + Mensajes temporales activados sólo para los contactos nuevos. + Usar perfil incógnito + Mi contacto empresarial + Mi contacto + Mi grupo + 4 idiomas nuevos + Catalán, Indonesio, Rumano y Vietnamita - gracias a los colaboradores! + Crea tu dirección + Activa por defecto los mensajes temporales + Mantén los chats limpios + Añade mensaje de bienvenida y biografía del perfil + Comparte tu dirección + Direcciones SimpleX cortas + Actualiza tu dirección + Da la bienvenida a tus contactos 👋 + Compartir dirección antigua + Compartir enlace antiguo + El enlace será corto y el perfil del grupo se compartirá mediante el enlace. + Actualizar enlace de grupo + SOLICITUDES DE CONTACTO EN GRUPOS + conexión solicitada desde el grupo %1$s + Esta configuración se aplica al perfil actual + Miembro eliminado, no puede aceptar solicitudes + Se permiten archivos y multimedia pero sólo si tu contacto también los permite. + Permes que tus contactos envíen archivos y multimedia. + Bot + Tanto tú como tu contacto podéis enviar archivos y multimedia. + Los archivos y multimedia no están permitidos en este chat. + Sólo tú puedes enviar archivos y multimedia. + Sólo tu contacto puede enviar archivos y multimedia. + Abre para usar el bot + Archivos y multimedia no permitidos. + Pulsa Conectar para usar el bot + Para enviar comandos debes estar conectado. + Opciones obsoletas + Abrir enlace limpio + Limpiar enlaces de seguimiento + Abrir enlace completo + Enlace de servidor SimpleX + Error al marcar como leído + La huella en la dirección del servidor no coincide con el certificado: %1$s. + La huella en la dirección del servidor de destino no coincide con el certificado: %1$s. + La huella en la dirección del servidor de reenvío no coincide con el certificado: %1$s. + Sin suscripciones + No estás conectado al servidor usado para recibir mensajes de esta conexión (no suscrito). + 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 866506460c..eba31ba788 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -11,14 +11,14 @@ لغو تغییر نشانی تغییر نشانی را لغو می‌کنید؟ درباره سیمپل‌اکس(SimpleX) - به وسیله نشانی مخاطب متصل می‌شوید؟ - به وسیله لینک یک بار مصرف متصل می‌شوید؟ - از نمایه ناشناس جدید استفاده کن - در حال گشودن پایگاه داده… - نمایه شما به مخاطبی که این لینک را از او دریافت کردید، فرستاده خواهد شد. - متصل شدن به صورت ناشناس - شما یک مسیر نامعتبر پرونده به اشتراک گذاشتید. موضوع را به توسعه‌دهندگان برنامه گزارش دهید. - هنوز از دریافت پرونده پشتیبانی نمی‌شود + با نشانی مخاطب ارتباط برقرار شود؟ + با لینک یک بار مصرف ارتباط برقرار شود؟ + استفاده از پروفایل ناشناس جدید + در حال باز کردن پایگاه داده… + پروفایل شما به مخاطبی که این لینک را از او دریافت کردید، فرستاده خواهد شد. + اتصال ناشناس + شما یک مسیر نامعتبر فایل به اشتراک گذاشتید. موضوع را به توسعه‌دهندگان برنامه گزارش دهید. + هنوز از دریافت فایل پشتیبانی نمی‌شود قالب پیام نامعتبر حذف شده خطا در نمایش پیام @@ -30,22 +30,22 @@ در حال اتصال k به گروه می‌پیوندید؟ - از نمایه کنونی استفاده کن + استفاده از پروفایل کنونی به تمام اعضای گروه متصل خواهید شد. متصل شدن - مسیر نامعتبر پرونده + مسیر نامعتبر فایل برنامه از کار افتاد - در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s). + در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s). حذف شد علامت گذاشته شده به عنوان حذف شده توسط %s حذف شد مسدود %d پیام مسدود شده - هنوز از ارسال پرونده پشتیبانی نمی‌شود + هنوز از ارسال فایل پشتیبانی نمی‌شود شما قالب پیام ناشناخته زنده - گپ نامعتبر + چت نامعتبر داده نامعتبر خطا در نمایش محتوا خطا در رمزگشایی @@ -68,7 +68,7 @@ لینک‌های SimpleX خطا در ذخیره کردن سرورهای SMP خطا در ذخیره کردن سرورهای XFTP - خطا در مذاکره مجدد رمزگذاری + خطای تجدید مذاکره رمزنگاری اتصال %1$d اتصال برقرار شد شما لینک یک بار مصرف به اشتراک گذاشتید @@ -78,39 +78,39 @@ لینک گروه SimpleX به وسیله %1$s به وسیله مرورگر - نشانی مخاطب SimpleX + آدرس مخاطب SimpleX دعوت یک بار مصرف SimpleX در حال اتصال… - باز کردن لینک در مرورگر ممکن است حریم خصوصی و امنیت اتصال را کاهش دهد. لینک‌های SimpleX ناموثق قرمز خواهند بود. + باز کردن لینک در مرورگر ممکن است حریم خصوصی و امنیت اتصال را کاهش دهد. لینک‌های SimpleX غیر قابل اعتماد به رنگ قرمز خواهند بود. خطا در بارگیری سرورهای XFTP - خطا در ایجاد نمایه! - خطا در تعویض نمایه! + خطا در ایجاد پروفایل! + خطا در تعویض پروفایل! توقف اتصال خطا در اتصال خطا در ارسال پیام خطا در ایجاد پیام خطا در بارگیری جزئیات خطا در پیوستن به گروه - فرستنده انتقال پرونده را لغو کرد. + فرستنده انتقال فایل را لغو کرد. شما از قبل به %1$s متصل هستید. خطا در اتصال (تصدیق) خطا در بارگیری سرورهای SMP - عدم موفقیت در بارگیری گپ‌ها + عدم موفقیت در بارگیری چت‌ها لطفا برنامه را به‌روزرسانی کنید و با توسعه‌دهندگان تماس بگیرید. نام نمایشی همسان! نام نمایشی نامعتبر است. لطفا نام دیگری انتخاب کنید. خطا در افزودن اعضا - امکان دریافت پرونده وجود ندارد - لطفا اتصال خود را با %1$s بررسی کنید و دوباره امتحان کنید. - خطا در دریافت پرونده + امکان دریافت فایل وجود ندارد + لطفا اتصال شبکه خود را با %1$s بررسی کنید و دوباره امتحان کنید. + خطا در دریافت فایل مخاطب از قبل وجود دارد لینک اتصال نامعتبر لطفا بررسی کنید که از لینک صحیح استفاده کردید یا از مخاطبتان بخواهید لینک دیگری برایتان بفرستد. مطمئن شوید قالب نشانی‌های سرور SMP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند. خطا در به‌روزرسانی پیکربندی شبکه - عدم موفقیت در بارگیری گپ + عدم موفقیت در بارگیری چت نام نمایشی نامعتبر! - شما یک نمایه گپ با نام نمایشی یکسان دارید، لطفا نام دیگری انتخاب کنید. + شما یک پروفایل چت با نام نمایشی یکسان دارید، لطفا نام دیگری انتخاب کنید. خطا در ایجاد نشانی مطمئن شوید قالب نشانی‌های سرور XFTP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند. خطا در پذیرش درخواست مخاطب @@ -119,14 +119,14 @@ خطا در لغو تغییر نشانی خطا در انطباق زمانی اتصال آزمایش در گام %s ناموفق بود. - سرور برای بارگذازی به اجازه نیاز دارد، گذرواژه را بررسی کنید - احتمال دارد اثر انگشت گواهینامه در نشانی سرور نادرست باشد + سرور برای بارگذاری نیاز به مجوز دارد، گذرواژه را بررسی کنید. + اثر انگشت در نشانی سرور با گواهی مطابقت ندارد. ایجاد صف - بارگذاری پرونده - بارگیری پرونده - مقایسه پرونده - حذف پرونده - خطا در حذف نمایه کاربر + بارگذاری فایل + بارگیری فایل + مقایسه فایل + حذف فایل + خطا در حذف پروفایل کاربر خطا در به‌روزرسانی حریم خصوصی کاربر خطا در حذف مخاطب خطا در حذف گروه @@ -137,10 +137,10 @@ خطا اتصال قطع اتصال - ایمن‌سازی صف + صف امن حذف صف - ایجاد پرونده - سرور برای ایجاد صف داده‌ها به اجازه نیاز دارد، گذرواژه را بررسی کنید + ایجاد فایل + سرور برای ایجاد صف‌ها نیاز به مجوز دارد، گذرواژه را بررسی کنید. مگر اینکه مخاطبتان اتصال را حذف کرده یا این لینک قبلا استفاده شده باشد، ممکن است این یک اشکال باشد - لطفا آن را گزارش دهید. \nبرای متصل شدن، لطفا از مخاطبتان بخواهید لینک اتصال دیگری ایجاد کند و بررسی کنید که اتصال شبکه باثباتی دارید. عملکرد کند @@ -154,29 +154,29 @@ به وسیله تنظیمات می‌تواند غیرفعال شود – اعلان‌ها تا زمانی که برنامه در حال اجراست، همچنان نمایش داده می‌شوند.]]> جابه‌جایی پایگاه داده در حال جریان است. \nممکن است دقایقی زمان ببرد. - به SimpleX اجازه دهید در پس‌زمینه اجرا شود. در غیر این صورت، اعلان‌ها غیرفعال خواهند شد.]]> - سرویس پس‌زمنیه SimpleX دارد – سرویس، هر روز درصد معدودی از باتری را استفاده می‌کند.]]> + اجازه دهید تا اعلان‌ها را فوری دریافت کنید.]]> + SimpleX در پس‌زمینه اجرا می‌شود و به جای استفاده از پوش نوتیفیکیشن، کار می‌کند.]]> ذخیره شده ذخیره شده از %s ذخیره شده از فرستاده شده - رمزگذاری سرتاسر با محرمانگی پیشرو، مردودسازی و بازیابی ورود غیرمجاز محافظت شده‌اند.]]> + رمزنگاری انتها به انتها با محرمانگی پیشرو، مردودسازی و بازیابی ورود غیرمجاز محافظت شده‌اند.]]> بهینه‌سازی باتری فعال است، سرویس پس‌زمینه و درخواست‌های متناوب برای پیام‌های جدید خاموش می‌شوند. می‌توانید آن‌ها را از طریق تنظیمات باز فعال کنید. اعلان‌های متناوب اعلان‌های متناوب غیرفعالند! - رمزگذاری سرتاسر مقاوم در برابر کوانتوم با محرمانگی پیشرو، مردودسازی و بازیابی ورود غیرمجاز محافظت شده‌اند.]]> - این گپ به وسیله رمزگذاری سرتاسر محافظت شده است. - این گپ به وسیله رمزگذاری سرتاسر مقاوم در برابر کوانتوم محافظت شده است. + رمزنگاری انتها به انتها مقاوم در برابر کوانتوم با محرمانگی پیشرو، مردودسازی و بازیابی ورود غیرمجاز محافظت شده‌اند.]]> + این چت به وسیله رمزنگاری انتها به انتها محافظت شده است. + این چت به وسیله رمزنگاری انتها به انتها مقاوم در برابر کوانتوم محافظت شده است. غیرفعال کردن اعلان‌ها SimpleX نمی‌تواند در پس‌زمینه اجرا شود. فقط وقتی برنامه در حال اجراست اعلان‌ها را دریافت خواهید کرد. - برنامه پیام‌های جدید را به طور متناوب دریافت می‌کند - درصد معدودی از باتری در روز استفاده می‌کند. برنامه از اعلان‌های رانشی استفاده نمی‌کند - داده‌ای از دستگاه شما به سرورها فرستاده نمی‌شود. + برنامه پیام‌های جدید را به طور متناوب دریافت می‌کند - درصد کمی از باتری در روز استفاده می‌کند. برنامه از پوش نوتیفیکیشن استفاده نمی‌کند - داده‌ای از دستگاه شما به سرورها فرستاده نمی‌شود. اجازه دادن باز کردن تنظیمات برنامه - نمایش پیش‌نما + نمایش پیش‌نمایش وقتی برنامه باز است، اجرا می‌شود هر ۱۰ دقیقه به مدت ۱ دقیقه پیام‌های جدید را بررسی می‌کند فقط نمایش مخاطب - گشودن کنسول گپ + باز کردن کنسول چت پنهان کردن پیام‌های SimpleX Chat تماس‌های SimpleX Chat @@ -185,7 +185,7 @@ پنهان پنهان کردن مخاطب و پیام مخاطب پنهان: - گشودن نمایه‌های گپ + تغییر پروفایل‌های چت سرویس پس‌زمینه همیشه در حال اجراست - اعلان‌ها به محض موجود شدن پیام‌ها به نمایش درمی‌آیند. قفل SimpleX پایان تماس @@ -198,8 +198,8 @@ پیام جدید نمایش مخاطب و پیام درخواست مخاطب جدید - توقف گپ - گشودن صفحه جابه‌جایی + توقف چت + بازکردن صفحه جابه‌جایی می‌توانید قفل SimpleX را از طریق تنظیمات روشن کنید. خطا در نمایش اعلان، با توسعه‌دهندگان تماس بگیرید. قفل SimpleX فعال نیست! @@ -207,18 +207,18 @@ ارسال شده نماد زمینه لغو پیش‌نمایش تصویر - لغو پیش‌نمایش پرونده + لغو پیش‌نمایش فایل مصرف باتری برنامه / نامحدود را در تنظیمات برنامه انتخاب کنید.]]> برنامه ممکن است بعد از ۱ دقیقه در پس‌زمینه بسته شود. مقداردهی اولیه پایگاه داده ممکن نیست عبارت عبور الزامی است برای دریافت اعلان‌ها، لطفا، عبارت عبور پایگاه داده را وارد کنید - پایگاه داده به درستی کار نمی‌کند. برای آگاهی بیشتر لمس کنید + پایگاه داده به درستی کار نمی‌کند. برای آگاهی بیشتر ضربه بزنید سرویس SimpleX Chat در حال دریافت پیام‌ها… روشن کردن عدم موفقیت تصدیق - گشودن قفل + باز کردن قفل اطلاعات ورودتان را تایید کنید پاسخ تاریخچه @@ -227,11 +227,11 @@ بسط دادن پیام برای تمام اعضا به عنوان حذف شده علامت‌گذاری خواهد شد. توقف - دریافت پرونده متوقف خواهد شد. + دریافت فایل متوقف خواهد شد. خوش آمدید! خطا در کدبرداری ارسال پیام مستقیم برای اتصال - لطفا، تا زمانی که پرونده در حال بارگیری از موبایل متصل است، منتظر باشید. + لطفا تا زمانی که فایل در حال بارگیری از موبایل متصل است، منتظر باشید. حذف مخاطب مشاهده کد امنیتی تایید کد امنیتی @@ -246,10 +246,10 @@ کپی پیام به عنوان حذف شده علامت‌گذاری خواهد شد. گیرنده‌ها قادر خواهند بود این پیام را آشکار کنند. ارسال غیرموفق - برای اتصال لمس کنید + برای اتصال ضربه بزنید امکان کدبرداری ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. تصویر - پرونده پیدا نشد + فایل پیدا نشد لمس دکمه ایجاد گروه: برای ایجاد یک گروه جدید.]]> بدون تماس‌های پس‌زمینه @@ -257,23 +257,23 @@ تصدیق ویرایش بدون اطلاعات تحویل - لغو پرونده + ابطال فایل در حال اتصال… - شما هیچ گپی ندارید - بارگذاری گپ‌ها… + شما هیچ چتی ندارید + بارگذاری چت‌ها… ویدئو وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند. - بیشترین اندازه پشتیبانی شده فعلی پرونده %1$s است. - خطا در ذخیره‌سازی پرونده - در حال بارگیری پرونده + بیشترین اندازه پشتیبانی شده فعلی فایل %1$s است. + خطا در ذخیره‌سازی فایل + در حال بارگیری فایل حذف و مخاطب را باخبر کن متصل - مذاکره مجدد رمزگذاری؟ + مذاکره مجدد رمزنگاری؟ شما نیاز دارید به مخاطبتان اجازه ارسال پیام‌های صوتی دهید تا بتوانید آن‌ها را ارسال کنید. ارسال پیام زنده پیام ناپدید شونده زمان سفارشی به حافظه کپی شد - شروع گپ جدید + شروع چت جدید از گالری تماس تصویری برای محافظت از اطلاعاتتان، قفل SimpleX را روشن کنید. @@ -299,48 +299,48 @@ خوانده نشده خوش آمدید، %1$s! این متن در تنظیمات دردسترس است - گپ با توسعه‌دهندگان - گپ پالایش شده‌ای نیست + چت با توسعه‌دهندگان + چت فیلتر شده‌ای نیست به %1$s متصل شوید؟ - گپی انتخاب نشده + چتی انتخاب نشده فرستادن پیام… تعداد ویدئوی بیش از اندازه! فقط ۱۰ ویدئو در هر زمان می‌توان ارسال کرد - شما نمی‌توانید پیامی بفرستید! + شما ناظر هستید لطفا با مدیر گروه تماس بگیرید. - فقط صاحبان گروه می‌توانند پرونده‌ها و رسانه را فعال کنند. + فقط صاحبان گروه می‌توانند فایل‌ها و رسانه را فعال کنند. دریافت تصویر درخواست شده در انتظار تصویر تصویر در گالری ذخیره شد ویدئو ویدئو وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید. - پرونده - پرونده حجیم! - در انتظار پرونده - پرونده وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند. + فایل + فایل حجیم! + در انتظار فایل + فایل وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند. پیام صوتی (%1$s) اعلان‌ها مخاطب و تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! پیام‌های صوتی مجازند؟ پیام‌های صوتی ممنوع هستند! اسکن کد QR.]]> - اشتراک‌گذاری پرونده… + اشتراک‌گذاری فایل… تعداد تصویر بیش از اندازه! - برای اسکن لمس کنید - انتخاب پرونده - برای شروع گپ جدید + برای اسکن ضربه بزنید + انتخاب فایل + برای شروع چت جدید تحویل پیام عضو حذف شود؟ برای همه - ارسال پرونده متوقف خواهد شد. - دریافت پرونده متوقف شود؟ - پرونده از سرورها حذف خواهد شد. - ارسال پیام مستقیم + ارسال فایل متوقف خواهد شد. + دریافت فایل متوقف شود؟ + فایل از سرورها حذف خواهد شد. + برای اتصال ارسال کنید اشتراک‌گذاری رسانه… فقط ۱۰ تصویر در هر زمان می‌توان ارسال کرد تماس صوتی - لطفا آن را به خاطر داشته باشید یا به طور امن ذخیره کنید - راهی برای بازیابی کلمه عبور وجود ندارد! - قفل SimpleX روشن است + لطفا آن را به خاطر داشته باشید یا به طور امن ذخیره کنید - راهی برای بازیابی رمز عبور گم‌شده وجود ندارد! + قفل SimpleX روشن شد وقتی برنامه را شروع می‌کنید یا بعد از ۳۰ ثانیه در پس‌زمینه آن از سر می‌گیرید، نیاز به تصدیق خواهید داشت. جستجو غیرفعال کردن قفل SimpleX @@ -362,16 +362,16 @@ بدون تاریخچه حذف پیام حذف شود؟ - پرونده لغو شود؟ - توقف پرونده - ارسال پرونده متوقف شود؟ - لغو + فایل ابطال شود؟ + توقف فایل + ارسال فایل متوقف شود؟ + ابطال در حال اتصال… - گپ‌ها + چت‌ها شما به گروه دعوت شده‌اید پیوستن به عنوان %s جستجو یا الصاق لینک SimpleX - برای شروع گپ جدید لمس کنید + برای شروع چت جدید ضربه بزنید اشتراک‌گذاری پیام… در انتظار تصویر پیام صوتی… @@ -385,17 +385,17 @@ تایید بازنشاندن اسکن کد QR - (اسکن یا الصاق از حافظه) + (اسکن یا الصاق از کلیپ بورد) (تنها ذخیره شده توسط اعضای گروه) فعال کردن دسترسی دوربین اجازه داده نشد! دوربین سپاس برای نصب SimpleX Chat! - گشودن در موبایل را لمس کنید، سپس اتصال را در برنامه لمس کنید.]]> + باز کردن در موبایل را لمس کنید، سپس اتصال را در برنامه لمس کنید.]]> درخواست اتصال پذیرفته شود؟ پیام‌های صوتی مجاز نیستند لینک‌های SimpleX مجاز نیستند - پرونده‌ها و رسانه مجاز نیست + فایل و رسانه مجاز نیست در انتظار ویدئو دریافت ویدئو درخواست شده ویدئو ارسال شد @@ -412,22 +412,22 @@ افزودن مخاطب: برای ایجاد لینک دعوت جدید، یا اتصال از طریق لینکی که دریافت کردید.]]> اتصال از طریق لینک تصویر وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید! - نوع قفل SimpleX - تصدیق سیستم + حالت قفل SimpleX + احراز هویت سیستم تغییر کد عبور در پاسخ به فوری تصدیق دستگاه فعال نیست. زمانی که تصدیق دستگاه را فعال کنید می‌توانید قفل SimpleX را از طریق تنظیمات روشن کنید. اجازه دادن ضمیمه - امکان کدبرداری تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. - پرونده‌ها و رسانه ممنوع است! + تصویر قابل رمزگشایی نیست. لطفاً یک تصویر دیگر را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. + فایل و رسانه ممنوع است! تصویر ارسال شد در انتظار ویدئو - مخاطبتان پرونده‌ای ارسال کرد که از بیشترین اندازه پشتیبانی شده (%1$s) بزرگتر است. - پرونده وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید. - پرونده ذخیره شد - رمزگذاری در حال کار است و نیازی به توافق رمزگذاری جدید نیست. ممکن است باعث بروز خطاهای اتصال شود! + مخاطبتان فایلی ارسال کرد که از بیشترین اندازه پشتیبانی شده (%1$s) بزرگتر است. + فایل وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید. + فایل ذخیره شد + رمزنگاری در حال کار است و توافق جدید رمزنگاری لازم نیست. این ممکن است منجر به خطاهای اتصال شود! لطفا از مخاطبتان بخواهید ارسال پیام‌های صوتی را فعال کند. ارسال ویدئو @@ -436,9 +436,9 @@ خطا آدرس دریافت به سرور متفاوتی تغییر خواهد یافت. تغییر نشانی وقتی تکمیل خواهد شد که فرستنده آنلاین شود. تایید شما ممکن نیست؛ لطفا دوباره امتحان کنید. - پرونده + فایل شما ناظر هستید - گپ پاک شود؟ + چت پاک شود؟ تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! حذف پذیرش ناشناس @@ -454,47 +454,47 @@ مخاطبی که این لینک را با او به اشتراک گذاشتید قادر به اتصال نخواهد بود! اگر نپذیرید، فرستنده باخبر نخواهد شد. تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! پیام‌ها فقط برای شما حذف خواهند شد. - پاک‌سازی گپ + پاک‌سازی چت اتصال را پذیرفتید مخاطب هنوز متصل نشده است! شما مخاطبی را دعوت کردید اتصال در حال انتظار حذف شود؟ - اتصالی که پذیرفتید لغو خواهد شد! + اتصالی که شما قبول کردید، لغو خواهد شد! بی‌صدا لغو بی‌صدا علامت‌گذاری به عنوان خوانده شده خلع برگزیده تیم SimpleX - گپ جدید + چت جدید روش استفاده راهنمای مارکداون لینک نامعتبر! اشتراک‌گذاری لینک یک بار مصرف یا کد QR را اسکن کنید تلاش مجدد - عبارت عبور و صدور پایگاه داده + عبارت عبور و اکسپورت پایگاه داده افزودن سرورهای از پیش تنظیم شده افزودن به دستگاه دیگر حذف سرور چگونه از سرورهای خود استفاده کنید تصویر پیش‌نمایش لینک - نمایه‌های گپ شما + پروفایل‌های چت شما سرورهای SMP SimpleX Chat را برای ترمینال نصب کنید - در GitHub ستاره بزنید + در GitHub ستاره بدهید همکاری کنید کد QR - یک نمایه تصادفی جدید به اشتراک گذاشته خواهد شد. + یک پروفایل تصادفی جدید به اشتراک گذاشته خواهد شد. اتصال از طریق لینک لینک دعوت یک‌بارمصرف %s تایید شده است - ایجاد نمایه گپ - کنسول گپ - آزمایش سرور + ایجاد پروفایل چت + کنسول چت + سرور آزمایشی وارد کردن دستی سرور سرورهای ICE (یکی در هر خط) خطا در ذخیره کردن سرورهای ICE - جایگزین تصویر نمایه + جایگزین تصویر پروفایل دکمه بستن لغو پیش‌نمایش لینک تنظیمات @@ -505,7 +505,7 @@ درخواست اتصال ارسال شد! وقتی دستگاه میزبان گروه آنلاین شد، به گروه متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید! لینکی که دریافت کردید را الصاق کنید تا به مخاطبتان متصل شوید… - نمایه شما %1$s به اشتراک گذاشته خواهد شد. + پروفایل شما %1$s به اشتراک گذاشته خواهد شد. برای اتصال، مخاطبتان می‌تواند کد QR را اسکن یا از لینک در برنامه استفاده کند. اگر نمی‌توانید ملاقات حضوری داشته باشید، کد QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید. می‌توانید نشانی خود را به صورت لینک یا کد QR به اشتراک بگذارید - هر کسی می‌تواند به شما متصل شود. @@ -525,7 +525,7 @@ کد امنیتی را از برنامه مخاطبتان اسکن کنید. علامت‌گذاری به عنوان تایید شده %s تایید نشده است - برای تایید رمزگذاری سرتاسر، روی دستگاه‌های خود، کد را با مخاطبتان مقایسه(یا اسکن) کنید. + برای تایید رمزنگاری انتها به انتها، روی دستگاه‌های خود، کد را با مخاطبتان مقایسه(یا اسکن) کنید. سرورهای XFTP شما روش استفاده در حال استفاده از سرورهای SimpleX Chat. @@ -540,10 +540,10 @@ به ما ایمیل بفرستید قفل SimpleX افزودن سرور - آزمایش سرورها + سرورهای آزمایشی ذخیره سرورها عدم موفقیت آزمایش سرور! - عدم موفقیت آزمایش چند سرور: + برخی از سرورها در تست ناموفق بودند: اسکن کد QR سرور سرورهای SMP شما سرورهای XFTP @@ -552,14 +552,14 @@ سرورهای ICE شما اتصال الصاق - گشودن در برنامه موبایل کلیک کنید.]]> + باز کردن در برنامه موبایل کلیک کنید.]]> افزودن مخاطب نشانی‌ SimpleX پاک‌سازی تایید نشانی‌ سرور از پیش تنظیم شده کد امنیتی به‌کارگیری از سرور - سرورها برای اتصال‌های جدید نمایه گپ فعلی شما + سرورها برای اتصال‌های جدید پروفایل چت فعلی شما سرورها ذخیره شوند؟ به برنامه امتیاز بدهید نشانی‌ سرور شما @@ -572,18 +572,17 @@ این رشته متن، یک لینک اتصال نیست! کدی که اسکن کردید یک کد QR لینک SimpleX نیست. اسکن کد - تصویر نمایه + تصویر پروفایل بیشتر نمایش کد QR کد QR نامعتبر وقتی درخواست اتصال شما پذیرفته شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید! کد QR را در تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.]]> - نمایه گپ شما ارسال خواهد شد -\nبه مخاطبتان + پروفایل چت شما ارسال خواهد شد \nبه مخاطبتان اطلاعات بیشتر اگر بعدا نشانی‌ خود را حذف کنید، مخاطبان خود را از دست نخواهید داد. این لینک دعوت یک‌بارمصرف را به اشتراک بگذارید - برای الصاق لینک لمس کنید + برای الصاق لینک ضربه بزنید تنظیمات شما میزبان‌های Onion برای اتصال الزامی خواهد بود. \nلطفا توجه داشته باشید: شما بدون نشانی‌ onion. قادر نخواهید بود به سرورها متصل شوید. @@ -603,9 +602,9 @@ مخاطبانتان متصل باقی خواهند ماند. اشتراک‌گذاری لینک دسترسی به سرورها از طریق پروکسی SOCKS روی پورت %d؟ پروکسی باید قبل از فعال کردن این گزینه، راه اندازی شده باشد. - برای هر نمایه گپی که در برنامه دارید استفاده خواهد شد.]]> + برای هر پروفایل چتی که در برنامه دارید استفاده خواهد شد.]]> نمایش خطاهای داخلی - نمایش تماس‌های کند API + نمایش API call های کند خیر استفاده از میزبان‌های onion. را روی «خیر» تنظیم کنید اگر پروکسی SOCKS از آنها پشتیبانی نمی‌کند.]]> سفارشی کردن تم @@ -623,17 +622,17 @@ وقتی موجود بود از میزبان‌های Onion استفاده نخواهد شد. انزوای ترابری - نمایه گپ + پروفایل چت اتصال ساختار برنامه: %s - تنظیمات شبکه + تنظیمات پیشرفته نمایش: - لطفا توجه داشته باشید: واسطه‌های پیام و پرونده از طریق پروکسی SOCKS متصل می‌شوند. تماس‌ها و ارسال پیش‌نمایش‌های لینک از اتصال مستقیم استفاده می‌کنند.]]> + لطفا توجه داشته باشید: واسطه‌های پیام و فایل از طریق پروکسی SOCKS متصل می‌شوند. تماس‌ها و ارسال پیش‌نمایش‌های لینک از اتصال مستقیم استفاده می‌کنند.]]> گزینه‌های توسعه‌دهنده پنهان کردن: بسته شود؟ تمام مخاطبانتان متصل باقی خواهند ماند. - تمام مخاطبانتان متصل باقی خواهند ماند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد. + تمام مخاطبانتان متصل باقی خواهند ماند. به‌روزرسانی پروفایل به مخاطبانتان ارسال خواهد شد. اگر تایید کنید، سرورهای پیام‌رسانی خواهند توانست نشانی‌ IP، و فراهم‌کننده شما را ببینند - و این که به چه سرورهایی متصل می‌شوید. مطمئن شوید قالب نشانی‌های سرور WebRTC ICE صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند. برای هر مخاطب و عضو گروه استفاده خواهد شد. \nلطفا توجه داشته باشید: اگر اتصال‌های زیادی داشته باشید، مصرف باتری و ترافیک شما می‌تواند به شکل قابل توجه بالاتر باشد و بعضی اتصال‌ها ممکن است با موفقیت انجام نشوند.]]> @@ -641,41 +640,41 @@ ویرایش تصویر ایجاد نشانی‌ SimpleX اشتراک‌گذاری با مخاطبان - به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد. + به‌روزرسانی پروفایل به مخاطبانتان ارسال خواهد شد. پذیرفتن خودکار ورود پیام خوشامدگویی…(اختیاری) تنظیمات ذخیره شوند؟ - ذخیره تنظیمات پذیرفتن خودکار + ذخیره تنظیمات آدرس SimpleX حذف نشانی سلام! \nبه وسیله SimpleX Chat به من متصل شوید: %s نشانی ایجاد نشود ادامه - نمایه فعلی شما + پروفایل فعلی شما نام کامل: - ذخیره و مخاطبان مطلع شوند - بیایید در SimpleX Chat گفتگو کنیم + ذخیره کردن و اطلاع به مخاطبان + بیایید در SimpleX Chat چت کنیم تنظیمات ذخیره شوند؟ - پنهان کردن نمایه + پنهان کردن پروفایل کلمه عبور برای نمایش - ذخیره کلمه عبور نمایه + ذخیره کلمه عبور پروفایل اشتراک‌گذاری نشانی متوقف شود؟ توقف اشتراک‌گذاری می‌توانید بعدا آن را ایجاد کنید می‌توانید آن را از طریق تنظیمات برای مخاطبان SimpleX خود قابل رویت کنید. - نام نمایه: + نام پروفایل: حذف تصویر - ذخیره و اعضای گروه مطلع شوند - ذخیره و مخاطب مطلع شود + ذخیره کردن و اطلاع به اعضای گروه + ذخیره کردن و اطلاع به مخاطب خروج بدون ذخیره کردن - برای آشکار کردن نمایه پنهان خود، یک کلمه عبور کامل در فیلد جستجو در صفحه نمایه‌های گپتان وارد کنید. + برای آشکار کردن پروفایل پنهان خود، یک کلمه عبور کامل در فیلد جستجو در صفحه پروفایل‌های چت خود وارد کنید. دعوت از دوستان تایید کلمه عبور - کلمه عبور نمایه پنهان - به نمایه خود نشانی اضافه کنید، تا مخاطبانتان بتوانند آن را با اشخاص دیگر به اشتراک بگذارند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد. + کلمه عبور پروفایل پنهان + به پروفایل خود نشانی اضافه کنید، تا مخاطبانتان بتوانند آن را با اشخاص دیگر به اشتراک بگذارند. به‌روزرسانی پروفایل به مخاطبانتان ارسال خواهد شد. نشانی با مخاطبان به اشتراک گذاشته شود؟ یک نشانی ایجاد کنید تا اشخاص بتوانند به شما متصل شوند. - نمایه شما روی دستگاهتان ذخیره شده و فقط با مخاطبانتان به اشتراک گذاشته می‌شود. سرورهای SimpleX قادر به دیدن نمایه شما نیستند. + پروفایل شما روی دستگاهتان ذخیره شده و فقط با مخاطبانتان به اشتراک گذاشته می‌شود. سرورهای SimpleX قادر به دیدن پروفایل شما نیستند. خطا در ذخیره کردن کلمه عبور کاربر تماس‌های صوتی و تصویری تماس‌های شما @@ -692,7 +691,7 @@ چرخش دوربین پیش‌نویس پیام وقتی برنامه در حال اجراست - تماس صوتی رمزگذاری سرتاسر شده + تماس صوتی با رمزنگاری انتها به انتها پذیرفتن حریم خصوصی شما حالت قفل @@ -704,42 +703,42 @@ فعال کردن (نگه‌داشتن مقدارهای جایگزین شده گروه) غیرفعال برای همه گروه‌ها کمک - گپ‌ها - اجرای گپ - گپ در حال اجراست + چت‌ها + اجرای چت + چت در حال اجراست توقف - حذف تمام پرونده‌ها + حذف تمام فایل‌ها اصلاح نام به %s؟ - بعدا از طریق تنظیمات قابل تغییر است. - تماس پذیرفته نشده + چگونه بر باتری تأثیر می‌گذارد + تماس رد شده هش پیام ناصحیح هش پیام قبلی متفاوت است. شناسه پیام ناصحیح رسیدها فعال شوند؟ شما - پیام‌ها و پرونده‌ها + پیام‌ها و فایل‌ها تماس‌ها حالت ناشناس عبارت عبور پایگاه داده - صدور پایگاه داده - نمایه گپ حذف شود؟ + اکسپورت پایگاه داده + پروفایل چت حذف شود؟ نام خود را وارد کنید: ایجاد روش استفاده از مارکداون می‌توانید از مارکداون برای آرایش پیام‌ها استفاده کنید: - تماس بی‌پاسخ + تماس رد شده تماس پذیرفته نامتمرکز - نمایه خود را ایجاد کنید + پروفایل خود را ایجاد کنید SimpleX چگونه کار می‌کند مخزن GitHub ما.]]> - استفاده از گپ + استفاده از چت بهترین گزینه برای باتری. شما اعلان‌ها را فقط وقتی دریافت می‌کنید که برنامه در حال اجراست (بدون سرویس پس‌زمینه).]]> تماس‌ها روی صفحه قفل: پذیرفتن سرور واسط از نشانی IP شما محافظت می‌کند، اما سرور می‌تواند مدت تماس را مشاهده کند. - گشودن - رمزگذاری سرتاسر شده + باز کردن + رمزنگاری شده به صورت انتها به انتها قطع تماس ویدئو خاموش ویدئو روشن @@ -759,7 +758,7 @@ نام نمایشی جدید: اگر کد عبور خودتخریبی خود را زمان باز کردن برنامه وارد کنید: تمام اطلاعات برنامه حذف می‌شود. - این تنظیمات برای نمایه فعلی شما هستند + این تنظیمات برای پروفایل فعلی شما هستند ارسال رسید برای %d مخاطب فعال است غیرفعال برای همه فعال برای همه گروه‌ها @@ -767,13 +766,13 @@ توقف برنامه تم‌ها حذف پایگاه داده - پایگاه داده گپ وارد شد - %d پرونده با اندازه کل %s - رمزگذاری سرتاسر دو لایه را ذخیره می‌کنند.]]> + پایگاه داده چت ایمپورت شد + %d فایل با اندازه کل %s + فقط دستگاه‌های کلاینت پروفایل‌های کاربری، مخاطبان، گروه‌ها و پیام‌ها را ذخیره می‌کنند. نادیده گرفتن بلوتوث وارد کردن پایگاه داده - اشخاص فقط از طریق لینک‌هایی که به اشتراک می‌گذارید می‌توانند به شما متصل شوند. + شما تصمیم می‌گیرید که چه کسی می‌تواند متصل شود. تماس از پیش پایان یافته! هش پیام ناصحیح پذیرفتن خودکار تصاویر @@ -788,29 +787,29 @@ استفاده از کامپیوتر آرشیو پایگاه داده جدید آرشیو پایگاه داده قدیمی - خطا در شروع گپ - ایمن در برابر اسپم و سو استفاده + خطا در شروع چت + مصونیت در برابر هرزنامه چگونه کار می‌کند تماس تصویری گوشی بلندگو هدفون‌ها - نسل بعدی پیام‌رسانی خصوصی + آینده پیام‌رسانی پایان یافت خطا در باز کردن مرورگر - جهت صدور عبارت عبور تعیین کنید + عبارت عبور برای اکسپورت را تعیین کنید محافظت از صفحه برنامه شروع مجدد ویژگی‌های آزمایشی باز کردن پوشه پایگاه داده - گپ متوقف شده است - خطا در حذف پایگاه داده گپ - خطا در وارد کردن پایگاه داده گپ - شما گپ خود را کنترل می‌کنید! - نمایه، مخاطبان و پیام‌های تحویل داده شده شما روی دستگاهتان ذخیره می‌شوند. - نمایه فقط با مخاطبانتان به اشتراک گذاشته می‌شود. + چت متوقف شده است + خطا در حذف پایگاه داده چت + خطا در ایمپورت کردن پایگاه داده چت + شما چت خود را کنترل می‌کنید! + پروفایل، مخاطبان و پیام‌های تحویل داده شده شما روی دستگاهتان ذخیره می‌شوند. + پروفایل فقط با مخاطبانتان به اشتراک گذاشته می‌شود. نام نمایشی نمی‌تواند شامل نویسه‌های فاصله باشد. - ایجاد نمایه + ایجاد پروفایل نام نامعتبر! برجسته در انتظار تایید… @@ -837,24 +836,24 @@ آن‌ها در تنظیمات مخاطب و گروه قابل جایگزینی هستند. گروه‌های کوچک (حداکثر ۲۰) تنظیمات - اولین بن‌سازه بدون هیچ شناسه کاربری - با طرح‌ریزی خصوصی + هیچ شناسه کاربری وجود ندارد اعلان‌های خصوصی عبارت عبور تصادفی در تنظیمات به صورت متن آشکار ذخیره می‌شود. \nمی‌توانید بعدا آن را تغییر دهید. تماس تصویری رسیده تماس صوتی رسیده - تماس تصویری رمزگذاری سرتاسر شده - تماس صوتی (رمزگذاری سرتاسر نشده) - بدون رمزگذاری سرتاسر - مخاطب رمزگذاری سرتاسر دارد + تماس تصویری با رمزنگاری انتها به انتها + تماس صوتی (بدون رمزنگاری انتها به انتها) + بدون رمزنگاری انتها به انتها + مخاطب رمزنگاری انتها به انتها دارد این اتفاق وقتی می‌افتد که شما یا اتصالتان از پشتیبان پایگاه داده قدیمی استفاده کرده باشید. - مذاکره مجدد رمزگذاری ناموفق بود. - رمزگذاری پرونده‌های محلی + تجدید مذاکره رمزنگاری ناموفق بود. + رمزنگاری فایل‌های محلی پشتیبان‌گیری اطلاعات برنامه قفل بعد از تایید کد عبور کد عبور تغییر نکرد! - ایجاد نمایه + ایجاد پروفایل مورب ما هیچکدام از مخاطبان و پیام‌های(وقتی تحویل داده شدند) شما را روی سرورها ذخیره نمی‌کنیم. رنگی @@ -867,7 +866,7 @@ در حال راه‌اندازی… متصل در حال برقراری تماس… - تماس تصویری (رمزگذاری سرتاسر نشده) + تماس تصویری (بدون رمزنگاری انتها به انتها) رد کردن تعیین کد عبور رسیدها برای گروه‌ها غیرفعال شوند؟ @@ -879,45 +878,45 @@ آزمایشی اتصال شبکه آیکون برنامه - پایگاه داده گپ + پایگاه داده چت بن‌سازه پیام‌رسانی و کاربردی که از حریم خصوصی و امنیت شما محافظت می‌کند. گزینه خوب برای باتری. سرویس پس‌زمینه هر ۱۰ دقیقه پیام‌ها را بررسی می‌کند. ممکن است تماس‌ها یا پیام‌های ضروری را از دست دهید.]]> - پیام‌ها از قلم افتادند + پیام‌های نادیده گرفته شده مرورگر وب پیش‌فرض برای تماس‌ها لازم است. لطفا مرورگر پیش‌فرض را در سیستم تنظیم کنید، و اطلاعات بیشتر را با توسعه‌دهندگان به اشتراک بگذارید. سرور واسط فقط در زمان نیاز مورد استفاده قرار می‌گیرد. طرف دیگری قادر به مشاهده نشانی IP شما خواهد بود. نمایش غیرفعال - گشودن SimpleX Chat برای پذیرفتن تماس + باز کردن SimpleX Chat برای پذیرفتن تماس تماس‌ها از صفحه قفل را از طریق تنظیمات فعال کنید. - مخاطب رمزگذاری سرتاسر ندارد + مخاطب رمزنگاری انتها به انتها ندارد سرورهای WebRTC ICE شناسه پیام بعدی نادرست است (کمتر یا برابر است با قبلی). \nبروز این اتفاق می‌تواند به دلیل وجود اشکال نرم‌افزاری یا مورد حمله قرار گرفتن اتصال باشد. لطفا آن را به توسعه‌دهندگان گزارش دهید. پیام همسان - گپ متوقف شود؟ - پایگاه داده با استفاده از یک عبارت عبور تصادفی رمزگذاری شده، لطفا پیش از صدور آن را تغییر دهید. - خطا در متوقف کردن گپ - خطا در صدور پایگاه داده گپ - پایگاه داده گپ وارد شود؟ + چت متوقف شود؟ + پایگاه داده با استفاده از یک عبارت عبور تصادفی رمزنگاری شده، لطفا پیش از اکسپورت آن را تغییر دهید. + خطا در متوقف کردن چت + خطا در اکسپورت پایگاه داده چت + پایگاه داده چت ایمپورت شود؟ وارد کردن - به منظور استفاده از پایگاه داده گپ وارد شده، برنامه را شروع مجدد کنید. - چند خطای غیر مهلک هنگام وارد کردن رخ داد - برای اطلاعات بیشتر می‌توانید کنسول گپ را ببینید. - پایگاه داده گپ حذف شد - به منظور ایجاد نمایه گپ جدید، برنامه را شروع مجدد کنید. - پرونده‌ها و رسانه - حذف پرونده‌ها برای تمام نمایه‌های گپ - پرونده‌ها و رسانه حذف شوند؟ - هیچ پرونده دریافتی یا ارسالی وجود ندارد + به منظور استفاده از پایگاه داده چت ایمپورت شده، برنامه را شروع مجدد کنید. + چند خطای غیر مهلک هنگام ایمپورت رخ داد: + پایگاه داده چت حذف شد + به منظور ایجاد پروفایل چت جدید، برنامه را شروع مجدد کنید. + فایل‌ها و رسانه + حذف فایل‌ها برای تمام پروفایل‌های چت + فایل‌ها و رسانه حذف شوند؟ + هیچ فایل دریافتی یا ارسالی وجود ندارد هرگز %s ثانیه اجرای اتصال خصوصی جابه‌جایی از دستگاهی دیگر - یک نمایه گپ خالی با نام فراهم شده ایجاد می‌شود، و برنامه به طور معمول باز می‌شود. + یک پروفایل چت خالی با نام فراهم شده ایجاد می‌شود، و برنامه به طور معمول باز می‌شود. اعطای اجازه‌ها در تنظیمات این مجوز را در تنظیمات اندروید پیدا و به صورت دستی آن را اعطا کنید. باز کردن تنظیمات - پروتکل و کد متن‌باز - هر کسی می‌تواند سرورها را راه‌اندازی کند. + هر کسی می‌تواند سرویس‌دهنده میزبانی کند. رسیدها غیرفعال شوند؟ فعال کردن (نگه‌داشتن مقدارهای جایگزین شده) اعطای اجازه‌ها @@ -925,55 +924,54 @@ دوربین دوربین و میکروفون اعطای اجازه‌ها برای برقراری تماس‌ها - حریم خصوصی باز تعریف شده - برای حفاظت از حریم خصوصی، به جای شناسه‌های کاربری مورد استفاده در بن‌سازه‌های دیگر، SimpleX شناسه‌هایی برای صفوف پیام دارد، جدا برای هر کدام از مخاطبان شما. + تعریف مجدد حریم خصوصی + برای حفظ حریم خصوصی شما، SimpleX از شناسه‌های جداگانه برای هر یک از مخاطبان شما استفاده می‌کند. از باتری بیشتر استفاده می‌کند! سرویس پس‌زمینه همیشه در حال اجراست - اعلان‌ها به محض موجود شدن، نمایش داده می‌شوند.]]> وقتی می‌تواند اتفاق بیفتد که: \n۱. پیام‌ها در کلاینت فرستنده بعد از ۲ روز یا روی سرور بعد از ۳۰ روز منقضی شده باشند. \n۲. رمزگشایی پیام ناموفق بود، چون شما یا مخاطبتان از پشتیبان پایگاه داده قدیمی استفاده استفاده کردید. \n۳. اتصال مورد حمله قرار گرفته باشد. - تصاویر نمایه - به منظور صدور، ورود و حذف پایگاه داده گپ، گپ را متوقف کنید. هنگامی که گپ متوقف شده است، شما قادر به دریافت و ارسال پیام نخواهید بود. - پایگاه داده گپ فعلی شما حذف و توسط پایگاه داده وارد شده جایگزین خواهد شد. -\nاین عمل قابل برگشت نیست - نمایه، مخاطبان، پیام‌ها و پرونده‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت. - پایگاه داده گپ شما - این عمل قابل برگشت نیست - نمایه، مخاطبان، پیام‌ها و پرونده‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت. + تصاویر پروفایل + چت را متوقف کنید تا بتوانید پایگاه داده چت را اکسپورت، ایمپورت یا حذف کنید. در حالی که چت متوقف است، نمی‌توانید پیام‌ها را ارسال یا دریافت کنید. + پایگاه داده چت فعلی شما حذف و توسط پایگاه داده ایمپورت شده جایگزین خواهد شد. \nاین عمل قابل برگشت نیست - پروفایل، مخاطبان، پیام‌ها و فایل‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت. + پایگاه داده چت شما + این عمل قابل برگشت نیست - پروفایل، مخاطبان، پیام‌ها و فایل‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت. ارسال رسید برای %d مخاطب غیرفعال است - این عمل قابل برگشت نیست - تمام پرونده‌ها و رسانه دریافتی حذف خواهند شد. عکس‌های با کیفیت پایین باقی خواهند ماند. - شما باید از تازه‌ترین نسخه پایگاه داده گپ خود روی فقط یک دستگاه استفاده کنید، در غیر این صورت ممکن است از بعضی از مخاطب‌ها ‌دیگر پیامی دریافت نکنید. + این عمل قابل برگشت نیست - تمام فایل‌ها و رسانه دریافتی حذف خواهند شد. عکس‌های با کیفیت پایین باقی خواهند ماند. + شما باید از تازه‌ترین نسخه پایگاه داده چت خود روی فقط یک دستگاه استفاده کنید، در غیر این صورت ممکن است از بعضی از مخاطب‌ها ‌دیگر پیامی دریافت نکنید. پیام‌ها این عمل قابل برگشت نیست - پیام‌های ارسالی و دریافتی قدیمی‌تر از زمان انتخابی حذف خواهند شد. این کار ممکن است چندین دقیقه زمان ببرد. خطا در تغییر تنظیمات ذخیره عبارت عبور در تنظیمات حذف پیام‌ها ذخیره عبارت عبور در مخزن کلید - این تنظیمات بر پیام‌های موجود در نمایه گپ فعلی شما اعمال می‌شود + این تنظیمات بر پیام‌های موجود در پروفایل چت فعلی شما اعمال می‌شود حذف خودکار پیام فعال شود؟ برگرداندن - ارتقا و گشودن گپ + ارتقا و باز کردن چت دعوت به گروه %1$s به گروه می‌پیوندید؟ ترک - دریافت پیام‌ها از این گروه برای شما متوقف خواهد شد. تاریخچه گپ حفظ خواهد شد. + دریافت پیام‌ها از این گروه برای شما متوقف خواهد شد. تاریخچه چت حفظ خواهد شد. دعوت اعضا %d رویداد گروه %s، %s و %d عضو دیگر متصل شدند %s و %s %s، %s و %d عضو و %d رویداد دیگر - گشودن + باز کردن کد امنیتی تغییر پیدا کرد وضعیت ناشناخته سازنده عبارت عبور جدید… - پایگاه داده رمزگذاری و عبارت عبور در تنظیمات ذخیره خواهد شد. - عبارت عبور رمزگذاری پایگاه داده به‌روز و در تنظیمات ذخیره خواهد شد. + پایگاه داده رمزنگاری و عبارت عبور در تنظیمات ذخیره خواهد شد. + عبارت عبور رمزنگاری پایگاه داده به‌روز و در تنظیمات ذخیره خواهد شد. امکان دسترسی مخزن کلید برای ذخیره کلمه عبور پایگاه داده وجود ندارد خطای پایگاه داده ناشناخته: %s تایید جابه‌جایی نامعتبر شما به این گروه پیوستید شما ترک کردید - نمایه گروه به‌روز شد + پروفایل گروه به‌روز شد عضو مدیر صاحب @@ -982,41 +980,41 @@ عبارت عبور از تنظیمات پاک شود؟ لطفا توجه داشته باشید: اگر عبارت عبور را از دست بدهید، قادر نخواهید بود آن را بازیابی کنید یا تغییر دهید.]]> عبارت عبور پایگاه داده اشتباه - پرونده: %s - عبارت عبور پایگاه داده برای گشودن گپ الزامی است. + فایل: %s + عبارت عبور پایگاه داده برای باز کردن چت الزامی است. خطای ناشناخته - گشودن گپ + باز کردن چت تلاش برای تغییر عبارت عبور پایگاه داده کامل نشد. ارتقای پایگاه داده تنزل پایگاه داده تایید ارتقای پایگاه داده گروه را ترک می‌کنید؟ امکان دعوت مخاطبان وجود ندارد! - در حال استفاده از نمایه ناشناس برای این گروه هستید - برای جلوگیری از اشتراک‌گذاری نمایه اصلی شما، دعوت مخاطبان مجاز نیست + در حال استفاده از پروفایل ناشناس برای این گروه هستید - برای جلوگیری از اشتراک‌گذاری پروفایل اصلی شما، دعوت مخاطبان مجاز نیست مخاطب حذف شد متصل شد ترک کرد %1$s حذف شد - شما حذف شدید - توافق رمزگذاری + شما را حذف کرد + رمزنگاری مورد توافق قرار گرفت ناظر - عضو پیشین %1$s + اعضا %1$s بسط دادن انتخاب نقش - عبارت عبور رمزگذاری پایگاه داده به‌روز خواهد شد. - مستقیما متصل شد + عبارت عبور رمزنگاری پایگاه داده به‌روز خواهد شد. + درخواست اتصال کرد %s متصل شد پیوستن به گروه به‌روزرسانی تایید عبارت عبور جدید… - پایگاه داده رمزگذاری شود؟ + پایگاه داده رمزنگاری شود؟ نسخه پایگاه داده ناسازگار - نمایه گروه به‌روز شد + پروفایل گروه به‌روز شده شما نشانی را برای %s تغییر دادید شما نشانی را تغییر دادید - مذاکره مجدد رمزگذاری مجاز است + تجدید مذاکره رمزنگاری مجاز است گروه حذف شد در حال اتصال (دعوت معرفی) - رمزگذاری سرتاسر استاندارد + رمزنگاری استاندارد انتها به انتها در حال اتصال (پذیرفته شد) در حال اتصال (اعلام شد) متصل شد @@ -1035,48 +1033,48 @@ از %1$s دعوت شده نقش %s به %s تغییر کرد گروه حذف شد - مذاکره مجدد رمزگذاری الزامی است - رمزگذاری برای %s بی‌عیب است - مذاکره مجدد رمزگذاری برای %s مجاز است - رمزگذاری سرتاسر مقاوم در برابر کوانتوم + تجدید مذاکره رمزنگاری الزامی است + رمزنگاری برای %s مورد تایید است + تجدید مذاکره رمزنگاری برای %s مجاز است + رمزنگاری انتها به انتهای مقاوم در برابر کوانتوم مخاطبی برای افزودن وجود ندارد - رد شدن از دعوت اعضا + نادیده گرفتن دعوت از اعضا انتخاب مخاطبان - پایگاه داده رمزگذاری شده! + پایگاه داده رمزنگاری شده! عبارت عبور فعلی… عضو %1$s به %2$s تغییر کرد - رمزگذاری + رمزنگاری به‌روزرسانی عبارت عبور پایگاه داده تعیین عبارت عبور از مخزن کلید اندروید برای ذخیره امن عبارت عبور استفاده می‌شود - به سرویس اعلان اجازه عمل می‌دهد. باید هر بار که برنامه شروع می‌شود عبارت عبور را وارد کنید - در دستگاه ذخیره نمی‌شود. - پایگاه داده رمزگذاری شده + پایگاه داده رمزنگاری شده مخاطب بررسی شد پاک کردن نمایش کنسول در پنجره جدید - می‌توانید گپ را از طریق تنظیمات برنامه / پایگاه داده یا با شروع مجدد برنامه شروع کنید. - گپ شروع شود؟ + می‌توانید چت را از طریق تنظیمات برنامه / پایگاه داده یا با شروع مجدد برنامه شروع کنید. + چت شروع شود؟ هشدار: ممکن است بعضی از اطلاعات را از دست بدهید! - گپ متوقف شده است - خطا در رمزگذاری پایگاه داده + چت متوقف شده است + خطا در رمزنگاری پایگاه داده عبارت عبور از مخزن کلید پاک شود؟ اعلان‌ها فقط تا زمان توقف برنامه تحویل داده خواهند شد! پاک کردن تعیین عبارت عبور پایگاه داده - لطفا عبارت عبور فعلی درست را وارد کنید. - پایگاه داده گپ شما رمزگذاری نشده است - برای محافظت از آن عبارت عبور تعیین کنید. + لطفا عبارت عبور صحیح فعلی را وارد کنید. + پایگاه داده چت شما رمزنگاری نشده است - برای محافظت از آن عبارت عبور تعیین کنید. عبارت عبور به صورت متن آشکار در تنظیمات ذخیره شده است. بعد از تغییر عبارت عبور یا شروع مجدد برنامه، عبارت عبور به صورت متن آشکار در تنظیمات ذخیره خواهد شد. عبارت عبور پایگاه داده تغییر داده شود؟ - پایگاه داده رمزگذاری خواهد شد. - لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر نخواهید بود به گپ دسترسی پیدا کنید. + پایگاه داده رمزنگاری خواهد شد. + لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر نخواهید بود به چت دسترسی پیدا کنید. خطا در پایگاه داده خطا در Keychain عبارت عبور پایگاه داده با آنچه در مخزن کلید ذخیره شده متفاوت است. خطا: %s عبارت عبور اشتباه! ورود عبارت عبور… - ذخیره عبارت عبور و گشودن گپ + ذخیره عبارت عبور و باز کردن چت برگرداندن پشتیبان پایگاه داده پشتیبان پایگاه داده برگردانده شود؟ لطفا بعد از برگرداندن پشتیبان پایگاه داده، کلمه عبور قبلی را وارد کنید. این عمل قابل برگشت نیست. @@ -1088,8 +1086,8 @@ پیوستن به صورت ناشناس این گروه دیگر وجود ندارد. شما دعوت گروه ارسال کردید - برای پیوستن لمس کنید - برای پیوستن به صورت ناشناس لمس کنید + برای پیوستن ضربه بزنید + برای پیوستن به صورت ناشناس ضربه بزنید %s مسدود شد مسدود سازی %s لغو شد نقش شما به %s تغییر کرد @@ -1101,28 +1099,28 @@ در حال تغییر نشانی… در حال تغییر نشانی برای %s… در حال تغییر نشانی… - رمزگذاری بی‌عیب است - در حال توافق رمزگذاری برای %s… - توافق رمزگذاری برای %s + رمزنگاری مورد تایید است + در حال توافق رمزنگاری برای %s… + رمزنگاری برای %s مورد توافق قرار گرفت نویسنده دعوت شد در حال اتصال (معرفی شد) نقش آغازین مخاطبی انتخاب نشده - در حال توافق رمزگذاری… + در حال توافق رمزنگاری… ترک کرد مخاطب %1$s به %2$s تغییر کرد نشانی مخاطب حذف شد تعیین نشانی مخاطب جدید - پایگاه داده با استفاده از عبارت عبور تصادفی رمزگذاری شده، می‌توانید آن را تغییر دهید. + پایگاه داده با استفاده از عبارت عبور تصادفی رمزنگاری شده، می‌توانید آن را تغییر دهید. بعد از شروع مجدد برنامه یا تغییر عبارت عبور، از مخزن کلید اندروید برای ذخیره امن عبارت عبور استفاده خواهد شد - اجازه دریافت اعلان‌ها را خواهد داد. - پایگاه داده رمزگذاری و عبارت عبور در مخزن کلید ذخیره خواهد شد. - عبارت عبور رمزگذاری پایگاه داده به‌روز و در مخزن کلید ذخیره خواهد شد. + پایگاه داده رمزنگاری و عبارت عبور در مخزن کلید ذخیره خواهد شد. + عبارت عبور رمزنگاری پایگاه داده به‌روز و در مخزن کلید ذخیره خواهد شد. عبارت عبور درست را وارد کنید. لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر به تغییرش نخواهید بود. - عبارت عبور در مخزن کلید پیدا نشد، لطفا به صورت دستی آن را وارد کنید. دلیل این اتفاق ممکن است برگرداندن اطلاعات برنامه با استفاده از یک ابزار پشتیبان‌گیری باشد. اگر این طور نیست، لطفا با توسعه دهندگان تماس بگیرید. - تنزل و گشودن گپ - گپ متوقف شده است. اگر از پیش از این پایگاه داده روی دستگاه دیگری استفاده می‌کردید، بهتر است قبل از شروع گپ، آن را برگردانید. + عبارت عبور در کی‌استور پیدا نشد، لطفاً آن را به‌صورت دستی وارد کنید. این ممکن است در صورتی اتفاق افتاده باشد که داده‌های برنامه را با استفاده از ابزار پشتیبان‌گیری بازیابی کرده‌اید. اگر این مورد نیست، لطفاً با توسعه‌دهندگان تماس بگیرید. + تنزل و باز کردن چت + چت متوقف شده است. اگر از پیش از این پایگاه داده روی دستگاه دیگری استفاده می‌کردید، بهتر است قبل از شروع چت، آن را برگردانید. به این گروه پیوستید. در حال اتصال به عضوی از گروه که از شما دعوت کرد. دعوت منقضی شد! دعوت گروه منقضی شد @@ -1130,30 +1128,30 @@ شما نقش %s را به %s تغییر دادید شما %s را مسدود کردید شما %1$s را حذف کردید - عکس نمایه حذف شد - تعیین عکس نمایه جدید - نمایه به‌روز شد - مذاکره مجدد رمزگذاری برای %s الزامی است + عکس پروفایل حذف شد + تعیین عکس پروفایل جدید + پروفایل به‌روز شد + تجدید مذاکره رمزنگاری برای %s الزامی است در حال ارسال از طریق اتصال اصلاح شود؟ اصلاح توسط عضو گروه پشتیبانی نمی‌شود - نمایه گپ شما به اعضای گروه ارسال خواهد شد + پروفایل چت شما به اعضای گروه ارسال خواهد شد ایجاد لینک خطا در به‌روزرسانی لینک گروه - زمان توقف پروتکل + وقفه پروتکل می‌توانید این نشانی را با مخاطبان خود به اشتراک بگذارید تا به آن‌ها اجازه دهید به %s متصل شوند. غیرفعال رسیدها غیرفعال هستند برای کنسول حذف به‌روزرسانی تنظیمات کلاینت را دوباره به سرورها متصل خواهد کرد. - نمایه گپ حذف شود؟ - خصوصی کردن نمایه! - می‌توانید نمایه کاربر را پنهان یا بی‌صدا کنید - برای نمایش منو لمس کنید و‍ نگه دارید. - لغو پنهان‌سازی نمایه - لغو پنهان‌سازی نمایه گپ - کلمه عبور نمایه - نمایه تصادفی شما + پروفایل چت حذف شود؟ + خصوصی کردن پروفایل! + می‌توانید پروفایل کاربر را پنهان یا بی‌صدا کنید - برای نمایش منو لمس کنید و‍ نگه دارید. + لغو پنهان‌سازی پروفایل + لغو پنهان‌سازی پروفایل چت + کلمه عبور پروفایل + پروفایل تصادفی شما ابتدایی پیام ارسالی خیر @@ -1168,7 +1166,7 @@ می‌توانید یک لینک یا کد QR به اشتراک بگذارید - هر کسی می‌تواند به گروه بپیوندد. اگر بعدا گروه را حذف کنید، اعضای گروه را از دست نخواهید داد. خطا در ایجاد لینک گروه خطا در ارسال دعوت - اشتراک‌گذاری نشانی + اشتراک‌گذاری آدرس این گروه بیش از %1$d عضو دارد، رسیدهای تحویل ارسال نمی‌شوند. نام محلی شناسه پایگاه داده @@ -1192,10 +1190,10 @@ اتصال مستقیم؟ درخواست اتصال به این عضو گروه ارسال خواهد شد. پیام خوشامدگویی ذخیره شود؟ - مذاکره مجدد رمزگذاری - ذخیره نمایه گروه + تجدید مذاکره رمزنگاری + ذخیره پروفایل گروه به‌روزرسانی - شما هنوز تماس‌ها و اعلان‌های نمایه‌های بی‌صدا را وقتی فعال هستند دریافت می‌کنید. + شما هنوز تماس‌ها و اعلان‌های پروفایل‌های بی‌صدا را وقتی فعال هستند دریافت می‌کنید. ناشناس سیستم وارد کردن تم @@ -1218,20 +1216,20 @@ روشن خطا در وارد کردن تم مخاطب اجازه می‌دهد - شما در حال دعوت از مخاطبی که با او نمایه ناشناسی به اشتراک گذاشته‌اید به گروهی هستید که در آن از نمایه اصلی خود استفاده می‌کنید + شما در حال دعوت از مخاطبی که با او پروفایل ناشناسی به اشتراک گذاشته‌اید به گروهی هستید که در آن از پروفایل اصلی خود استفاده می‌کنید حذف گروه افزودن پیام خوشامدگویی وضعیت شبکه - نمایه گروه روی دستگاه‌های اعضا ذخیره می‌شود، نه روی سرورها. - اتصال‌های نمایه و سرور + پروفایل گروه روی دستگاه‌های اعضا ذخیره می‌شود، نه روی سرورها. + اتصال‌های پروفایل و سرور (فعلی) عضو عضو از گروه حذف خواهد شد - این عمل قابل برگشت نیست! مسدود برای همه اصلاح توسط مخاطب پشتیبانی نمی‌شود ثانیه - تمام گپ‌ها و پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! - فقط اطلاعات نمایه محلی + تمام چت‌ها و پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! + فقط اطلاعات پروفایل محلی دعوت اعضا گروه حذف شود؟ گروه برای تمام اعضا حذف خواهد شد - این عمل قابل برگشت نیست! @@ -1242,27 +1240,27 @@ توسط مدیر حذف شد در ایجاد شد در: %s عضو مسدود شود؟ - ویرایش نمایه گروه + ویرایش پروفایل گروه حذف لینک خطا در ایجاد مخاطب عضو %s در %s %s (فعلی) نقش - ذخیره و به‌روزرسانی نمایه گروه - وقتی نمایه ناشناسی را با کسی به اشتراک می‌گذارید، این نمایه برای گروه‌هایی که شما را به آن‌ها دعوت می‌کند استفاده خواهد شد. + ذخیره و به‌روزرسانی پروفایل گروه + وقتی پروفایل ناشناسی را با کسی به اشتراک می‌گذارید، این پروفایل برای گروه‌هایی که شما را به آن‌ها دعوت می‌کند استفاده خواهد شد. نام گروه را وارد کنید: ایجاد گروه - خطا در ذخیره نمایه گروه + خطا در ذخیره پروفایل گروه بازنشاندن به پیش‌فرض‌ها - صدور تم + اکسپورت تم خطا در حذف عضو خطا در تغییر نقش - ثانوی + ثانویه ارسال شد در: %s %s: %s پیام ذخیره شده تغییر نقش - حذف نمایه + حذف پروفایل شما: %1$s تمام اعضای گروه متصل باقی خواهند ماند. تنها صاحبان گروه می‌توانند تنظیمات گروه را تغییر دهند. @@ -1274,8 +1272,8 @@ لغو مسدودسازی عضو لغو مسدودسازی برای همه پیام‌های %s نشان داده خواهند شد! - نقش به «%s» تغییر داده خواهد شد. تمام افراد گروه مطلع خواهند شد. - نقش به «%s» تغییر داده خواهد شد. عضو یک دعوت جدید دریافت خواهد کرد. + نقش به %s تغییر خواهد کرد. همه در گروه مطلع خواهند شد. + نقش به %s تغییر خواهد کرد. عضو یک دعوت‌نامه جدید دریافت خواهد کرد. خطا در مسدودسازی عضو برای همه گروه اتصال @@ -1292,28 +1290,28 @@ ایجاد گروه محرمانه تماما نامتمرکز - قابل مشاهده فقط توسط اعضا. نام کامل گروه: - زمان توقف پروتکل در کیلوبایت - دریافت همزمان - زمان توقف اتصال TCP - وقفه پینگ - شمار پینگ + وقفه پروتکل به کیلوبایت + همزمانی دریافت + وقفه زمانی اتصال TCP + فاصله زمانی بین هر پینگ + تعداد پینگ فعال کردن زنده نگه‌داشتن TCP ذخیره تنظیمات شبکه به‌روزرسانی شود؟ - افزودن نمایه + افزودن پروفایل لغو پنهان‌سازی لغو بی‌صدا کلمه عبور را در جستجو وارد کنید - برای فعال‌سازی نمایه لمس کنید. + برای فعال‌سازی پروفای ضربه بزنید. دوباره نمایش داده نشود بی‌صدا هنگام غیرفعال بودن! - حذف نمایه گپ - حالت ناشناس از حریم خصوصی شما با استفاده از یک نمایه تصادفی جدید برای هر مخاطب محافظت می‌کند. - اجازه می‌دهد اتصال‌های بی‌نام زیادی داشته باشید بدون اطلاعات مشترک بین آن‌ها در تنها یک نمایه گپ. + حذف پروفایل چت + حالت ناشناس از حریم خصوصی شما با استفاده از یک پروفایل تصادفی جدید برای هر مخاطب محافظت می‌کند. + اجازه می‌دهد اتصال‌های بی‌نام زیادی داشته باشید بدون اطلاعات مشترک بین آن‌ها در تنها یک پروفایل چت. تاریک SimpleX تم تاریک - مطمئن شوید پرونده دارای ترکیب YAML صحیح است. برای داشتن یک نمونه از ساختار پرونده تم، تم را صادر کنید. + مطمئن شوید فایل دارای ترکیب YAML صحیح است. برای داشتن یک نمونه از ساختار فایل تم، تم را اکسپورت کنید. ثانوی اضافی ابتدایی اضافی پس‌زمینه @@ -1330,33 +1328,33 @@ فقط شما می‌توانید پیام‌های صوتی ارسال کنید. هر دوی شما و مخاطبتان می‌توانید واکنش‌های پیام اضافه کنید. فقط شما می‌توانید واکنش‌های پیام اضافه کنید. - ارسال پیام‌های ناپدید شونده را منع می‌کنید. + ممنوع کردن ارسال پیام‌های ناپدید شونده. اجازه ارسال پیام‌های مستقیم را به اعضا می‌دهید. - اجازه ارسال پرونده‌ها و رسانه را می‌دهید. - اعضای گروه می‌توانند پیام‌های ارسالی را به صورت غیرقابل برگشت حذف کنند. (۲۴ ساعت) + اجازه ارسال فایل‌ها و رسانه را می‌دهید. + اعضا می‌توانند پیام‌های ارسالی را به صورت غیرقابل برگشت حذف کنند. (۲۴ ساعت) حذف بعد از تنظیمات گفت‌و‌گو - پرونده‌ها و رسانه + فایل‌ها و رسانه تماس‌های صوتی/تصویری " \nموجود در نسخه 5.1" به مخاطبان خود اجازه ارسال پیام‌های صوتی می‌دهید. فقط زمانی اجازه حذف پیام‌ها به صورت غیرقابل برگشت را می‌دهید که مخاطب شما این اجازه را به شما بدهد. (۲۴ ساعت) - پیام‌های ناپدید شونده در این گروه ممنوع هستند. + پیام‌های ناپدید شونده ممنوع است. تعیین تنظیمات گروه واکنش‌های پیام - حذف پیام به صورت غیرقابل برگشت در این گپ ممنوع است. + حذف پیام به صورت غیرقابل برگشت در این چت ممنوع است. %d هفته به مخاطبان خود اجازه ارسال پیام‌های ناپدید شونده دهید. فقط وقتی پیام‌های صوتی را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. - واکنش‌های پیام در این گپ ممنوع هستند. - ارسال پیام‌های صوتی را منع می‌کنید. - اعضای گروه می‌توانند پیام‌های صوتی ارسال کنند. + واکنش‌های پیام در این چت ممنوع هستند. + ممنوع کردن ارسال پیام‌های صوتی. + اعضا می‌توانند پیام‌های صوتی ارسال کنند. پذیرفتن هر دوی شما و مخاطبتان می‌توانید پیام‌های ناپدید شونده ارسال کنید. فقط شما می‌توانید پیام‌های ناپدید شونده ارسال کنید. - حذف پیام به صورت غیرقابل برگشت را منع می‌کنید. - واکنش‌های پیام‌ها را منع می‌کنید. + ممنوع کردن حذف غیرقابل بازگشت پیام. + ممنوع کردن واکنش به پیام‌ها. ارسال ۱۰۰ پیام آخر به اعضای جدید. تا ۱۰۰ پیام آخر به اعضای جدید ارسال خواهد شد. %d ماه @@ -1364,12 +1362,12 @@ %d دقیقه فقط وقتی تماس‌ها را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. منع تماس‌های صوتی/تصویری. - ارسال پرونده‌ها و رسانه را منع می‌کنید. + ممنوع کردن ارسال فایل‌ها و رسانه‌ها. اجازه ارسال لینک‌های SimpleX را می‌دهید. تاریخچه به اعضای جدید ارسال نمی‌شود. - اعضای گروه می‌توانند واکنش‌های پیام اضافه کنند. - اعضای گروه می‌توانند لینک‌های SimpleX ارسال کنند. - لینک‌های SimpleX در این گروه ممنوع هستند. + اعضا می‌توانند به پیام‌ها واکنش نشان دهند. + اعضا می‌توانند لینک‌های SimpleX را ارسال کنند. + لینک‌های SimpleX ممنوع هستند. %d ثانیه %d دقیقه %d ماه @@ -1379,9 +1377,9 @@ مطالعه بیشتر هر دوی شما و مخاطبتان می‌توانید پیام‌های صوتی ارسال کنید. فقط وقتی واکنش‌های پیام را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. - منع واکنش‌های پیام. + منع واکنش‌ها به پیام. فقط زمانی پیام‌های ناپدید شونده را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز بداند. - ارسال پیام‌های ناپدید شونده را منع می‌کنید. + ممنوع کردن ارسال پیام‌های ناپدید شونده. فقط مخاطبتان می‌تواند پیام‌های صوتی ارسال کند. %d روز مخاطبان می‌توانند پیام‌ها را برای حذف علامت بگذارند؛ شما قادر به مشاهده آن‌ها خواهید بود. @@ -1397,11 +1395,11 @@ فعال برای شما فعال برای مخاطب خاموش - دریافتی، ممنوع + دریافت شد، ممنوع شد تعیین ۱ روز - منع ارسال پیام‌ها صوتی. + ممنوع کردن ارسال پیام‌ها صوتی. به مخاطبان خود اجازه تماس با شما را می‌دهید. - پیام‌های ناپدید شونده در این گپ ممنوع هستند. + پیام‌های ناپدید شونده در این چت ممنوع هستند. هر دوی شما و مخاطبتان می‌توانید پیام‌ها را به صورتی غیرقابل برگشت حذف کنید. (۲۴ ساعت) هر دوی شما و مخاطبتان می‌توانید تماس برقرار کنید. فقط شما می‌توانید تماس برقرار کنید. @@ -1410,16 +1408,16 @@ اجازه ارسال پیام‌های ناپدید شونده می‌دهید. اجازه ارسال پیام‌های صوتی را می‌دهید. اجازه واکنش‌های پیام را می‌دهید. - ارسال لینک‌های SimpleX را منع می‌کنید + ممنوع کردن ارسال لینک‌های SimpleX عدم ارسال تاریخچه به اعضای جدید. - اعضای گروه می‌توانند پیام‌های ناپدید شونده ارسال کنند. - اعضای گروه می‌توانند پیام‌های مستقیم ارسال کنند. + اعضا می‌توانند پیام‌های ناپدید شونده ارسال کنند. + اعضا می‌توانند پیام‌های مستقیم ارسال کنند. پیام‌های مستقیم بین اعضا در این گروه ممنوع هستند. - حذف غیرقابل برگشت در این گروه ممنوع است. - پیام‌های صوتی در این گروه ممنوع هستند. - واکنش‌های پیام در این گروه ممنوع هستند. - اعضای گروه می‌توانند پرونده‌ها و رسانه ارسال کنند. - پرونده‌ها و رسانه در این گروه ممنوع هستند. + حذف پیام‌های غیرقابل بازگشت ممنوع است. + پیام‌های صوتی ممنوع است. + واکنش به پیام‌ها ممنوع است. + اعضا می‌توانند فایل‌ها و رسانه‌ها را ارسال کنند. + فایل‌ها و رسانه‌ها ممنوع هستند. %d ثانیه %d ساعت %d ساعت @@ -1434,10 +1432,10 @@ تمام اعضا فعال برای چی جدید است - پیام‌های صوتی در این گپ ممنوع هستند. + پیام‌های صوتی در این چت ممنوع هستند. فقط مخاطبتان می‌تواند واکنش‌های پیام اضافه کند. اجازه حذف پیام‌های ارسالی به صورت غیرقابل برگشت را می‌دهید. (۲۴ ساعت) - ارسال پیام‌های مستقیم به اعضا را منع می‌کنید. + ممنوع کردن ارسال پیام‌های مستقیم به اعضا. ظرفیت از محدودیت فراتر رفت - گیرنده پیام‌های ارسالی پیشین را دریافت نکرد. خطای سرور مقصد: %1$s خطا: %1$s @@ -1492,15 +1490,15 @@ عبارت عبور را وارد کنید خطا در حذف پایگاه داده کدهای امنیتی را با مخاطبان خود مقایسه کنید. - برنامه پرونده‌های جدید محلی (به جز ویدئوها) را رمزگذاری می‌کند. - رمزگذاری پرونده‌ها و رسانه ذخیره شده + برنامه فایل‌های جدید محلی (به جز ویدئوها) را رمزنگاری می‌کند. + رمزنگاری فایل‌ها و رسانه ذخیره شده به وسیله پروتکل امن مقاوم در برابر کوانتوم. مسدودسازی اعضای گروه خطا %s قطع شد، به دلیل: %s]]> کامپیوتر رابط چینی و اسپانیایی - به وسیله نمایه گپ (پیش‌فرض) یا به وسیله اتصال (آزمایشی). + به وسیله پروفایل چت (پیش‌فرض) یا به وسیله اتصال (آزمایشی). در حین اتصال به کامپیوتر، مهلت زمان اتصال تمام شد. روز‍ فعال کردن @@ -1521,9 +1519,9 @@ اتصال به صورت خودکار قابل کشف از طریق شبکه محلی سلولی - ایجاد نمایه جدید در برنامه کامپیوتر. 💻 + ایجاد پروفایل جدید در برنامه کامپیوتر. 💻 نشانی کامپیوتر - الصاق نشانی کامپیوتر + الصاق نشانی دسکتاپ یافتن از طریق شبکه محلی تمام اطلاعات وقتی وارد می‌شوند پاک می‌شوند. سفارشی کردن و اشتراک‌گذاری تم‌های رنگ. @@ -1532,24 +1530,24 @@ \n- رسیدهای تحویل (تا ۲۰ دقیقه). \n- سریع‌تر و پایداری بیشتر. گروه‌های بهتر - فعال کردن در گپ های مستقیم (آزمایشی)! + فعال کردن در چت‌های مستقیم (آزمایشی)! اتصال با کامپیوتر قطع شود؟ به زودی! بارگیری موفق نبود - گپ جابه‌جا شد! + چت منتقل شد! آرشیو و بارگذاری تایید بارگذاری اتصال اینترنت خود را بررسی و دوباره امتحان کنید برنامه کامپیوتر جدید وصل کردن برنامه‌های موبایل و کامپیوتر! 🔗 یافتن و پیوستن به گروه‌ها - ایجاد یک گروه با استفاده از یک نمایه تصادفی. + ایجاد یک گروه با استفاده از یک پروفایل تصادفی. جابه‌جایی اطلاعات برنامه استفاده از کامپیوتر را در برنامه موبایل باز و کد QR را اسکن کنید.]]> در حال بارگیری آرشیو - خطا در صدور پایگاه داده گپ + خطا در اکسپورت پایگاه داده چت خطا در ذخیره تنظیمات - تمام مخاطبان، مکالمات و پرونده‌های شما به صورت امن، رمزگذاری و به صورت بسته‌های داده به واسطه‌های XFTP تنظیم شده، بارگذاری خواهند شد. + تمام مخاطبان، مکالمات و فایل‌های شما به صورت امن، رمزنگاری و به صورت بسته‌های داده به واسطه‌های XFTP تنظیم شده، بارگذاری خواهند شد. موبایل متصل شد مدیران می‌توانند لینک‌ها را برای پیوستن به گروه‌ها ایجاد کنند. قطع اتصال موبایل‌ها @@ -1582,14 +1580,14 @@ نمایش وضعیت پیام برای محافظت از نشانی IP شما، مسیریابی خصوصی از سرورهای SMP شما به منظور تحویل پیام‌ها استفاده می‌کند. مسیریابی پیام خصوصی - مسیریابی خصوصی + مسیردهی خصوصی تنظیمات سرور بهبودیافته با پیام خوشامدگویی اختیاری. مخاطبان شما می‌توانند اجازه حذف کامل پیام را بدهند. - چندین نمایه گپ - نام‌های پرونده خصوصی - پالایش گپ‌های خوانده نشده و برگزیده. - اصلاح رمزگذاری بعد از برگرداندن پشتیبان‌ها. + چندین پروفایل چت + نام‌های فایل خصوصی + فیلتر کردن چت‌های خوانده‌نشده و موردعلاقه. + اصلاح رمزنگاری بعد از برگرداندن پشتیبان‌ها. مدیریت گروه رابط کاربری ژاپنی و پرتقالی ناپدید کردن یک پیام @@ -1601,8 +1599,8 @@ \n- ویرایش تاریخچه. پیوستن سریع‌تر و پیام‌های قابل اطمینان تر. باز فرستادن و ذخیره پیام‌ها - مربع، دایره، و هر چیزی در این بین. - در گپ‌های مستقیم فعال خواهد شد! + مربع، دایره، یا هر چیزی در این بین. + در چت‌های مستقیم فعال خواهد شد! ارسال رسیدهای تحویل برای تمام مخاطبان فعال خواهد شد. می‌توانید بعدا از طریق تنظیمات آن را فعال کنید نام این دستگاه @@ -1612,7 +1610,7 @@ در انتظار متصل شدن موبایل: %s مشغول است]]> تصادفی - تجدید + تازه‌سازی %s نسخه پشتیبانی نشده دارد. لطفا، اطمینان حاصل کنید که از نسخه یکسان روی هر دو دستگاه استفاده می‌کنید]]> %s قطع شد]]> این ویژگی هنوز پشتیبانی نمی‌شود. انتشار بعدی را امتحان کنید. @@ -1620,22 +1618,22 @@ گروه از قبل وجود دارد! شما هم اکنون در حال پیوستن به گروه از طریق این لینک هستید. در حال وارد کردن آرشیو - یا لینک آرشیو را الصاق کنید + یا لینک آرشیو را پیست کنید الصاق لینک آرشیو می‌توانید دوباره امتحان کنید. - پرونده حذف شد یا لینک نامعتبر است + فایل حذف شد یا لینک نامعتبر است جابه‌جایی دستگاه در حال آماده‌سازی بارگذاری نهایی‌سازی جابه‌جایی - یا لینک پرونده را به صورت امن به اشتراک بگذارید - شروع گپ + یا لینک فایل را به صورت امن به اشتراک بگذارید + شروع چت اترنت باسیم بهبودهای بیشتر به زودی! سازگار نیست! تایید اتصال‌ها %s غیرفعال است]]> شما از پیش اتصال به وسیله این نشانی را درخواست کرده‌اید! - شروع مجدد گپ + شروع مجدد چت حتی وقتی در مکالمه غیرفعال باشند. خیر موبایل متصلی وجود ندارد @@ -1646,21 +1644,21 @@ محفوظ نگه داشتن پیش‌نویس پیام آخر، به همراه ضمیمه‌ها. درخواست اتصال تکرار شود؟ از موبایل اسکن کنید - ارسال رسیدهای تحویل برای تمام مخاطبان در تمام نمایه‌های گپ قابل مشاهده، فعال خواهد شد. + ارسال رسیدهای تحویل برای تمام مخاطبان در تمام پروفایل‌های چت قابل مشاهده، فعال خواهد شد. پیام‌ها مستقیما ارسال شود وقتی نشانی IP محافظت می‌شود و سرور مقصد شما از مسیریابی خصوصی پشتیبانی نمی‌کند. پیام‌ها مستقیما ارسال شود وقتی سرور مقصد شما از مسیریابی خصوصی پشتیبانی نمی‌کند. - شکل دادن به تصاویر نمایه + شکل دادن به تصاویر پروفایل با سپاس از کاربران - از طریق Weblate همکاری کنید! با سپاس از کاربران - از طریق Weblate همکاری کنید! این لینک یک‌بارمصرف خودتان است! این نشانی‌ SimpleX خودتان است! - واسطه‌های ناشناخته + سرورهای ناشناخته محافظت نشده تایید اتصال تایید عبارت عبور پایگاه داده وقتی IP پنهان است شما هم اکنون در حال اتصال از طریق لینک یک‌بارمصرف هستید! - نمایه‌های گپ پنهان + پروفایل‌های چت پنهان (این دستگاه v%s)]]> متصل کردن یک موبایل پیش‌نویس پیام @@ -1668,7 +1666,7 @@ پیام‌های صوتی پیام‌های ارسال شده بعد زمان تعیین شده حذف خواهند شد. رابط فرانسوی - به وسیله یک کلمه عبور از نمایه‌های گپ خود محافظت کنید! + به وسیله یک کلمه عبور از پروفایل‌های چت خود محافظت کنید! بهبودهای بیشتر به زودی! با سپاس از کاربران - از طریق Weblate همکاری کنید! رسیدهای تحویل پیام! @@ -1683,7 +1681,7 @@ \n- و بیشتر! کد عبور خودتخریبی با سپاس از کاربران - از طریق Weblate همکاری کنید! - ویدئوها و پرونده‌ها تا ۱ گیگابایت + ویدئوها و فایل‌ها تا ۱ گیگابایت تایید عبارت عبور لینک نامعتبر هنگام برقراری تماس‌های صوتی و تصویری. @@ -1697,20 +1695,18 @@ ماه انتخاب دستگاه موبایل جدید - در حال متوقف کردن گپ + در حال متوقف کردن چت بدون اتصال شبکه - دیگر + سایر تایید امنیت اتصال در حال آماده‌سازی بارگیری جابه‌جایی کامل شد نباید از یک پایگاه داده روی دو دستگاه استفاده کنید.]]> پیام خوشامدگویی گروه - - مطلع کردن اختیاری مخاطبان حذف شده. -\n- نام‌های نمایه شامل فاصله. -\n- و بیشتر! + - مطلع کردن اختیاری مخاطبان حذف شده. \n- نام‌های پروفایل شامل فاصله. \n- و بیشتر! رابط کاربری مجارستانی و ترکی جابه‌جایی به دستگاه دیگر از طریق کد QR. - گشودن گروه + باز کردن گروه WiFi تماس‌های تصویر در تصویر استفاده از برنامه در حین مکالمه. @@ -1720,7 +1716,7 @@ پنهان کردن صفحه برنامه در برنامه‌های اخیر. پیام‌های زنده گیرنده‌ها به‌روزرسانی‌ها را هم‌زمان با تایپ کردن شما مشاهده می‌کنند. - برای محافظت از منطقه زمانی، پرونده‌های تصویر/صدا از UTC استفاده می‌کنند. + برای محافظت از منطقه زمانی، فایل‌های تصویر/صدا از UTC استفاده می‌کنند. سریع و بدون منتظر ماندن تا زمانی که فرستنده آنلاین شود. کاهش بیشتر استفاده باتری تعیین پیام نمایش داده شده به اعضای جدید! @@ -1731,11 +1727,11 @@ تیک دومی که ما نداشتیم! ✅ برای پنهان کردن پیام‌های ناخواسته. یادداشت‌های خصوصی - با پرونده‌ها و رسانه رمزگذاری شده. + با فایل‌ها و رسانه رمزنگاری شده. تحویل پیام بهبود یافته ساعت می‌توانید بعدا از طریق تنظیمات حریم خصوصی و امنیت برنامه آن‌ها را فعال کنید. - گشودن پورت در فایروال + باز کردن پورت در فایروال %s مفقود است]]> برای اجازه دادن به برنامه موبایل به کامپیوتر متصل شوید، این پورت را در فایروال خود باز کنید، اگر فعال است خطای داخلی @@ -1743,9 +1739,9 @@ %1$s هستید.]]> تکرار بارگیری وارد کردن ناموفق بود - تکرار وارد کردن + تکرار ایمپورت نهایی‌سازی جابه‌جایی در دستگاه دیگر. - برای ادامه دادن، گپ باید متوقف شود. + برای ادامه دادن، چت باید متوقف شود. تکرار بارگذاری %s بارگذاری شد بارگذاری ناموفق بود @@ -1757,34 +1753,34 @@ حالت ناشناس ساده‌شده تغییر حالت ناشناس هنگام اتصال. پیوستن به مکالمات گروه - جهت اتصال لینک را الصاق کنید - تاریخچه اخیر و روبات فهرست راهنمای بهبودیافته. - نوار جستجو لینک‌های دعوت قبول می‌کند. + جهت اتصال لینک را الصاق کنید! + تاریخچه اخیر و روبات فهرست راهنمای بهبود یافته. + نوار جستجو لینک‌های دعوت را قبول می‌کند. با استفاده باتری کاهش یافته. ثانیه - رمزگذاری مقاوم در برابر کوانتوم + رمزنگاری مقاوم در برابر کوانتوم گروه‌های امن‌تر تنها یک دستگاه در هر زمان می‌تواند مورد استفاده قرار گیرد درخواست پیوستن تکرار شود؟ به گروه خود می‌پیوندید؟ %s بارگیری شد - پرونده صادر شده وجود ندارد + فایل اکسپورت شده وجود ندارد جابه‌جایی به دستگاه دیگر - هشدار: شروع گپ روی چندین دستگاه پشتیبانی نمی‌شود و باعث عدم موفقیت در تحویل پیام خواهد شد + هشدار: شروع چت روی چندین دستگاه پشتیبانی نمی‌شود و باعث عدم موفقیت در تحویل پیام خواهد شد نام دستگاه با کلاینت موبایل متصل شده به اشتراک گذاشته خواهد شد. - پیدا کردن سریع‌تر گپ‌ها + پیدا کردن سریع‌تر چت‌ها لینک‌های گروه حذف غیرقابل برگشت پیام - امنیت SimpleX Chat به وسیله Tails of Bits مورد سنجش قرار گرفت. + امنیت SimpleX Chat به وسیله Tails of Bits مورد سنجش قرار گرفته است. انزوای ترابری کد نشست %1$s هستید.]]> موبایل‌های متصل سرورهای ناشناخته! حفاظت از نشانی IP - برنامه از شما خواهد خواست تا بارگیری‌ها از سرورهای پرونده ناشناخته را تایید کنید (به جز .onion یا وقتی پروکسی SOCKS فعال است). - پرونده‌ها - بدون تور یا VPN، نشانی IP شما برای سرورهای پرونده قابل رویت خواهد بود. + این اپلیکیشن از شما می‌خواهد که دانلودها از سرورهای فایل ناشناخته را تأیید کنید (به جز .onion یا زمانی که پروکسی SOCKS فعال باشد). + فایل‌ها + بدون Tor یا VPN، نشانی IP شما برای سرورهای فایل قابل رویت خواهد بود. بدون تور یا VPN، نشانی IP شما برای این واسطه‌های XFTP قابل رویت خواهد بود: \n%1$s. هیچ @@ -1794,11 +1790,11 @@ اشکال‌زدایی تحویل اطلاعات صف پیام تم برنامه - تایید پرونده‌ها از سرورهای ناشناخته. + تایید فایل‌ها از سرورهای ناشناخته. اطلاعات صف سرور: %1$s \n \nآخرین پیام دریافتی: %2$s - نمایش فهرست گپ در پنجره جدید + نمایش فهرست چت در پنجره جدید حالت تاریک سیاه حالت رنگ @@ -1809,9 +1805,9 @@ سیستم خطا در مقداردهی اولیه WebView. سیستم خود را به نسخه جدید به روز کنید. لطفا با توسعه‌دهنگان تماس بگیرید. \nخطا: 9%s - رنگ‌های گپ - تم گپ - تم نمایه + رنگ‌های چت + تم چت + تم پروفایل پس‌زمینه کاغذدیواری ابتدایی اضافی ۲ تنظیمات پیشرفته @@ -1832,22 +1828,708 @@ اعمال بر حالت روشن مسیریابی پیام خصوصی 🚀 - ظاهر گپ‌های خود را متمایز کنید! - تم‌های جدید گپ + ظاهر چت‌های خود را متمایز کنید! + تم‌های جدید چت از نشانی IP خود در برابر واسطه‌های پیام‌رسانی انتخاب شده توسط مخاطبانتان محافظت کنید. \nدر تنظیمات «شبکه و سرورها» فعال کنید. - دریافت امن پرونده‌ها - پرونده یافت نشد - احتمالا حذف یا لغو شده. - کلید اشتباه یا نشانی پرونده ناشناخته - به احتمال زیاد پرونده حذف شده است. - خطای پرونده - خطای پرونده موقت - وضعیت پرونده + دریافت امن فایل‌ها + فایل یافت نشد - احتمالا حذف یا لغو شده. + کلید اشتباه یا نشانی پرونده ناشناخته - به احتمال زیاد فایل حذف شده است. + خطای فایل + خطای فایل موقت + وضعیت فایل وضعیت پیام - وضعیت پرونده: %s + وضعیت فایل: %s وضعیت پیام: %s خطای کپی - لطفا بررسی کنید که تلفن همراه و کامپیوتر به شبکه محلی یکسانی متصل هستند، و فایروال کامپیوتر شما اجازه اتصال را میدهد. -\nلطفا هر مشکل دیگری را با توسعه‌دهندگان به اشتراک بگذارید. + لطفا بررسی کنید که تلفن همراه و کامپیوتر به شبکه محلی یکسانی متصل هستند، و فایروال کامپیوتر شما اجازه اتصال را می‌دهد. \nلطفا هر مشکل دیگری را با توسعه‌دهندگان به اشتراک بگذارید. این لینک توسط موبایل دیگری استفاده شده است، لطفا لینک جدیدی در کامپیوتر بسازید. - خطای سرور پرونده:%1$s - \ No newline at end of file + خطای سرور فایل:%1$s + %1$d خطای فایل:\n%2$s + %1$d فایل هنوز در حال بارگیری هستند + %1$d فایل پاک شدند. + %1$d فایل بارگیری نشدند. + %1$d خطای دیگر در فایل. + %1$s پیام باز ارسال نشدند. + چت با یک عضو + یک گزارش + یک سال + a + b + درباره عملگرها + پذیرفتن + پذیرفتن + پذیرفتن به عنوان عضو + پذیرفتن به عنوان ناظر + پذیرفتن شرایط + %1$s پذیرفته شد + دعوت پذیرفته شد + شما پذیرفته شدید + پذیرفتن عضو + تأیید شد + خطاهای تأیید + ارتباطات فعال + سرویس‌دهنده‌های رسانه و فایل اضافه شدند + سرویس‌دهنده‌های پیام اضافه شدند + %1$d فایل هنوز در حال بارگیری هستند. + شرایط پذیرفته شد + افزودن دوستان + افزودن فهرست + نشانی یا پیوند یک‌بار مصرف؟ + تنظیمات نشانی + به‌روزرسانی آدرس + اضافه کردن اعضای تیم + افزودن به فهرست + اعضای تیم خود را به چت‌ها اضافه کنید. + همه + همه + همه چت‌ها از فهرست %s پاک خواهند شد و فهرست حذف می‌شود. + همه پیام‌های جدید از این اعضا پنهان خواهند شد! + اجازه تماس‌ها؟ + اجازه گزارش پیام‌ها به ناظران. + همه پروفایل‌ها + همه گزارش‌ها برای شما بایگانی خواهند شد. + همه سرویس‌دهنده‌ها + دلیلی دیگر + برنامه همیشه در پس‌زمینه اجرا می‌شود + نشست برنامه + نوارهای ابزار برنامه + به‌روزرسانی برنامه بارگیری شد + بایگانی + همه گزارش‌ها بایگانی شوند؟ + بایگانی کردن تماس‌ها برای چت کردن در آینده. + مخاطبین بایگانی شده + گزارش بایگانی شده + گزارش بایگانی شده توسط %s + بایگانی %d گزارش؟ + بایگانی گزارش + گزارش بایگانی شود؟ + پیوند نامعتبر + لطفاً بررسی کنید که لینک سیمپلکس درست است. + فقط شما و ناظران آن را می‌بینید + فقط فرستنده و ناظران آن را می‌بینند + درخواست اتصال داد + پیوند کانال SimpleX + هرزنامه + محتوای نامناسب + نقض دستورالعمل‌های انجمن + پروفایل نامناسب + خطا در ذخیره سرویس‌دهنده‌ها + بدون سرویس‌دهنده پیام. + هیچ سرویس‌دهنده‌ای برای دریافت پیام وجود ندارد. + هیچ سرویس‌دهنده‌ای برای مسیردهی پیام خصوصی وجود ندارد. + بدون سرویس‌دهنده رسانه و فایل. + هیچ سرویس‌دهنده‌ای برای ارسال فایل وجود ندارد. + هیچ سرویس‌دهنده‌ای برای دریافت فایل وجود ندارد. + برای پروفایل چت %s: + خطاها در پیکربندی سرویس‌دهنده‌ها. + خطا در پذیرفتن شرایط + هرزنامه + محتوا شرایط استفاده را نقض می‌کند + نشانی سرویس‌دهنده با تنظیمات شبکه سازگار نیست: %1$s. + نسخه سرویس‌دهنده با برنامه شما سازگار نیست: %1$s. + خطای مسیردهی خصوصی + خطا در اتصال به سرویس‌دهنده فورواردینگ %1$s. لطفاً بعداً تلاش کنید. + نشانی سرویس‌دهنده فورواردینگ با تنظیمات شبکه سازگار نیست: %1$s. + نسخه سرویس‌دهنده فورواردینگ با تنظیمات شبکه سازگار نیست: %1$s. + سرویس‌دهنده فورواردینگ %1$s نتوانست به سرویس‌دهنده مقصد %2$s متصل شود. لطفاً بعداً تلاش کنید. + نشانی سرویس‌دهنده مقصد %1$s با تنظیمات سرویس‌دهنده فورواردینگ %2$s سازگار نیست. + نسخه سرویس‌دهنده مقصد %1$s با سرویس‌دهنده فورواردینگ %2$s سازگار نیست. + لطفاً بعداً تلاش کنید. + خطا در فوروارد کردن پیام‌ها + خطا در ایجاد گزارش + خطا در پذیرفتن عضو + خطا در حذف کردن چت + پیوند اتصال پشتیبانی‌نشده + این پیوند به نسخه جدیدتری از برنامه نیاز دارد. لطفاً برنامه را به‌روزرسانی کنید یا از مخاطب خود بخواهید پیوند سازگاری بفرستد. + اتصال مسدود شد + اتصال توسط گرداننده سرویس‌دهنده مسدود شده است:\n%1$s. + پیام‌های نرسیده + ارتباط به حد نصاب پیام‌های نرسیده رسید، ممکن است مخاطب شما آفلاین باشد. + خطا در به‌روزرسانی فهرست چت + افزودن پیام + امکان تماس وجود ندارد! + خطا در ارسال پیام + تماس با مخاطب امکان پذیر نیست + تماس با عضو گروه ممکن نیست + تغییر پروفایل امکان پذیر نیست + ارسال پیام به عضو گروه امکان پذیر نیست + ارسال پیام امکان پذیر نیست + بایگانی گزارش ها + تماس های با کیفیت تر + عملکرد بهتر گروه ها + امنیت و حریم خصوصی قوی تر + تغییر لیست + تغییر ترتیب + چت + این چت از قبل وجود دارد! + چت با اعضا + پیام برای همه اعضا حذف خواهد شد - این عمل قابل بازگشت نیست! + بخش های حذف شده + بخش های دانلود شده + برای مجوز میکروفون، روی آیکون اطلاعات کنار نوار آدرس کلیک کنید + تکمیل شد + تایید حذف مخاطب؟ + اتصال + اتصال + متصل + اتصال سریع تر! 🚀 + درحال اتصال + درحال اتصال به مخاطب، لطفا منتظر بمانید یا بعداً بررسی کنید! + وضعیت اتصال و سرور ها + اتصال آماده نیست + مخاطب حذف شد. + مخاطبین + باز کردن چت + باز کردن چت جدید + باز کردن گروه جدید + به منوی تنظیمات سافاری / وب‌سایت‌ها / و سپس میکروفن رفته و گزینه \"اجازه دادن برای localhost\" را انتخاب کنید. + باز کردن تنظیمات سرور + باز کردن لینک + باز کردن لینک‌ها از لیست چت‌ها + باز کردن برای پذیرش + باز کردن برای اتصال + باز کردن برای عضویت + آیا لینک وب باز شود؟ + باز کردن با %s + اپراتور + سرور اپراتور + سازماندهی چت‌ها به صورت لیست‌ها + یا ایمپورت کردن فایل آرشیو + یا به‌طور خصوصی به اشتراک بگذارید + سایر + سایر خطاها + سایر سرورهای SMP + سایر سرورهای XFTP + عبارت عبور در کی‌استور قابل خواندن نیست، لطفاً آن را به‌صورت دستی وارد کنید. این ممکن است پس از یک به‌روزرسانی سیستم که با برنامه سازگار نیست، اتفاق افتاده باشد. اگر این مورد نیست، لطفاً با توسعه‌دهندگان تماس بگیرید. + عبارت عبور در مخزن کلید قابل خواندن نیست. این ممکن است پس از به‌روزرسانی سیستم که با برنامه سازگار نیست، اتفاق افتاده باشد. اگر این طور نیست، لطفا با توسعه دهندگان تماس بگیرید. + کلمه عبور + الصاق لینک + در حال انتظار + در حال انتظار + در انتظار تأیید + در انتظار بررسی + پخش کردن از فهرست چت. + لطفاً از مخاطب خود بخواهید که تماس‌ها را فعال کند. + لطفاً اندازه پیام را کاهش دهید و دوباره ارسال کنید. + لطفاً اندازه پیام را کاهش دهید یا فایل رسانه‌ای را حذف کنید و دوباره ارسال کنید. + لطفا برنامه را شروع مجدد کنید. + لطفاً منتظر بمانید تا مدیران گروه درخواست شما برای پیوستن به گروه را بررسی کنند. + سرورهای از پیش تنظیم شده + سرورهای از پیش تنظیم شده + سرورهای متصل قبلی + حریم خصوصی برای مشتریان شما. + سیاست حریم خصوصی و شرایط استفاده. + چت‌های خصوصی، گروه‌ها و مخاطبین شما برای اپراتورهای سرور قابل دسترسی نیستند. + نام‌های فایل‌های رسانه‌ای خصوصی. + وقفه در مسیردهی خصوصی + آدرس کوتاه خواهد بود و پروفایل شما از طریق آدرس به اشتراک گذاشته خواهد شد. + ممنوع کردن گزارش پیام‌ها به مدیران. + وقفه پس‌زمینه پروتکل + پروکسی شده + سرورهای پروکسی شده + احراز هویت پروکسی + نوار ابزارهای قابل دسترسی در برنامه + نوار ابزار چت قابل دسترس + نوار ابزار چت قابل دسترس + پیام‌های دریافتی + مجموع دریافتی + خطاهای دریافت + اتصال مجدد + اتصال مجدد به تمام سرورهای متصل برای مجبور کردن به تحویل پیام. این کار از ترافیک اضافی استفاده می‌کند. + اتصال مجدد به تمام سرورها + اتصال مجدد به سرور؟ + اتصال مجدد به سرورها؟ + اتصال مجدد به سرور برای مجبور کردن به تحویل پیام. این کار از ترافیک اضافی استفاده می‌کند. + رد کردن + رد کردن درخواست مخاطب + رد شده + رد شده + رد کردن عضو؟ + بعداً یادآوری کن + موبایل‌های از راه دور + حذف کردن آرشیو؟ + از گروه حذف شد + اعضا حذف شوند؟ + پیام‌ها را حذف می‌کند و اعضا را مسدود می‌کند. + گزارش + گزارش محتوا: فقط مدیران گروه آن را خواهند دید. + گزارش دادن پیام‌ها در این گروه ممنوع است. + گزارش دادن پروفایل عضو: فقط مدیران گروه آن را خواهند دید. + گزارش سایر موارد: فقط مدیران گروه آن را خواهند دید. + دلیل گزارش؟ + گزارش: %s + گزارش‌ها + گزارش‌های ارسالی به مدیران + گزارش هرزنامه: فقط مدیران گروه آن را خواهند دید. + گزارش تخلف: فقط مدیران گروه آن را خواهند دید. + درخواست ارسال شد + درخواست عضویت رد شد + بازنشاندن + تنظیم مجدد تمام راهنماها + تنظیم مجدد تمام آمار + تمام آمار مجدد تنظیم شود؟ + بررسی + بررسی شرایط + بررسی شده توسط مدیران + بررسی اعضای گروه + بعداً بررسی شود + بررسی اعضا + بررسی اعضا قبل از پذیرش (کوبیدن). + آیا تنظیمات پذیرش ذخیره شود؟ + ذخیره و برقراری مجدد اتصال + ذخیره فهرست + در حال ذخیره پیام‌های %1$s + اسکن / الصاق لینک + جستجو + امن شده + انتخاب + انتخاب پروفایل چت + تنظیمات برگزیده چت، این پیام را ممنوع می‌کند. + %d انتخاب شده + اپراتورهای شبکه را برای استفاده انتخاب کنید. + تنظیم پذیرش اعضا + تنظیم انقضای پیام در چت‌ها. + تنظیمات + اشتراک‌گذاری لینک یک بار مصرف با یک دوست + اشتراک‌گذاری آدرس به صورت عمومی + آیا لینک گروه به‌روزرسانی شود؟ + اشتراک‌گذاری پروفایل + به‌روزرسانی + آیا آدرس به‌روزرسانی شود؟ + اشتراک‌گذاری آدرس SimpleX در شبکه‌های اجتماعی + توضیح کوتاه: + لینک کوتاه + نمایش اطلاعات برای + نمایش درصد + آدرس SimpleX و لینک‌های یکبارمصرف برای اشتراک توسط هرگونه پیامرسانی امن می‌باشند. + آدرس SimpleX یا لینک یکبار مصرف؟ + SimpleX Chat و Flux به توافق رسیده‌اند که سرورهای تحت کنترل Flux را به برنامه اضافه کنند. + پروتکل‌های SimpleX توسط Trail of Bits بررسی شده‌اند. + اندازه + نادیده گرفتن این نسخه + سرور SMP + پروکسی SOCKS + نرم + برخی از فایل(ها) اکسپورت نشدند + صدا خاموش شد + %s سرور + پایدار + در حال شروع از %s. + در حال شروع از %s.\nتمام داده‌ها به صورت خصوصی بر روی دستگاه شما نگهداری می‌شوند. + آمار + خط‌خورده + قوی + مشترک شد + خطاهای اشتراک + اشتراک‌ها نادیده گرفته شدند + در حین تماس، صدا و ویدیو را جابجا کنید. + پروفایل چت را برای دعوت‌های یک‌بار مصرف تغییر دهید. + حالت سیستم + انتها + برای چت، روی \"اتصال\" ضربه بزنید. + برای ارسال درخواست، روی \"اتصال\" ضربه بزنید. + برای ایجاد آدرس SimpleX در آینده، در منو روی \"ایجاد آدرس SimpleX\" ضربه بزنید. + روی \"پیوستن به گروه\" ضربه بزنید. + اتصال TCP + وقفه زمانی اتصال TCP در پس‌زمینه + پورت TCP برای پیام‌رسانی + این اپلیکیشن با استفاده از اپراتورهای مختلف در هر مکالمه، از حریم خصوصی شما محافظت می‌کند. + پیام‌ها برای تمام اعضا حذف خواهند شد. + پیام‌ها برای تمام اعضا به عنوان مدیریت‌شده علامت‌گذاری خواهند شد. + پذیرفتن درخواست تماس + پذیرفتن درخواست تماس + پرسش + تلاش‌ها + بتا + بهبود تاریخ پیام‌ها + امنیت بهتر ✅ + تجربه کاربری بهتر + بیوگرافی: + بیو خیلی بزرگ است + مسدود کردن اعضا برای همه؟ + تار کردن + تار کردن برای حفظ بهتر حریم خصوصی. + تار کردن رسانه + آدرس کسب و کار + چت‌های تجاری + ارتباط تجاری + کسب و کارها + با استفاده از SimpleX Chat شما موافقت می‌کنید که:\n- فقط محتوای قانونی را در گروه‌های عمومی ارسال کنید.\n- به سایر کاربران احترام بگذارید - از ارسال هرزنامه خودداری کنید. + تماس + با یک مخاطب استفاده شود - آن را به صورت حضوری یا از طریق هر پیام‌رسانی به اشتراک بگذارید.]]> + به صورت رمزنگاری انتها به انتهاو با امنیت پساکوانتومی در پیام‌های مستقیم ارسال می‌شوند.]]> + %s.]]> + %s.]]> + رمزنگاری انتها به انتها محافظت می‌شوند.]]> + %s اعمال خواهد شد.]]> + %s اعمال خواهد شد.]]> + %s اعمال خواهد شد.]]> + %s اعمال خواهد شد.]]> + پایگاه داده چت اکسپورت شد + چت برای شما حذف خواهد شد - این عمل غیرقابل بازگشت است! + چت با مدیران + چت با مدیران + چت با مدیران + چت با عضو + چت با اعضا قبل از پیوستن. + بررسی به‌روزرسانی‌ها + بررسی به‌روزرسانی‌ها + بررسی پیام‌ها هر ۱۰ دقیقه + بخش‌ها بارگذاری شدند + شرایط در %s پذیرفته شد. + شرایط استفاده + شرایط برای اپراتورهای فعال پس از ۳۰ روز پذیرفته خواهد شد. + شرایط در: %s پذیرفته خواهد شد. + شرایط به‌طور خودکار برای اپراتورهای فعال در: %s پذیرفته خواهد شد. + سرورهای SMP پیکربندی‌شده + سرورهای XFTP پیکربندی‌شده + پیکربندی اپراتورهای سرور + سرورهای متصل شده + اتصال نیاز به تجدید مذاکره رمزنگاری دارد. + اتصالات + امنیت اتصال + به دوستان خود سریع‌تر متصل شوید. + مخاطب حذف شد. + مخاطب حذف شد! + مخاطب غیر فعال شد + مخاطب آماده نیست + مخاطب باید قبول کند… + مخاطب حذف خواهد شد - این عمل غیرقابل بازگشت است! + ادامه + ادامه + کنترل شبکه خود را در دست بگیرید + گفتگو حذف شد! + گوشه + ایجاد + ایجاد لینک یک‌بار مصرف + ایجاد شد + ایجاد لیست + متن شرایط فعلی بارگذاری نشد، می‌توانید شرایط را از طریق این لینک بررسی کنید: + پروفایل فعلی + شکل پیام قابل تنظیم. + %d چت(ها) + %d چت با اعضا + خطاهای رمزگشایی + پیش‌فرض (%s) + حذف + حذف چت + حذف چت + چت حذف شود؟ + پیام‌های چت را از دستگاه خود حذف کنید. + حذف پروفایل چت برای + آیا می‌خواهید چت با عضو را حذف کنید؟ + حذف شد + آیا می‌خواهید %d پیام از اعضا را حذف کنید؟ + حذف لیست؟ + حذف پیام‌ها بعد از + حذف یا مدیریت حداکثر ۲۰۰ پیام. + حذف گزارش + حذف حداکثر ۲۰ پیام به‌طور همزمان. + حذف بدون اطلاع‌رسانی + حذف خطاها + توضیحات خیلی بزرگ است + آمار دقیق + جزئیات + پیام‌های مستقیم بین اعضا ممنوع است. + پیام‌های مستقیم بین اعضا در این چت ممنوع است. + غیر فعال + آیا می‌خواهید حذف خودکار پیام‌ها را غیرفعال کنید؟ + غیر فعال شد + غیر فعال شد + غیرفعال کردن حذف پیام‌ها + %d پیام + از اعتبارنامه‌ها با پروکسی استفاده نشود. + پیام‌های مهم را از دست ندهید. + دانلود + دانلود شده + دانلود فایل‌ها + دانلود خطاها + در حال دانلود به‌روزرسانی، اپلیکیشن را نبندید. + دانلود نسخه‌های جدید از گیت‌هاب. + دانلود %s (%s) + %d گزارش + اعضا از چت حذف خواهند شد - این عمل غیرقابل بازگشت است! + اعضا از گروه حذف خواهند شد - این عمل غیرقابل بازگشت است! + عضو از چت حذف خواهد شد - این عمل غیرقابل بازگشت است! + عضو به گروه ملحق خواهد شد، آیا می‌خواهید عضو را بپذیرید؟ + منشن اعضا👋 + پیام + پیام + پیام فوروارد شد. + پیام بلافاصله پس از ضربه زدن بر روی \"اتصال\" ارسال می‌شود. + پیام خیلی بزرگ است! + اگر عضو فعال شود، پیام ممکن است بعداً ارسال گردد. + دریافت پیام + سرورهای پیام + پیام‌های این اعضا نمایش داده خواهد شد! + شکل پیام + پیام‌ها در این چت هرگز حذف نخواهند شد. + پیام‌ها دریافت شدند + پیام‌ها ارسال شدند + پیام‌ها پس از انتخاب حذف شدند. + پیام‌ها حذف خواهند شد - این عمل غیرقابل بازگشت است! + پیام‌ها برای حذف علامت‌گذاری خواهند شد. دریافت‌کننده(ها) قادر خواهند بود این پیام‌ها را آشکار کنند. + مدیر + مدیران + بی‌صدا کردن همه + تمرکززدایی شبکه + اپراتور شبکه + اپراتورهای شبکه + تجربه چت جدید 🎉 + نقش جدید گروه: مدیر + گزینه‌های رسانه‌ای جدید + عضو جدید می‌خواهد به گروه ملحق شود. + پیام جدید + سرور جدید + اعتبارنامه‌های جدید SOCKS هر بار که اپلیکیشن را راه‌اندازی کنید، استفاده خواهند شد. + اعتبارنامه‌های جدید SOCKS برای هر سرور استفاده خواهند شد. + خیر + هیچ سرویس پس‌زمینه‌ای وجود ندارد + هیچ چتی وجود ندارد + هیچ چتی پیدا نشد + هیچ چتی در لیست %s وجود ندارد. + هیچ چتی با اعضا وجود ندارد + هنوز هیچ ارتباط مستقیمی وجود ندارد، پیام توسط مدیر فوروارد شده است. + هیچ مخاطب فیلتر شده‌ای وجود ندارد + هیچ اطلاعاتی وجود ندارد، سعی کنید دوباره بارگذاری کنید + هیچ پیامی وجود ندارد + هیچ جلسه مسیریابی خصوصی وجود ندارد + یادداشت‌ها + هیچ چیزی انتخاب نشده است + هیچ چیزی برای فوروارد کردن وجود ندارد! + اطلاع‌رسانی‌ها و باتری + همگام‌سازی نشده است + هیچ چت خوانده‌نشده‌ای وجود ندارد + خاموش + خاموش + خاموش + فقط مالکین چت می‌توانند تنظیمات را تغییر دهند. + فقط گفتگو حذف شود + باز کردن + باز کردن تغییرات + - باز کردن چت برای اولین پیام خوانده نشده.\n- پرش به پیام‌های نقل‌قول شده. + شرایط باز کردن + باز کردن مکان فایل + دستگاه‌های شیائومی: لطفاً در تنظیمات سیستم، گزینه شروع خودکار را فعال کنید تا اعلان‌ها کار کنند.]]> + %s.]]> + %s.]]> + %s، شرایط استفاده را بپذیرید.]]> + %1$s متصل هستید.]]> + پس از پذیرش درخواست‌تان قادر به ارسال پیام خواهید بود.]]> + تغییر حذف خودکار پیام؟ + پایگاه داده چت + تکراری‌ها + تغییر + Flux را در تنظیمات شبکه و سرورها فعال کنید تا حریم خصوصی فراداده بهتر شود. + فعال کردن لاگ‌ها + مذاکره مجدد رمزنگاری در حال انجام است. + خطا + خطا در افزودن سرور + خطا در تغییر پروفایل + خطا در ایجاد لیست چت + خطا در راه‌اندازی WebView. لطفاً اطمینان حاصل کنید که WebView نصب شده و معماری پشتیبانی شده آن arm64 است. \nخطا: %s + خطا در بارگذاری لیست‌های چت + خطا در باز کردن چت + خطا در باز کردن گروه + خطا در خواندن عبارت عبور پایگاه داده + خطا در اتصال مجدد به سرور + خطا در اتصال مجدد به سرورها + خطا در رد درخواست مخاطب + خطا در بازنشانی آمار + خطاها + خطا در ذخیره‌سازی پایگاه داده + خطا در ذخیره‌سازی پروکسی + خطا در ذخیره‌سازی تنظیمات + خطا در تغییر پروفایل + خطا در به‌روزرسانی سرور + منقضی شده + حذف سریع‌تر گروه‌ها. + ارسال سریع‌تر پیام‌ها. + علاقه‌مندی‌ها + فایل توسط اپراتور سرور مسدود شده است: \n%1$s. + فایل‌ها + اصلاح کردن + اتصال اصلاح شود؟ + اندازه فونت + برای همه مدیران + برای بهبود حریم خصوصی فراداده. + به عنوان مثال، اگر مخاطب شما پیام‌ها را از طریق یک سرور چت SimpleX دریافت کند، برنامه شما آن‌ها را از طریق یک سرور Flux تحویل خواهد داد. + برای من + برای مسیریابی خصوصی + برای رسانه‌های اجتماعی + آیا می‌خواهید %1$s پیام را ارسال کنید؟ + در حال ارسال %1$s پیام + در حال ارسال پیام‌ها… + آیا می‌خواهید پیام‌ها را بدون فایل ارسال کنید؟ + حداکثر می‌توانید ۲۰ پیام را به‌طور همزمان ارسال کنید. + لینک کامل + زمانی که به شما اشاره شد، مطلع شوید. + گروه + گروه حذف شد + گروه‌ها + به مدیران در مدیریت گروه‌هایشان کمک کنید. + چگونه به حفظ حریم خصوصی کمک می‌کند + بهبود ناوبری چت + غیرفعال + اندازه فونت را افزایش دهید. + با موفقیت نصب شد + نصب به‌روزرسانی + دعوت + دعوت + دعوت به چت + آدرس IP و اتصالات شما را محافظت می‌کند. + پیوستن به گروه + گفتگو را حفظ کنید + ترک کردن چت + آیا می‌خواهید چت را ترک کنید؟ + ترافیک کمتری در شبکه‌های موبایل + فهرست + نام فهرست... + نام فهرست و ایموجی باید برای تمام فهرست‌ها متفاوت باشد. + در حال فراخوانی پروفایل… + اطمینان حاصل کنید که پیکربندی پروکسی صحیح است. + سرورهای رسانه و فایل + متوسط + پذیرش عضو + عضو از نسخه قدیمی استفاده می‌کند + عضو غیرفعال + گزارش‌های عضو + اعضا می‌توانند پیام‌ها را به مدیران گزارش دهند. + آیا می‌خواهید درخواست تماس ارسال کنید؟ + ارسال خطاها + پیامی ارسال کنید تا تماس‌ها فعال شوند. + گزارش‌های خصوصی ارسال کنید + ارسال درخواست + درخواست را بدون پیام ارسال کنید + بازخورد خصوصی خود را به گروه‌ها ارسال کنید. + مستقیم ارسال شد + پیام‌های ارسال شده + تعداد کل ارسال شده + پس از اتصال به مخاطب شما ارسال شد. + توسط پروکسی ارسال شد + سرور + سرور به اپراتور %s اضافه شد. + آدرس سرور + اپراتور سرور تغییر کرد. + اپراتورهای سرور + پروتکل سرور تغییر کرد. + اطلاعات سرورها + آمار سرورها بازنشانی خواهد شد - این عمل غیرقابل بازگشت است! + نام چت را تنظیم کنید… + گزارش برای شما بایگانی خواهد شد. + نقش به %s تغییر خواهد کرد. همه در چت مطلع خواهند شد. + دومین اپراتور پیش‌فرض در برنامه! + فرستنده مطلع نخواهد شد. + سرورهای فایل‌های جدید پروفایل چت فعلی شما + آرشیو پایگاه داده بارگذاری‌شده به‌طور دائمی از سرورها حذف خواهد شد. + این عمل غیرقابل بازگشت است - پیام‌های ارسال‌شده و دریافت‌شده در این چت که قبل از تاریخ انتخاب‌شده هستند، حذف خواهند شد. + این پیام حذف شده یا هنوز دریافت نشده است. + زمان ناپدید شدن فقط برای مخاطبان جدید تنظیم شده است. + برای مطلع شدن از نسخه‌های جدید، بررسی دوره‌ای برای نسخه‌های Stable یا Beta را فعال کنید. + تغییر لیست چت: + برای برقراری تماس، اجازه دهید از میکروفن شما استفاده شود. تماس را پایان دهید و دوباره تلاش کنید. + برای جلوگیری از جایگزینی لینک شما، می‌توانید کدهای امنیتی مخاطب را مقایسه کنید. + برای دریافت + برای ارسال + کل + برای استفاده از پروفایل دیگر پس از تلاش برای اتصال، چت را حذف کرده و دوباره از لینک استفاده کنید. + شفافیت + جلسات Transport + رفع انسداد اعضا برای همه؟ + منشن‌های خوانده نشده + به‌روزرسانی + به‌روزرسانی موجود است: %s + شرایط به‌روزرسانی شده + دانلود به‌روزرسانی لغو شد + به‌روزرسانی خودکار برنامه + بارگذاری شد + فایل‌های بارگذاری‌شده + خطاهای بارگذاری + از اعتبارنامه‌های پروکسی متفاوت برای هر اتصال استفاده شود. + از اعتبارنامه‌های پروکسی متفاوت برای هر پروفایل استفاده شود. + استفاده برای فایل‌ها + استفاده برای پیام‌ها + استفاده از اعتبارنامه‌های تصادفی + نام کاربری + استفاده از %s + استفاده از سرورها + زمانی که پورتی مشخص نشده است، از پورت TCP %1$s استفاده شود. + فقط برای سرورهای پیش‌فرض از پورت TCP 443 استفاده شود. + از برنامه با یک دست استفاده کنید. + از پورت وب استفاده کنید + ویدئو + مشاهده شرایط + مشاهده شرایط به‌روزرسانی شده + وب‌سایت + پیام خوش‌آمدگویی + زمانی که بیش از یک اپراتور فعال است، هیچ‌کدام از آن‌ها فراداده‌ای برای فهمیدن اینکه چه کسی با چه کسی ارتباط برقرار می‌کند، ندارند. + سرور XFTP + بله + شما این عضو را پذیرفتید + شما به این سرورها متصل نیستید. از مسیریابی خصوصی برای ارسال پیام‌ها به آن‌ها استفاده می‌شود. + شما می‌توانید آن را در تنظیمات ظاهر تغییر دهید. + شما می‌توانید اپراتورها را در تنظیمات شبکه و سرورها پیکربندی کنید. + شما می‌توانید سرورها را از طریق تنظیمات پیکربندی کنید. + شما می‌توانید پیام را کپی کرده و اندازه آن را کاهش دهید تا ارسال شود. + شما می‌توانید در هر پیام تا %1$s عضو را منشن کنید! + شما می‌توانید به %1$s از مخاطبان بایگانی‌شده پیام ارسال کنید. + شما می‌توانید نام اتصال را تنظیم کنید تا به یاد داشته باشید که لینک با چه کسی به اشتراک گذاشته شده است. + شما هنوز می‌توانید مکالمه با %1$s را در فهرست چت‌ها مشاهده کنید. + شما نمی‌توانید پیام‌ها را ارسال کنید! + شما می‌توانید گزارش‌های خود را در چت با مدیران مشاهده کنید. + شما ترک کردید + شما می‌توانید پایگاه داده اکسپورت شده را منتقل کنید. + شما می‌توانید آرشیو اکسپورت شده را ذخیره کنید. + شما باید به مخاطب خود اجازه تماس دهید تا بتوانید با او تماس بگیرید. + بیوگرافی شما: + مخاطب تجاری شما + پروفایل چت شما به اعضای چت ارسال خواهد شد. + اتصال شما به %s منتقل شد، اما هنگام تغییر پروفایل خطایی رخ داد. + مخاطب شما + مخاطبان شما + اعتبارنامه‌های شما ممکن است به‌صورت رمزنگاری‌نشده ارسال شوند. + گروه شما + پروفایل شما + سرورهای شما + شما دیگر پیام‌هایی از این چت دریافت نخواهید کرد. تاریخچه چت حفظ خواهد شد. + بزرگنمایی + استفاده از پروفایل ناشناس + به‌روزرسانی لینک گروه + لینک کوتاه خواهد بود و پروفایل گروه از طریق لینک به اشتراک گذاشته خواهد شد. + اشتراک آدرس قدیمی + اشتراک لینک قدیمی + به مخاطبین خود خوش آمد بگویید 👋 + بیوگرافی پروفایل و پیام خوش آمد را تنظیم کنید. + چت‌های خود را تمیز نگه‌دارید + پیام‌های ناپدید شونده به‌طور پیش‌فرض فعال شوند. + آدرس کوتاه SimpleX + آدرس خود را ایجاد کنید + آدرس خود را به‌روزرسانی کنید + آدرس خود را به اشتراک بگذارید + ۴ زبان جدید رابط کاربری + کاتالان، اندونزیایی، رومانیایی و ویتنامی - با تشکر از کاربران ما! + عضو حذف شده است - نمی‌توان درخواست را قبول کرد + این تنظیمات برای پروفایل فعلی شماست + درخواست‌های تماس از گروه‌ها + درخواست اتصال از گروه %1$s + برای استفاده از ربات باز کنید + برای استفاده از ربات، روی اتصال ضربه بزنید + ربات + برای ارسال دستورات، باید متصل باشید. + گزینه‌های منسوخ شده + حذف ردیابی لینک + باز کردن لینک کامل + باز کردن لینک تمیز + اجازه دهید مخاطبین شما فایل‌ها و رسانه‌ها را ارسال کنند. + فقط در صورتی که مخاطب شما اجازه دهد، فایل‌ها و رسانه‌ها را مجاز کنید. + ارسال فایل‌ها و رسانه‌ها را ممنوع کنید. + هم شما و هم مخاطب شما می‌توانید فایل‌ها و رسانه‌ها را ارسال کنید. + فقط شما می‌توانید فایل و رسانه ارسال کنید. + فقط مخاطب شما می‌تواند فایل‌ها و رسانه‌ها را ارسال کند. + ارسال فایل‌ها و رسانه‌ها در این چت ممنوع است. + پل ارتباطی سیمپلکس + خطا در علامت گذاری به عنوان خوانده شده + اثر انگشت در نشانی سرور مقصد با گواهی مطابقت ندارد: ‎%1$s. + اثر انگشت در نشانی سرور انتقال با گواهی مطابقت ندارد: ‎%1$s. + اثر انگشت در نشانی سرور با گواهی مطابقت ندارد: ‎%1$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 26847aeaf5..c6b094ab56 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -1036,7 +1036,7 @@ Profiilisi lähetetään kontaktille, jolta sait tämän linkin. Liityt ryhmään, johon tämä linkki viittaa, ja muodostat yhteyden sen ryhmän jäseniin. Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta. - Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta (virhe: %1$s). + Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta (virhe: %1$s). sinä kertalinkillä %1$s:n kautta @@ -1501,4 +1501,4 @@ Yhteensopimaton versio Uusi mobiililaite Tämä laite - \ No newline at end of file + 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 62891fd8b8..563b1a02c7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -52,7 +52,7 @@ Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre. Erreur de connexion Erreur lors de l\'ajout de membre·s - Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %1$s). + Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %1$s). format de message invalide Lien entier Erreur lors de la sauvegarde des serveurs SMP @@ -80,14 +80,14 @@ 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 Aperçu affiché Nom du contact @@ -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) @@ -370,11 +370,11 @@ confimation reçu… connexion… 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. + 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 @@ -438,7 +438,7 @@ en attente de confirmation… connecté terminé - La nouvelle génération \nde messagerie privée + L\'avenir de la messagerie La vie privée redéfinie Aucun identifiant d\'utilisateur. Protégé du spam @@ -448,12 +448,12 @@ Établir une connexion privée Comment ça fonctionne Comment SimpleX fonctionne - 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. 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. @@ -575,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 @@ -595,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 @@ -638,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 @@ -753,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 @@ -789,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 @@ -818,7 +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. + 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. @@ -828,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 @@ -842,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. @@ -853,7 +853,7 @@ 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 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 @@ -872,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 @@ -916,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 @@ -947,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 @@ -1035,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 @@ -1055,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 @@ -1075,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 @@ -1119,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 + Arrière-plan Thème sombre Exporter le thème Importer un thème @@ -1133,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 @@ -1144,7 +1143,7 @@ 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. @@ -1181,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 @@ -1250,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 ! @@ -1273,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. @@ -1424,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 !]]> @@ -1480,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 @@ -1680,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 @@ -1691,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 @@ -1717,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é @@ -1848,7 +1847,7 @@ Erreur de fichier Erreur de fichier temporaire Statut du fichier - Statut du fichier: %s + Statut du fichier : %s Statut du message Statut du message: %s Erreur de copie @@ -1951,8 +1950,7 @@ Infos serveurs Afficher les informations pour À partir de %s. - À partir de %s. -\nToutes les données restent confinées dans votre appareil. + À partir de %s. \nToutes les données restent confinées dans votre appareil. Statistiques Total Serveur XFTP @@ -2001,7 +1999,7 @@ Pas de contacts filtrés Coller le lien Vos contacts - Barre d\'outils accessible + 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. @@ -2017,7 +2015,7 @@ Veuillez demander à votre contact d\'autoriser les appels. Envoyer un message pour activer les appels. Archiver les contacts pour discuter plus tard. - Rendez les images floues et protégez-les contre les regards indiscrets. + 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 @@ -2114,4 +2112,253 @@ 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. - \ No newline at end of file + 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 ! + Utiliser le profil incognito + Chat ouvert + Ouvrir un nouveau chat + Ouvrir un nouveau groupe + Lien pour la voie SimpleX + Pas de session de routage privé + Membre acceptant des erreurs + Erreur effaçant le chat avec le membre + Lien de connexion pas soutenu + Ce lien requiert une version de l\'application plus récente. Veuillez s\'il-vous-plait actualiser l\'application ou demander à votre contact de vous envoyer un lien compatible. + Erreur lors de rejeter la demande de contact + Erreur en ouvrant le chat + Erreur en ouvrant le groupe + Erreur en changeant le profil + Quatres nouvelles langues d\'interface + Partagez votre adresse + Actualisez votre adresse + 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 9e0d476dc1..e6f02f34f2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml @@ -285,4 +285,4 @@ सदस्य खोजें बंद है - \ No newline at end of file + 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 531e29de20..12b578edbf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -1,6 +1,6 @@ - %1$d üzenet visszafejtése sikertelen. + Nem sikerült visszafejteni %1$d üzenetet. %1$d üzenet kihagyva. %1$d üzenet kihagyva %1$s TAG @@ -10,197 +10,196 @@ 5 perc 1 perc A SimpleX-címről - Címváltoztatás megszakítása? + Megszakítja a cím módosítását? Megszakítás 30 másodperc - Egyszer használható meghívó-hivatkozás - %1$s szeretne kapcsolatba lépni Önnel ezen keresztül: - SimpleX Chat névjegye + 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 + Cím módosításának megszakítása A SimpleXről - Kiemelés + Kiemelőszín fogadott hívás - Hozzáférés a kiszolgálókhoz SOCKS proxyn keresztül a(z) %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 inkognitóban - Kapcsolatkérés elfogadása? + Elfogadja a kapcsolódási kérést? Elfogadás Elfogadás - Cím 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 + 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. + 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 hívások kezdeményezése le van tiltva ebben a csevegésben. - Az összes ismerősé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.]]> + A hívások kezdeményezése le van tiltva. + Az összes partneréhez és csoporttaghoz külön TCP-kapcsolat (és SOCKS-hitelesítési adat) lesz használva.\nMegjegyzés: ha sok kapcsolata van, akkor 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.]]> + Az összes csevegési profiljához az alkalmazásban külön TCP-kapcsolat (és SOCKS-hitelesítési 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. - Hibás az üzenet hasító értéke + Az Android Keystore-t a jelmondat biztonságos tárolására használják – lehetővé teszi az értesítési szolgáltatás működését. + Hibás az üzenet kivonata Háttér - Megjegyzés: az üzenet- és fájlközvetí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.]]> + Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének küldése közvetlen kapcsolatot használ.]]> Alkalmazásadatok biztonsági mentése Az adatbázis előkészítése sikertelen - Az 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. 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 az ismerőse is engedélyezi. - Az alkalmazás build száma: %s + 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. + Alkalmazás összeállítási száma: %s Hang- és videóhívások Speciális hálózati beállítások - A hangüzenetek küldése engedélyezve van 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 - Az eltűnő üzenetek küldésének 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ó + Alkalmazás verziója Üdvözlőüzenet hozzáadása titkosítás elfogadása %s számára… - " -\nElérhető a v5.1-ben" + \nElérhető az v5.1-es kiadásban Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra) Továbbfejlesztett csoportok - Az összes ü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. Hívás befejeződött HÍVÁSOK é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 megjelennek, amint az üzenetek elérhetővé válnak. + 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 - Az üzenetreakciók küldése engedélyezve van az ismerősei számára. + Hibás az üzenet azonosítója + 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. - Az üzenetreakciók küldése csak abban az esetben van engedélyezve, 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.]]> + 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… - Nem lehet meghívni az ismerőst! - téves üzenet ID - Kapcsolatkérések automatikus elfogadása - Megjegyzés: NEM fogja tudni helyreállítani, vagy megváltoztatni a jelmondatot abban az esetben, ha elveszíti.]]> + Nem lehet meghívni a partnert! + hibás az üzenet azonosítója + Partneri kapcsolatkérések automatikus elfogadása + Megjegyzés: NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]> 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. + 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.]]> + Több akkumulátort használ! Az alkalmazás mindig fut a háttérben – az értesítések azonnal megjelennek.]]> Letiltás adminisztrátor Képelőnézet visszavonása - A jelkód megadása után az összes adat törlésre kerül. + A jelkód megadása után az összes adat törölve lesz. Felkérték a videó fogadására Letiltás - Még néhány dolog + 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örlésre kerül - ez a művelet nem vonható vissza! + 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 közvetítő-kiszolgálót + Az alkalmazásjelkód helyettesítve lesz egy önmegsemmisítő jelkóddal. + Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek. + Engedélyezi a hangüzeneteket? + Mindig használjon továbbítókiszolgálót mindig A hívás már befejeződött! Engedélyezés - Az összes ismerősével kapcsolatban marad. + Az összes partnerével kapcsolatban marad. Élő csevegési üzenet visszavonása - Az üzenetek végleges törlése csak abban az esetben van engedélyezve, 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 - hibás az üzenet hasító értéke + hibás az üzenet kivonata Mindig fut - 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é teszi az értesítések fogadását. + 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ás aktív, ez kikapcsolja a háttérszolgáltatást és az új üzenetek rendszeres lekérdezését. A beállításokban újraengedélyezheti. - Biztosan letiltja? + 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ő. 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ő - Az elküldött üzenetek végleges törlése engedélyezve van 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 meg van nyitva. A háttérszolgáltatás nem indul el + 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 megszakad. A régi fogadási cím kerül felhasználásra. + A kliens megszakítja a cím módosítását, és a régi fogadási címet fogja használni. Engedélyezés - Hibás számítógép cím + Hibás a számítógép címe Profil hozzáadása - Csatolás - Alkalmazás jelkód + Mellékelés + Alkalmazásjelkód Felkérték a kép fogadására Kamera Nem érhető el a Keystore az adatbázis jelszavának mentéséhez hívás folyamatban Képek automatikus elfogadása - A hívások kezdeményezése engedélyezve van az ismerősei számára. + 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. - Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, 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 között engedélyezve van. ALKALMAZÁS Hívás folyamatban - Mindkét fél küldhet üzenetreakciókat. + Mindkét fél hozzáadhat az üzenetekhez reakciókat. Mindkét fél tud hívásokat kezdeményezni. Sikertelen hitelesítés - Az összes %s által írt új üzenet elrejtésre kerül! - Alkalmazás verzió: v%s - A hívások kezdeményezése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. + %s összes új üzenete el lesz rejtve! + 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 Társított hordozható eszköz kapcsolódva - Szerepkör megváltoztatása + Szerepkör módosítása Kapcsolódva - Hitelesítő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ési adat 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ódott + partneri kapcsolatot kért 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ősei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat. - Kapcsolódás egyszer használható meghívó-hivatkozással? + 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) Csak név @@ -208,40 +207,40 @@ Cím létrehozása Másolás Folytatás - Kapcsolódás egy hivatkozáson keresztül? - Az ismerős már létezik + 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? - Kimásolva a vágólapra - Kapcsolatkérés elküldve! + Partner ellenőrizve + Kapcsolódik saját magához? + Vágólapra másolva + Kapcsolódási kérés elküldve! Kapcsolódás a számítógéphez Kapcsolat - Név helyesbítése erre: %s? + Helyesbíti a nevet a következőre: %s? Időtúllépés kapcsolódáskor - Kapcsolódás a következővel: %1$s? + 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 + 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-cím létrehozása - törölt ismerős - Csoporttag üzenetének törlése? + törölt partner + Törli a tag üzenetét? A csevegés fut - Egyszer használható 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 @@ -249,14 +248,14 @@ 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 - Nem lehet meghívni az ismerősöket! - 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örölte a csoportot Törlés az összes tagnál Hivatkozás létrehozása Csevegési beállítások @@ -272,15 +271,15 @@ Jelenleg támogatott legnagyobb fájl méret: %1$s. Fájl törlése Hamarosan! - cím megváltoztatása nála: %s … + cím módosítása %s számára… Csevegési adatbázis importálva Ü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 @@ -289,77 +288,77 @@ készítő Megerősítés Csak nálam - %d üzenet törlése? + 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? + Törli a csoportot? Adatbázis fejlesztésének megerősítése - Saját profil létrehozása - cím megváltoztatása… + Profil létrehozása + cím módosítása… kapcsolódás… Hívás kapcsolása - A fájlok- és a médiatartalmak törlése? + Törli a fájlokat és a médiatartalmakat? befejezett CSEVEGÉSI ADATBÁZIS - Önmegsemmisító jelkód megváltoztatása - Sorbaállítás létrehozása + Önmegsemmisítő jelkód módosítása + Várólista 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 - Egyéni időbeállítás + 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) + kapcsolódás (bemutatkozva) Csoporthivatkozás létrehozása Csevegési konzol Fájlok törlése az összes csevegési profilból - Sorbaállítás törlése - Ismerős törlése - cím megváltoztatása… + Várólista 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ása + Válasszon ki egy fájlt Kép törlése Fájl létrehozása - Tikos csoport létrehozása + Titkos csoport létrehozása Elvetés - Ismerős törlése? + Törli a partnert? Kiürítés Cím létrehozása, hogy az emberek kapcsolatba léphessenek Önnel. - Biztonsági kódok összehasonlítása az ismerősökével. + Biztonsági kódok összehasonlítása a partnerekével. Fájl-összehasonlítás Csevegések - Üzenet törlése? - Függőben lévő ismerőskérelem törlése? + Törli az üzenetet? + Törli a függőben lévő kapcsolatot? Adatbázis titkosítva! - Üzenetek kiürítése? + Kiüríti a csevegést? Adatbázis visszafejlesztése Üzenetek kiürítése - Adatbázis titkosítási jelmondat frissítve lesz. + Az adatbázis titkosítási jelmondata frissítve lesz. Kapcsolódás automatikusan Adatbázishiba - Adatbázis titkosítási jelmondat frissül és eltárolásra kerül a beállításokban. + 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 jelmondat 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ázis-azonosítók és átvitelelkülönítési 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 Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár ki van kapcsolva. Letiltás Letiltás az összes csoport számára Engedélyezés az összes csoport számára - engedélyezve az ismerős 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 @@ -367,23 +366,23 @@ %dmp Kézbesítési jelentések! 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 + Titkosítás-visszafejtési hiba + Eltűnik: %s szerkesztve Törlés %d óra %d hónap - Cím törlése? - Kézbesítési jelentések letiltása? + 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 az összes tag számára + Letiltás Fejlesztői eszközök Adatbázis-jelmondat %d nap Kapcsolat bontva - Az adatbázis egy véletlenszerű jelmondattal van titkosítva, ami megváltoztatható. + 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 @@ -392,50 +391,50 @@ Eltűnő üzenet Ne hozzon létre címet Ne mutasd újra - SimpleX zár 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 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. - Kézbesítés jelentések 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 - Duplikált megjelenített név! - Letiltás (felülírások megtartásával) + Duplikált megjelenítendő név! + Letiltás (egyéni beállítások megtartása) Adatbázis fejlesztése %d üzenet letiltva - Eltűnik ekkor: + Eltűnik %d hét engedélyezve az Ön számára Eltűnő üzenetek Törlés - Törlés, és az ismerős értesítése + Törlés, és a partner értesítése letiltva - %d másodperc + %d mp Az összes fájl törlése - Az adatbázis titkosításra kerül. + Az adatbázis titkosítva lesz. Adatbázis-jelmondat és -exportálás - Az adatbázis titkosításra kerül és a jelmondat a Keystore-ban lesz tárolva. - Automatikus üzenet törlés engedélyezése? + 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 - Szétkapcsolás + Kapcsolat bontása Szerkesztés - Letiltás (csoport felülírások megtartásával) + Letiltás (csoport egyéni beállításainak megtartása) %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 @@ -448,131 +447,131 @@ Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben. alapértelmezett (%s) duplikált üzenet - Számítógép leválasztása? - Számítógép-alkalmazás verziója %s nem kompatibilis ezzel az alkalmazással. + 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 + %d fájl, %s összméretben A csevegés megnyitásához adja meg az adatbázis jelmondatát. %dnap 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 - A tárolt fájlok- és a médiatartalmak titkosítása - Hiba a cím beállításakor + Tárolt fájlok és médiatartalmak titkosítása + Hiba történt a cím beállításakor A csoportmeghívó lejárt - Hiba az ICE-kiszolgálók mentésekor + 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 + 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ített név nem tartalmazhat szóközöket. + A megjelenítendő név nem tartalmazhat szóközöket. Csoport - Üdvözlőüzenet megadása… (nem kötelező) - 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 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 Csoporthivatkozások Végre, megvannak! 🚀 - Hiba 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 adatvédelem frissítésekor + Hiba történt a csevegés elindításakor + A csoportprofil a tagok eszközein tárolódik, nem a kiszolgálókon. + 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 + 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 + Gyors és nem kell várni, amíg az üzenetküldő online lesz! + Hiba történt a csoporthoz való csatlakozáskor Kedvenc - Csoport moderáció + Csoport moderálása Fájl Csoporthivatkozás - titkosítás-újraegyeztetés szükséges ehhez: %s - Hiba a profilváltáskor! + 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) + Engedélyezés (egyéni beállítások megtartása) Adja meg a helyes jelmondatot. - A csoport törlésre kerül az Ön számára - ez a művelet nem vonható vissza! - Adatbázis titkosítása? - A zárolási képernyőn megjelenő hívások engedélyezése a Beállításokban. - titkosítás elfogadva - Kézbesítési jelentések engedélyezése? - Hiba a csoportprofil mentésekor + 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ása 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ókról. Akkor is, ha le van tiltva a beszélgetésben. Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés. Zárolás engedélyezése SÚGÓ - Teljesen decentralizált - csak a tagok számára látható. + Teljesen decentralizált – csak a tagok számára látható. Fájl: %s Hívás befejezése - Hiba a csoporthivatkozás törlésekor - Fájl elmentve + Hiba történt a csoporthivatkozás törlésekor + Fájl mentve Kapcsolat javítása? Fájlok és médiatartalmak KONZOLHOZ - Sikertelen titkosítás-újraegyeztetés. - Hiba a felhasználó-profil törlésekor + Nem sikerült a titkosítást újraegyeztetni. + Hiba történt a felhasználói profil törlésekor Csoporttag általi javítás nem támogatott - Üdvözlőüzenet 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 + Nem sikerült betölteni a csevegést Kiszolgáló megadása kézzel A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! - Hiba a csoporthivatkozás létrehozásakor + 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 + Engedélyezés (csoport egyéni beállításainak megtartása) + Hiba történt a partner törlésekor A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra) - Hiba a szerepkör megváltoztatásakor + Hiba történt a szerepkör módosításakor Javítás 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 tagok küldhetnek fájlokat és médiatartalmakat. Törlés ennyi idő után - Hiba a beállítás megváltoztatásakor - Hiba a csoporthivatkozás frissítésekor + 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 csoportprofil 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 + Hiba történt a függőben lévő kapcsolat 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 a tag eltávolításakor + Hiba történt a tag eltávolításakor befejeződött A csoport üdvözlőüzenete - Csoport neve: - Hiba a meghívó küldésekor + 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 Adja meg ennek az eszköznek a nevét… Hiba @@ -581,49 +580,49 @@ súgó Önmegsemmisítő jelkód engedélyezése KÍSÉRLETI - Hiba a cím megváltoztatásának megszakításakor - Hiba a fájl fogadásakor - titkosítás rendben - Hiba a kapcsolatkérés törlésekor - Kézbesítési jelentések engedélyezése a csoportok számára? - Ismerős általi javítás nem támogatott + 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ása rendben van + Hiba történt a partneri kapcsolatké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 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 + Hiba történt a csevegés megállításakor titkosítás rendben %s számára - A csoport törlésre kerül az összes 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 a cím megváltoztatásakor + 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ár bekapcsolása - Hiba a kapcsolat szinkronizálásakor - Hiba a cím létrehozásakor + 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 kapcsolatkérés 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 partneri kapcsolatké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édiatartalmak - A kép elmentve a „Galériába” + Kép mentve a galériába Elrejtés Azonnal - A fájlok- és a médiatartalmak 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 nevének és az üzenet tartalmának elrejtése + 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ó @@ -635,7 +634,7 @@ Hogyan Összecsukás Kép - Fejlesztett adatvédelem és biztonság + Továbbfejlesztett adatvédelem és biztonság Mellőzés Kép elküldve Se név, se üzenet @@ -646,40 +645,40 @@ Inkognitó Használati útmutató Alkalmazás képernyőjének elrejtése a gyakran használt alkalmazások között. - Javított kiszolgáló konfiguráció + Továbbfejlesztett kiszolgálókonfiguráció Előzmények Rejtett profiljelszó Adatbázis importálása Importálás Azonnali értesítések Inkognitómód - Csevegési adatbázis importálása? + 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édiatartalmak küldése le van tiltva. + A fájlok és a médiatartalmak küldése le van tiltva. Hogyan működik - Elrejtés: - 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 eltávolításra kerül! + 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.]]> - A kapott SimpleX Chat-meghívó-hivatkozását megnyithatja a böngészőjében: + 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ési profil létrehozása - Levélszemét elleni védelem + 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örválasztás bővítése + Különböző nevek, profilképek és átvitelelkülönítés. + Elutasítás esetén a kérés küldője NEM kap értesítést. + 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íva Érvénytelen kapcsolattartási hivatkozás @@ -687,20 +686,20 @@ nincsenek részletek Nem fogadott hívás Világos - Az üzenet törlésre kerül - ez a művelet nem vonható vissza! + Az üzenet törölve lesz – ez a művelet nem vonható vissza! Markdown súgó új üzenet 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 arról, hogy az XFTP-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. - Nincs kiválasztva ismerős - Nincsenek fogadott, vagy küldött fájlok + 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 nevű csoportba Zárolási mód Új hordozható eszköz Kapcsolatok megtartása @@ -711,17 +710,17 @@ Nagy fájl! Helyi név Hálózat és kiszolgálók - Értesítés előnézete + Értesítésekben megjelenő információk 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 küldése le van tiltva ebben a csevegésben. + 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. + Ez akkor fordulhat elő, ha Ön vagy a partnere egy régi adatbázis biztonsági mentését használta. Ú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 küldése az üzenetekre le van tiltva. + A reakciók hozzáadása az üzenetekhez le van tiltva. Nem nincs szöveg TAG @@ -734,17 +733,17 @@ 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. - Az 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 kapcsolatkérés + Új partneri kapcsolatkérés Csatlakozás a csoporthoz Társított számítógép beállítások - meghíva az Ön csoporthivatkozásán keresztül + meghíva a saját csoporthivatkozásán keresztül elhagyta a csoportot Társított számítógépek - Nincs alkalmazás jelkód + Nincs alkalmazásjelkód Némítás, ha inaktív! A meghívó lejárt! (csak a csoporttagok tárolják) @@ -752,105 +751,105 @@ bekapcsolva Japán és portugál kezelőfelület Az üzenetek végleges törlése le van tiltva. - %s nevű hordozható eszközzel]]> + %s nevű hordozható eszköz le lett választva]]> 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 Az üzenetek végleges törlése le van tiltva ebben a csevegésben. Max 40 másodperc, azonnal fogadható. 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. + 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 Társított hordozható eszköz - Lehetővé teszi, hogy egyetlen csevegőprofilon belül több névtelen 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. Az üzenet törlésre lesz jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. 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 nincsenek duplikálva. - 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álható meghívó-hivatkozás + függőben lévő kapcsolat + Egyszer használható meghívó Értesítések Egyszerre csak 10 kép küldhető el - ajánlott %s: %2s + felajánlotta a következőt: %s: %2s Nem kompatibilis! Tegye priváttá a profilját! Üzenetkézbesítési hiba - Több csevegőprofil + 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. - A felhasználói profilok, névjegyek, csoportok és üzenetek csak az eszközön kerülnek tárolásra a kliensen belül. + 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Ő Megjelölés olvasatlanként Továbbiak - Bejelentkezés hitelesítőadatokkal - érvénytelen üzenet formátum + Bejelentkezés hitelesítési adatokkal + érvénytelen üzenetformátum Csatlakozás Az értesítések az alkalmazás elindításáig nem fognak működni kikapcsolva` (ez az eszköz: v%s)]]> - ajánlott %s + felajánlotta a következőt: %s Csoport elhagyása - Az összes %s által írt üzenet megjelenik! - 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 az üzenetet visszafejteni, mert Ön, vagy a partnere egy régi adatbázis biztonsági mentését használta.\n3. A kapcsolat sérült. megfigyelő inkognitó a csoporthivatkozáson keresztül - Onion-kiszolgálók használata, ha azok rendelkezésre állnak. - Ismerősök meghívása + 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álasztva csevegés + Csatlakozás mint: %s + Nincs csevegés kijelölve Csak helyi profiladatok - inkognitó egy egyszer használható hivatkozáson keresztül - Moderálva lett ekkor: %s - Egyszer használható 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 Chatben - Moderálva ekkor: + Moderálva Élő üzenetek 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! 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: + 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 + %s moderálta ezt az üzenetet Győződjön meg arról, hogy a fájl helyes YAML-szintaxist tartalmaz. Exportálja a témát, hogy legyen egy példa a témafájl szerkezetére. dőlt - Érvénytelen 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 @@ -860,36 +859,36 @@ Bejövő hanghívás Kulcstartóhiba Csatlakozik a csoporthoz? - Az inkognitómód védi személyes adatait azáltal, hogy az összes ismerőséhez ú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. - stabilabb üzenetkézbesítés.\n- picit továbbfejlesztett csoportok.\n- és még sok más! Üzenetreakciók 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 - Kapcsolatké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 kapcsolódá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-cím + SimpleX kapcsolattartási cím Megállítás Előre beállított kiszolgáló - Új csevegés kezdése + Új csevegés indítása Bárki üzemeltethet kiszolgálókat. Megnyitás Protokoll időtúllépése titok - Értesítés előnézete + Értesítésekben megjelenő információk várakozás a visszaigazolásra… Fájl megállítása a csoporthivatkozáson keresztül Időtartam a PING-ek között Eltűnő üzenet küldése - Önmegsemmisítési jelkód + Önmegsemmisítő jelkód Mentés és a csoportprofil frissítése Adatvédelem Profil SimpleX-címe @@ -899,50 +898,50 @@ Csak Ön tud hangüzeneteket küldeni. Frissítés Videó elküldve - Adatbázis-jelmondat megváltoztatása - Alkalmazás beállítások megnyitása - A jelkód nem változott! + Az adatbázis jelmondatának módosítása + Alkalmazásbeállítások megnyitása + A jelkód nem módosult! Frissítés - Kiválasztás + Kijelölés Csak Ön tud hívásokat indítani. - Biztonságos sorbaállítás + Biztonságos várólista Értékelje az alkalmazást - Egyszer használható meghívó-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 Ön engedélyezi Csökkentett akkumulátor-használat - Mentés és az ismerősök értesítése + 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 %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ó! Üdvözöljük! - Önmegsemmisítési jelkód - (beolvasás, vagy beillesztés a vágólapról) + Önmegsemmisítő jelkód + (beolvasás vagy beillesztés a vágólapról) Várakozás a videóra Válasz - Ez az Ön egyszer használható meghívó-hivatkozása! + Ez a saját egyszer használható meghívója! SimpleX Chat hívások Ú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 - Csak az ismerős nevének megjelenítése + Hivatkozások előnézetének megjelenítése + biztonsági kódja 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. + Biztonsági kód beolvasása a partnere alkalmazásából. Lépjen kapcsolatba a csoport adminisztrátorával. Videó bekapcsolva Profilnév: @@ -951,25 +950,25 @@ 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ás - Kérje meg az ismerősét, hogy engedélyezze a hangüzenetek küldését. - Ön egy egyszer használható meghívó-hivatkozást osztott meg + 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 + Ön elfogadta a kapcsolatot Elutasítás - Ismerős nevének és az üzenet tartalmának megjelenítése + Partner nevének és az üzenet tartalmának megjelenítése BEÁLLÍTÁSOK Profiljelszó mentése - Fájlküldés megállítása? - Számítógép leválasztása? - A hangüzenetek le vannak tilva! + 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-ek száma Fejlesztői beállítások megjelenítése @@ -980,7 +979,7 @@ %s (jelenlegi) Saját SMP-kiszolgáló Véletlen - Megosztás az ismerősökkel + Megosztás a partnerekkel Ön Nincsenek csevegései Küldés @@ -988,23 +987,23 @@ %s: %s 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! - %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. + 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 ekkor: %s + Elküldve: %s Jelenlegi profil használata Ez az eszköz - Megosztja a címet az ismerőseivel? + Megosztja a címet a partnereivel? Profiljelszó Téma - Jelmondat eltávolítása a beállításokból? + 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 a válaszra… - Ismerős nevének beállítása… + Partner nevének beállítása… Tag feloldása QR-kód beolvasása Kiszolgáló tesztelése @@ -1015,41 +1014,41 @@ Rendszer Elküldés Biztonsági kód - Adja meg a helyes, jelenlegi jelmondatát. + Adja meg a helyes, jelenlegi jelmondatot. Az elküldött üzenetek végleges törlése le van tiltva. - Az üzenetreakciók küldése le van tiltva. + 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 Kapott hivatkozás beillesztése - Kiszolgálók mentése? + Menti a kiszolgálókat? A SimpleX Chat biztonsága a Trail of Bits által lett auditálva. - frissítette a csoport profilját + frissítette a csoportprofilt SIMPLEX CHAT TÁMOGATÁSA SimpleX Chat szolgáltatás - Nem lehet üzeneteket küldeni! + Ön megfigyelő %s hitelesítve - Jelszó megjelenítése + Jelszó a megjelenítéshez Adatvédelem és biztonság Eltávolítás A jelkód beállítva! Elküldött üzenet - Ismerősök kiválasztása + Partnerek kijelölése ismeretlen üzenetformátum Kiszolgálók mentése Üdvözlőüzenet mp - A profilfrissítés elküldésre került az ismerősök számára. + A profilfrissítés el lesz küldve a partnerei számára. Egyszerűsített inkognitómód - Üdvözlőüzenet mentése? + 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 + Ön meghívta egy partnerét %s nincs hitelesítve Koppintson ide a kapcsolódáshoz Ennek az eszköznek a neve @@ -1062,7 +1061,7 @@ Adatbázis-jelmondat beállítása Üzenetbuborék színe Időszakosan indul - Ez az Ön SimpleX-címe! + Ez a saját SimpleX-címe! eltávolítva Megosztás SimpleX csapat @@ -1071,61 +1070,61 @@ tulajdonos Bekapcsolás %s, %s és %s kapcsolódott - Egyszer használható SimpleX-meghívó-hivatkozás + Egyszer használható SimpleX meghívó Hívások nem sikerült elküldeni KEZELŐFELÜLET SZÍNEI - 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 + 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 SMP-kiszolgálók Videó - Automatikus elfogadási beállítások mentése - Újraegyzetetés + SimpleX-cím beállításainak mentése + Újraegyeztetés Várakozás a videóra Saját XFTP-kiszolgálók Videó kikapcsolva Privát fájlnevek - Beállítások mentése? + Menti a beállításokat? Jelkód Ismeretlen hiba Saját SMP-kiszolgálójának címe - Csevegés konzol megnyitása + 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 kapcsolatkérést. - Hibás adatbázis-jelmondat + Feloldja a tag letiltását? + A kérés küldője törölhette a kapcsolódá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 - egyszer használható meghívó-hivatkozáson keresztül + 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 Társítás számítógéppel PROFIL - port %d + %d-s port Kapcsolódás egy hivatkozáson keresztül Cím megosztása - A kiszolgáló QR-kódjának beolvasása + Kiszolgáló QR-kódjának beolvasása Megállítás - Címmegosztás megállítása? - Csevegési profilok megváltoztatása - Csatlakozáskérés megismétlése? + Megállítja a címmegosztást? + Csevegési profilok módosítása + Megismétli a csatlakozási kérést? Várakozás a képre Hangüzenetek - Biztosan eltávolítja? + Eltávolítja a tagot? Biztonsági kód hitelesítése eltávolította Önt SimpleX-cím - Megjelenítés: + Megjelenítve: válasz fogadása… - Adatbázismentés visszaállítása? + Visszaállítja az adatbázismentést? Üzenetek fogadása… %s és %s kapcsolódott Ön megfigyelő @@ -1133,34 +1132,34 @@ Jelkód beállítása Újdonságok Csoport megnyitása - Elküldve ekkor: + Elküldve A hangüzenetek küldése le van tiltva. - Szobák utolsó üzeneteinek megjelenítése a listanézetben + Legutóbbi üzenetek 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! + 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! Várakozás a fájlra simplexmq: v%s (%2s) - Szétkapcsolás + Leválasztás Véletlenszerű profil - Hibás 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ő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 + Partner nevének beállítása Csak Ön tud eltűnő üzeneteket küldeni. Médiatartalom megosztása… Ön: %1$s @@ -1168,15 +1167,15 @@ Színek visszaállítása Mentés Váltás - A kapott hivatkozás beillesztése az ismerőshöz való kapcsolódáshoz… + A kapott hivatkozás beillesztése a partnerhez való kapcsolódáshoz… Beolvasás - Port megnyitása a tűzfalon + Port nyitása a tűzfalban indítás… Leállítás elküldve 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 @@ -1185,9 +1184,9 @@ Kézbesítési jelentések küldése SimpleX-cím Koppintson a - Mentés és az 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 @@ -1196,49 +1195,49 @@ Zárolási mód Fájl visszavonása XFTP-kiszolgálók - A fájlok- és a médiatartalmak küldése le van tiltva. + A fájlok és a médiatartalmak küldése le van tiltva. Fájl megosztása… Mentés - közvetítő-kiszolgálón keresztül + továbbítókiszolgálón keresztül Megosztás megállítása Ön eltávolította őt: %1$s Jelmondat mentése és a csevegés megnyitása - Beállítások mentése? - Nincsenek felhasználó-azonosítók. + Menti a beállításokat? + Nincsenek felhasználói 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? + 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 Védje meg a csevegési profiljait egy jelszóval! - Csak az ismerőse tud eltűnő üzeneteket küldeni. + Csak a partnere tud eltűnő üzeneteket küldeni. Saját ICE-kiszolgálók - QR-kód beolvasása számítógépről + QR-kód beolvasása a számítógépről SimpleX logó Feloldás Némítás megszüntetése SimpleX Chat megnyitása a hívás fogadásához - Fájlfogadás megállítása? - - értesíti az ismerősöket a törlésről (nem kötelező)\n- profil nevek szóközökkel\n- és még sok más! + 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 + Fogadva: %s SimpleX-zár Mentés és a csoporttagok értesítése Visszaállítás - Csak az ismerőse tud üzenetreakciókat küldeni. + Csak a partnere adhat hozzá reakciókat az üzenetekhez. Hangüzenetek Ön elhagyta a csoportot Hangüzenet rögzítése SimpleX-zár bekapcsolva - közvetlen üzenet küldése + elküldés a partnernek Beolvasás hordozható eszközről Kapcsolatok hitelesítése Üzenet megosztása… @@ -1252,259 +1251,259 @@ Kapcsolat hitelesítése Tudjon meg többet A fájl küldője visszavonta az átvitelt. - Csevegési szolgáltatás megállítása? - Fogadva ekkor: + Megállítja a csevegést? + Fogadva Beállítva 1 nap Felfedés Fogadott üzenetbuborék színe - 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? + 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ése - A(z) %1$s nevű profiljának SimpleX-címe megosztásra fog kerülni. + 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. - 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. + 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 ide az új csevegés indításához - Várakozás a számítógépre… - Az üzenetküldés jövője - Hálózati beállítások megváltoztatása? + Várakozás a számítógép-alkalmazásra… + 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 - cím megváltoztatva nála: %s + Ön módosította a címet %s számára fájlok fogadása egyelőre még nem támogatott Csoportprofil mentése Visszaállítás alapértelmezettre - Hacsak az ismerőse 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 az ismerősét, hogy hozzon létre egy másik kapcsolattartási 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.\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. + Használat új kapcsolatokhoz + 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 - kapcsolattartási cím-hivatkozáson keresztül + a kapcsolattartási címhivatkozáson keresztül a SimpleX a háttérben fut a push értesítések használata helyett.]]> - Az ismerősének online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nVisszavonhatja ezt az ismerőskérelmet és eltávolíthatja 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ágimenté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 + A partnereinek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nVisszavonhatja ezt a kapcsolatot é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 a 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 ismerősétő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. 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 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örlésre kerül. + 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ásokban” érhető el - A profilja elküldésre kerül az 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 + Ö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 jelszavát + A kiszolgálónak engedélyre van szüksége a várólisták 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 + A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal. 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! - Ellenőrizze a hálózati kapcsolatát a következővel: %1$s, és próbálja újra. + 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 az 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ősétől érkező üzenetek fogadására szolgál (hiba: %1$s). + É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. + Hiba történt a kapcsolódáskor ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál: %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. - Ön egy egyszer használható meghívó-hivatkozást osztott meg inkognitóban - Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétő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 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.]]> Profil felfedése 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) az ismerőse 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 ismerősétől. - Ez a beállítás csak a jelenlegi csevegési profiljában lévő üzenetekre vonatkozik - Meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. + 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 - Az 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 az alkalmazásban gombra.]]> - A 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 + 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 - 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 kapcsolatkérése elfogadásra kerül, várjon, vagy ellenőrizze később! + 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 + Átvitelelkülönítés + Akkor lesz kapcsolódva, ha a kapcsolódási kérését elfogadják, 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 kvantumrezisztens-protokollon keresztül. - - 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése. + Biztonságos kvantumbiztos protokollon keresztül. + - legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni időkorlát beállítása az üzenetek eltűnéséhez.\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 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. 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 - A 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. - Ön megváltoztatta %s szerepkörét erre: %s + 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 - Az Ön adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. - (a megosztáshoz az ismerősével) + Adatainak védelme érdekében a SimpleX külön azonosítókat használ minden egyes kapcsolatához. + (a megosztáshoz a partnerével) Csoportmeghívó elküldve - Átvitel-izoláció módjának frissítése? - Átvitel-izoláció + Frissíti az átvitelelkülönítési módot? + Átvitelelkülönítés 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 elküldésre kerül a csoporttagok számára + 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 írja be a teljes jelszavát a keresőmezőbe a „Csevegési profilok” 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 az ismerőse számára, hogy hangüzeneteket küldhessenek egymásnak. + 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.]]> - cím megváltoztatva - Az 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é 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, az ismerősei és az elküldött üzenetei az eszközén kerülnek tárolásra. + Ö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ön lévő alkalmazás társítását a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, ha az be van kapcsolva + A profilja, a partnerei és az elküldött üzenetei az 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ó-hivatkozás! - Új csevegés kezdése - A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül! - Nem veszíti el az ismerőseit, ha később törli a címét. + 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 Önnel! - saját szerepköre megváltozott erre: %s + Ö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 - Csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. + Ö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ómód használata kapcsolódáskor. - 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 nevű csoporthoz!]]> + 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ó profil használata kapcsolódáskor ki/be. + Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoporthivatkozást később törli, akkor nem fogja elveszíteni a csoport meglévő tagjait. + Ö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 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 kapcsolatkérés elküldésre kerül 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üldött egy kapcsolatkérést ezen a címen keresztül! - Megoszthatja ezt a SimpleX-címet az ismerőseivel, hogy kapcsolatba léphessenek vele: %s. + A csatlakozási kérése el lesz küldve ennek a csoporttagnak. + Ha egy inkognitóprofilt oszt meg valamelyik partnerével, a rendszer ezt az inkognitóprofilt fogja használni azokban a csoportokban, ahová az adott partnere meghívja Önt. + Már kért egy kapcsolódá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 kapcsolatot kérnek, Ö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 Weblate-en! - A kézbesítési jelentés küldése az összes ismerőse számára engedélyezésre kerül. - Protokoll időtúllépése KB-onként - Az adatbázis-jelmondat 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. - A 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 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örlésre kerülnek. Az alacsony felbontású képek viszont megmaradnak. - A 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 Weblate-en! - A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő összes ismerőse számára. + 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ók az ismerős- és csoportbeállításokban. - Az 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ó. + 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 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 - hozzájárulás a Weblate-en! - A közvetítő-kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet. - 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. + Köszönet a felhasználóknak a Weblate-en való közreműködésért! + A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. + Beállítás a rendszer-hitelesítés helyett. + Az üzenetfogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása akkor fejeződik be, amikor az üzenetküldési kiszolgáló online lesz. A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. Jelmondat mentése a Keystore-ba - Köszönet a felhasználóknak - hozzájárulás a Weblate-en! + 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! ✅ - A közvetítő-kiszolgáló megvédi az IP-címet, de megfigyelheti a hívás időtartamát. + A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. - A mentett WebRTC ICE-kiszolgálók eltávolításra kerülnek. + 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 szerepkör meg fog változni erre: %s. A csoport tagjai értesítést fognak kapni. + 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 - Egy üzenetküldő- és alkalmazásplatform, amely védi az adatait és biztonságát. - A profil aktiválásához koppintson az ikonra. - A kézbesítési jelentések le vannak tiltva %d ismerősnél + Egy üzenetváltó- és alkalmazásplatform, amely védi az adatait és biztonságát. + Koppintson ide a profil aktiválásához. + 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 Weblate-en! + 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 kérelem vissza lesz vonva! - Élő üzenet küldése - a címzett(ek) számára frissül, ahogy beírja + Az Ön által elfogadott kapcsolat 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 társított hordozható eszközön használt alkalmazással. + 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 megváltoztatni. - A jelmondat a beállításokban 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: + 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 szerepkör meg fog változni erre: %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 hasító értéke különbözik. - Ezek a beállítások csak a jelenlegi profiljára vonatkoznak + Az előző üzenet kivonata 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ésekor - hiba az üzenet megjelenítésekor - Láthatóvá teheti a SimpleXbeli 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 QR-kód nem egy SimpleX-QR-kód-hivatkozás. + GitHub-tárolónkban.]]> + 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-hivatkozás. 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? + 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 + Alkalmazásjelkód + Partner hozzáadása Koppintson ide a QR-kód beolvasásához Koppintson ide 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: ú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 ezt az adatbázist már használta egy másik eszközön, akkor a csevegés elindítása előtt vissza kell állítania azt. + Az előzmények nem lesznek elküldve az új tagok számára. Újrapróbálkozás A kamera 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. + 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? - Ennek az egyszer használható meghívó-hivatkozásnak a megosztása + Kamera-hozzáférés engedélyezése + 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… @@ -1516,23 +1515,23 @@ Kritikus hiba Belső hiba 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 - Számítógép-alkalmazásban hibás meghívókód szerepel + 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éphez való csatlakozáskor - Kapcsolat bontva a számítógéppel + A számítógép le lett választva A kapcsolat megszakadt A kapcsolat megszakadt - Számítógép kapcsolata rossz állapotban van + 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 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]]> + %s nevű hordozható eszköz le lett választva]]> Érvénytelen megjelenítendő név! - Ez a megjelenített név érvénytelen. Válasszon egy másik nevet. - %s nevű hordozható eszközzel, a következő okból: %s]]> + Ez a megjelenítendő név érvénytelen. Válasszon egy másik nevet. + %s nevű hordozható eszköz le lett választva, 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]]> @@ -1544,28 +1543,28 @@ Fejlesztői beállítások A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s %s hordozható eszköz elfoglalt]]> - Már nem tag %1$s + %1$s ismeretlen vagy már nem tag ismeretlen állapot - %1$s megváltoztatta a nevét erre: %2$s + %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ím beállítása + ú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 + frissítette a profilját + %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 kiüríté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 - Az összes üzenet törlésre kerül – ez a művelet nem vonható vissza! + 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 + Csatlakozás a csoportbeszélgetésekhez Hivatkozás beillesztése a kapcsolódáshoz! Privát jegyzetek - A keresősáv elfogadja a meghívó-hivatkozásokat. + 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 @@ -1576,29 +1575,29 @@ letiltva az adminisztrátor által Letiltva az adminisztrátor által letiltotta őt: %s - Letiltás az összes tag számára - Az összes tag számára letiltja ezt a tagot? + Letiltás + Az összes tag számára letiltja a tagot? %d üzenetet letiltott az adminisztrátor - Letiltás feloldása az összes tag számára + Feloldás Az összes tag számára feloldja a tag letiltását? Ön letiltotta őt: %s - Hiba a tag az összes csoporttag számára való letiltásakor + 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. Hanghívás A hívás befejeződött Videóhívás - Hiba a böngésző megnyitásakor + 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 + 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 ismerőse, -beszélgetése és -fájlja biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra. + 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 @@ -1610,13 +1609,13 @@ 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 hitelesíté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 @@ -1625,11 +1624,11 @@ Jelmondat beállítása Kép a képben hívások Biztonságosabb csoportok - Használja az alkalmazást hívás közben. + Alkalmazás használata hívás közben. 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 + 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 @@ -1645,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, és ü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 - Figyelmeztetés: 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 az ismerőseitő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. Érvénytelen hivatkozás - végpontok közötti kvantumrezisztens titkosítá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érülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi.]]> - végpontok közötti kvantumrezisztens titkosítással, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasí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, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve.]]> + végpontok közötti kvantumbiztos titkosítással, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve.]]> + 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 @@ -1678,7 +1677,7 @@ Engedélyek megadása Vezetékes Ethernet Mobilhálózat - Internetkapcsolat + Hálózati kapcsolat Nincs hálózati kapcsolat További Wi-Fi @@ -1692,15 +1691,15 @@ A hangüzenetek küldése le van tiltva 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 fájlok és a médiatartalmak küldése nincs engedélyezve A SimpleX-hivatkozások küldése engedélyezve van. - Számukra engedélyezve: + Számukra engedélyezve mentett - elmentve innen: %s - Továbbítva innen: + mentve innen: %s + Továbbítva innen A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet. Mentett - Elmentve innen: + Mentve innen Letöltés Továbbítás Továbbított @@ -1719,45 +1718,45 @@ 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. 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. + 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. - Hibás 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 + É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. + 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 megjelenítése + Ü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. + NE használjon privát útválasztást. Privát útválasztás - Használjon privát útválasztást ismeretlen kiszolgálókkal. + Privát útválasztás használata az ismeretlen kiszolgálókhoz. 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 az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. - Közvetlen üzenetküldés, ha az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + 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 az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. - Tor vagy VPN nélkül az IP-címe látható lesz a fájlkiszolgálók számára. + Privát útválasztás használata az 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 IP-címe láthatóvá válik 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 IP-címe látható lesz az XFTP-közvetítő-kiszolgálók számára:\n%1$s. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára:\n%1$s. Összes színmód Fekete Színmód @@ -1768,12 +1767,12 @@ Jó napot! Jó reggelt! Speciális beállítások - Alkalmazás erre + Használat ehhez Csevegés színei Csevegés témája Kitöltés Profiltéma - Csevegőlista megjelenítése új ablakban + Csevegési lista megjelenítése új ablakban Világos Világos mód Fogadott válaszüzenet-buborék színe @@ -1784,40 +1783,40 @@ Válaszüzenet-buborék színe Alapértelmezett téma beállítása Rendszer - Háttérkép kiemelés + Háttérkép kiemelőszíne Háttérkép háttérszíne - További kiemelés 2 - Alkalmazás téma + További kiemelőszín 2 + Alkalmazás témája Perzsa kezelőfelület - Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben. + Védje az IP-címét a partnerei által kiválasztott üzenetváltási 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 + Továbbfejlesztett ü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 a WebView előkészítésekor. Frissítse rendszerét az új verzióra. Lépjen kapcsolatba a fejlesztőkkel.\nHiba: %s + 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 - Üzenet-sorbaállítási információ + Üzenet várólista információi 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 kézbesített üzenet: %2$s - Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött. - Ideiglenesfájl-hiba - Üzenetállapot - Üzenetállapot: %s + a kiszolgáló várólista 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. + 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állapot - Fájlállapot: %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 kiválasztott csevegési beállítások tiltják ezt az üzenetet. + 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 @@ -1835,18 +1834,18 @@ Kapcsolódás Hibák Függőben - Statisztikagyűjtés kezdete: %s.\nAz összes adat biztonságban van az eszközén. + 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ódás a kiszolgálókhoz? - Újrakapcsolódás a kiszolgálóhoz? - Hiba a kiszolgálóhoz való újrakapcsolódáskor + Ú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 a statisztikák visszaállításakor + Hiba történt a statisztikák visszaállításakor Visszaállítás Az összes statisztika visszaállítása - Az összes statisztika visszaállítása? - A kiszolgálók statisztikái visszaállnak - ez a művelet nem vonható vissza! + 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 @@ -1857,7 +1856,7 @@ Üzenetküldési hibák Közvetlenül küldött Összes elküldött üzenet - Proxyn keresztül küldve + Proxyn keresztül küldött SMP-kiszolgáló Statisztikagyűjtés kezdete: %s. Feltöltve @@ -1871,17 +1870,17 @@ Törlési hibák Méret Feltöltött fájlok - Letöltött fájltöredékek + 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 - Nyugtázva - Nyugtázott hibák - próbálkozások - Törölt fájltöredékek + Visszaigazolt + Visszaigazolási hibák + kísérletek + Törölt töredékek Összes profil - Feltöltött fájltöredékek + Feltöltött töredékek Elkészült Kapcsolódott kiszolgálók Konfigurált XFTP-kiszolgálók @@ -1893,7 +1892,7 @@ Fogadott üzenetek Letöltési hibák Hiba - Hiba a kiszolgálókhoz való újrakapcsolódáskor + 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 @@ -1906,11 +1905,11 @@ Munkamenetek átvitele Összes kapcsolat Statisztikák - Információk megjelenítése ehhez: + 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 + Üzenetfogadás Feliratkozva Feliratkozási hibák Mellőzött feliratkozások @@ -1919,96 +1918,96 @@ 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) + 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 - Hagyja ki ezt a verziót + 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. - Frissítés érhető el: %s + Új verzió érhető el: %s A frissítés letöltése megszakítva Béta Letiltás Letiltva Stabil - Hiba 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 csatlakozni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. - A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbító-kiszolgáló beállításaival. + Hiba történt a(z) %1$s továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbítókiszolgálóval. + A(z) %1$s továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. + A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbítókiszolgáló beállításaival. Médiatartalom elhomályosítása Közepes Kikapcsolva Enyhe Erős - A továbbító-kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. - A továbbító-kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s. + A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. + A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s. hívás - Az ismerős törlésre fog kerülni - ez a művelet nem vonható vissza! + 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! - Ismerős törölve! - Archivált ismerősök - Nincsenek szűrt ismerősök + 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 az ismerőst + Nem lehet felhívni a partnert Nem lehet üzenetet küldeni a csoporttagnak - Kapcsolódás az ismerőshöz, várjon vagy ellenőrizze később! - Törölt ismerős. + Kapcsolódás a partnerhez, várjon vagy ellenőrizze később! + Törölt partner. Nem lehet felhívni a csoporttagot - Hívások engedélyezése? + Engedélyezi a hívásokat? Meghívás üzenet Beszélgetés megtartása - Biztosan törli az ismerőst? + Biztosan törli a partnert? kapcsolódás - Könnyen elérhető alkalmazás-eszköztárak + Könnyen elérhető eszköztárak Törlés értesítés nélkül Beállítások keresés videó - Az „Archivált ismerősökből” továbbra is küldhet üzeneteket neki: %1$s. - Ismerősök - Kérje meg az ismerősét, hogy engedélyezze a hívásokat. + 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 az ismerőse számára, hogy fel tudják hívni egymást. - A(z) %1$s nevű ismerősével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. + 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… - Kiválasztás + Kijelölés Az üzenetek az összes tag számára moderáltként lesznek megjelölve. - Nincs kiválasztva semmi + 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 kiválasztva - Az üzenetek az összes tag számára törlésre kerülnek. + %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 az ismerőseihez. + Kapcsolódjon gyorsabban a partnereihez. Folytatás - Ellenőrízze a hálózatát - Média- és fájlkiszolgálók + Ellenőrizze a hálózatát + Fájl- és médiakiszolgá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 + Könnyen elérhető csevegési eszköztár Üzenetkiszolgálók SOCKS proxy - Néhány fájl nem került exportálásra: + 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. - Az ismerősök archiválása a későbbi csevegéshez. - TCP kapcsolat - Az exportált archívumot elmentheti. + Alkalmazás egy kézzel való használata. + A partnerek archiválása a későbbi csevegéshez. + TCP-kapcsolat + Mentheti az exportált archívumot. Tippek visszaállítása - Csevegőlista átváltása: + Csevegési lista ki/be: Ezt a „Megjelenés” menüben módosíthatja. Új médiabeállítások Lejátszás a csevegési listából. Elhomályosítás a jobb adatvédelemért. - Automatikus frissítés + Automatikus alkalmazásfrissítés Létrehozás Új verziók letöltése a GitHubról. Betűméret növelése. @@ -2017,29 +2016,29 @@ Új üzenet Érvénytelen hivatkozás Ellenőrizze, hogy a SimpleX-hivatkozás helyes-e. - Hiba 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örlésre kerülnek - ez a művelet nem vonható vissza! - Archívum eltávolítása? - A feltöltött adatbázis-archívum véglegesen eltávolításra kerül a kiszolgálókról. + Hiba történt a profilváltáskor + A kapcsolata át lett helyezve ide: %s, de egy hiba történt a profilváltá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 kiválasztása - 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. + Csevegési profil kijelölése + Ne használja a hitelesítési adatokat proxyval. + Különböző proxy-hitelesítési adatok használata az összes profilhoz. + Különböző proxy-hitelesítési adatok használata az összes kapcsolathoz. Jelszó Felhasználónév - A hitelesítőadatai titkosítatlanul is elküldhetők. - Hiba a proxy mentésekor + A hitelesítési 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 + Véletlenszerű hitelesítési 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. - %1$d fájlt nem sikerült letölteni. + 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. @@ -2047,71 +2046,71 @@ Üzenetek továbbítása… %1$d fájlhiba:\n%2$s %1$s üzenet nem lett továbbítva - %1$s üzenet továbbítása? - Üzenetek továbbítása fájlok nélkül? - Az üzeneteket törölték miután kiválasztotta őket. + 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 az üzenetek továbbításakor + Hiba történt az üzenetek továbbításakor Hang elnémítva - Hiba 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 + 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. + Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítési adatok lesznek használva. Alkalmazás munkamenete - Az összes kiszolgálóhoz új, SOCKS-hitelesítő-adatok legyenek használva. + Az összes kiszolgálóhoz új, SOCKS-hitelesítési adatok lesznek 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 - Testreszabható üzenetbuborékok. + 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ó-hivatkozásokhoz. + 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 a kiszolgálók mentésekor - Nincsenek üzenet-kiszolgálók. - Nincsenek üzenetfogadó-kiszolgálók. - Nincsenek média- és fájlkiszolgálók. + A SimpleX protokollokat a Trail of Bits auditálta. + Hiba történt a kiszolgálók mentésekor + Nincsenek üzenetkiszolgálók. + Nincsenek üzenetfogadási kiszolgálók. + Nincsenek fájl- és médiakiszolgálók. A(z) %s nevű csevegési profilhoz: - Cím vagy egyszer használható meghívó-hivatkozás? + 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ó-kiszolgálók. + Nincsenek fájlküldési kiszolgálók. + Nincsenek fájlfogadási kiszolgálók. Hibák a kiszolgálók konfigurációjában. - Hiba a feltételek elfogadásakor + 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, az Ön ismerőse lehet, hogy offline állapotban van. + 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örlésre került vagy még nem érkezett meg. + 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ó-hivatkozás megosztása egy baráttal - egyetlen ismerőssel használható - személyesen vagy bármilyen üzenetküldőn keresztül megosztható.]]> - Beállíthatja az ismerős nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást. + 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ó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül. - A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat az ismerősével. + 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ó-hivatkozás? - Egyszer használható meghívó-hivatkozás létrehozása - Kiszolgáló-üzemeltetők - Hálózati üzemeltetők - Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetésben más-más üzemeltetőt használ. - Például, ha az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni. - Válassza ki a használni kívánt hálózati üzemeltetőket. + 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. @@ -2120,93 +2119,390 @@ Folytatás Feltételek felülvizsgálata Elfogadott feltételek - A feltételek automatikusan elfogadásra kerülnek az engedélyezett üzemeltetők számára: %s. - Az Ön kiszolgálói + 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.]]> %s kiszolgáló - Hálózati üzemeltető + Hálózatüzemeltető Weboldal - Feltételek elfogadva ekkor: %s. - A feltételek ekkor lesznek elfogadva: %s. + Feltételek elfogadásának ideje: %s. + A feltételek el lesznek fogadva a következő időpontban: %s. Kiszolgálók használata %s használata - A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket ezen a hivatkozáson keresztül vizsgálhatja felül: + 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.]]> %s.]]> %s.]]> Feltételek elfogadása Használati feltételek - %s kiszolgálók használatához fogadja el a használati feltételeket.]]> + %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 + Hozzáadott fájl- és médiakiszolgálók Feltételek megnyitása - Változások megnyitása - Hiba a kiszolgáló frissítésekor - A kiszolgáló-protokoll megváltozott. - A kiszolgáló üzemeltetője megváltozott. + 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 a kiszolgáló hozzáadásakor + 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! - Flux engedélyezése - Alkalmazás-eszköztárak + 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. + Eszköztárak a metaadatok jobb védelme érdekében. - Javított csevegési navigáció + Továbbfejlesztett 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 - Az Ön jelenlegi csevegőprofiljához tartozó új fájlok kiszolgálói + 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á csapattagjait a beszélgetésekhez. + 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á kvantumrezisztens titkosítással is rendelkeznek.]]> + 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 - Csevegés elhagyása? + 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 - Csapattagok hozzáadása - A csevegés minden tag számára törlésre kerül - ezt a műveletet nem lehet visszavonni! - A csevegés törlésre kerül az Ön számára - ezt a műveletet nem lehet visszavonni! - Csevegés törlése? + 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 beállításokat. + 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 - ezt a műveletet nem lehet visszavonni! + A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza! Csevegés - A szerepkör meg fog változni a következőre: %s. A csevegés tagjai értesítést fognak kapni. - Az Ön csevegési profilja el lesz küldve a csevegésben résztvevő tagok számára + 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 - Az Ön ügyfeleinek adatvédelme. + 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. + Csökkentse az üzenet méretét vagy távolítsa el a médiatartalmakat, é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ó - kérelmezve a kapcsolódáshoz + függőben lévő kapcsolat 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. - \ No newline at end of file + 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 az üzenet küldője é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 az eszközrő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 kiszolgálók üzemeltetői számára. + Kiszolgálóüzemeltetők beállítása + Nem támogatott kapcsolattartási hivatkozás + 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álókhoz. + Hiba a tag befogadásakor + %d csevegés a tagokkal + %d üzenet + 1 csevegés egy taggal + %d csevegés + A jelentés el lett küldve a moderátoroknak + A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben. + függőben lévő áttekintés + áttekintés + Csevegés az adminisztrátorokkal + Csevegés a tagokkal + Tagbefogadás + Nincsenek csevegések a tagokkal + Tagok áttekintése + Tagok áttekintése a befogadás előtt (kopogtatás). + Csevegés az adminisztrátorokkal + A tag csatlakozni akar a csoporthoz, befogadja a tagot? + Eltávolítás + Befogadás + Tag befogadása + összes + Csevegés a taggal + Új tag szeretne csatlakozni a csoporthoz. + kikapcsolva + Befogadás megfigyelőként + Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérését. + befogadta Önt + Tagbefogadás beállítása + Menti a befogadási beállításokat? + Ön befogadta ezt a tagot + Befogadás tagként + befogadta őt: %1$s + áttekintve a moderátorok által + nem lehet üzeneteket küldeni + partner letiltva + csoport törölve + eltávolítva a csoportból + csatlakozási kérés elutasítva + Ön elhagyta a csoportot + a tag régi verziót használ + Hiba a csevegés törlésekor + Ön nem tud üzeneteket küldeni! + a partner nem áll készen + nincs szinkronizálva + Törli a taggal való csevegést? + partner törölve + Csevegés törlése + Elutasítás + Elutasítja a tagot? + Cím frissítése + Csevegés megnyitása + Új csevegés megnyitása + Új csoport megnyitása + végpontok közötti titkosítással vannak védve.]]> + Hiba történt a partneri kapcsolatkérés elutasításakor + Hiba történt a csevegés megnyitásakor + Hiba történt a csoport megnyitásakor + Hiba a profil módosításakor + Megnyitás a csatlakozáshoz + Megnyitás a kapcsolódáshoz + Megnyitás az elfogadáshoz + a partnernek el kell fogadnia… + Csatlakozás a csoporthoz + Üzenet hozzáadása + Kapcsolódás + Elküldi a partneri kapcsolatkérést? + miután a kérését elfogadták.]]> + Kérés küldése üzenet nélkül + Kérés küldése + kérés elküldve + Partneri kapcsolatkérés elfogadása + Partneri kapcsolatkérés elutasítása + A kérés küldője NEM lesz értesítve. + Saját profil + Elküldés a partnernek a kapcsolódást követően. + Üdvözlőüzenet + Frissíti a címet? + A cím rövid lesz és a profil meg lesz osztva a címen keresztül. + Frissítés + Frissíti a csoporthivatkozást? + Nem lehet módosítani a profilt + Másik profil használatához a kapcsolatfelvételi kísérlet után törölje a csevegést, és használja újra a hivatkozást. + Csevegés az adminisztrátorokkal + Csevegés a tagokkal mielőtt csatlakoznának. + Gyorsabb kapcsolódás! 🚀 + Kevesebb adatforgalom a mobilhálózatokon. + Az üzenet azonnal megjelenik, amint a kapcsolódás gombra koppint. + Új szerepkör: Moderátor + Nincs privát útválasztási munkamenet + Privát útválasztás időtúllépése + Protokoll időtúllépése a háttérben + Üzenetek eltávolítása és a tagok tiltása. + Csoporttagok áttekintése + Küldjön privát visszajelzést a csoportoknak. + TCP-kapcsolat időtúllépése a háttérben + Profil betöltése… + Rövid leírás: + Saját névjegy: + Névjegy: + A névjegy túl hosszú + A leírás túl hosszú + Partneri kapcsolatkérés elfogadása + Üzleti kapcsolat + Csoport + Koppintson a „Csatlakozás a csoporthoz” gombra + Saját csoport + Üzleti partner + Partner + Koppintson a „Kapcsolódás” gombra a csevegéshez + Koppintson a „Kapcsolódás” gombra a kérés elküldéséhez + Az üzeneteltűnési idő csak az új partnerekre vonatkozik. + Inkognitóprofil használata + Saját cím létrehozása + Eltűnő üzenetek engedélyezése alapértelmezetten. + Tartsa tisztán a csevegéseit + Névjegy és üdvözlőüzenet beállítása a profilokhoz. + Saját cím megosztása + Rövid SimpleX-cím + Cím frissítése + Üdvözölje a partnereit 👋 + 4 új kezelőfelületi nyelv + Katalán, indonéz, román és vietnámi – köszönjük a felhasználóinknak! + Csoporthivatkozás frissítése + A hivatkozás rövid lesz és a csoportprofil meg lesz osztva a hivatkozáson keresztül. + Régi cím megosztása + Régi (hosszú) hivatkozás megosztása + PARTNERI KAPCSOLATKÉRÉSEK A CSOPORTOKBÓL + A tag törölve lett – nem lehet elfogadni a kérést + a(z) %1$s nevű csoportból partneri kapcsolatot kért + Ez a beállítás a jelenlegi profiljára vonatkozik + Megnyitás a bot használatához + Koppintson a „Kapcsolódás” gombra a bot használatához + Bot + A parancsok küldéséhez kapcsolódva kell lennie. + A fájlok és a médiatartalmak küldése engedélyezve van a partnerei számára. + A fájlok és a médiatartalmak küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. + A fájlok és a médiatartalmak küldése le van tiltva. + Mindkét fél küldhet fájlokat és médiatartalmakat. + Csak Ön küldhet fájlokat és médiatartalmakat. + Csak a partnere küldhet fájlokat és médiatartalmakat. + A fájlok és a médiatartalmak küldése le van tiltva ebben a csevegésben. + Elavult beállítások + Tiszta hivatkozás megnyitása + Teljes hivatkozás megnyitása + Nyomonkövetési paraméterek eltávolítása a hivatkozásokból + SimpleX továbbítókiszolgáló-hivatkozás + Hiba a csevegés olvasottként való megjelölésekor + A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. + A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. + A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. + nincs előfizetés + Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs előfizetés). + 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_circle_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_circle_right.svg new file mode 100644 index 0000000000..f5c27f5c9e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_circle_right.svg @@ -0,0 +1 @@ + \ 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_chat_person.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_person.svg new file mode 100644 index 0000000000..adfc09bd4a --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_person.svg @@ -0,0 +1 @@ + \ 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_down.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_down.svg new file mode 100644 index 0000000000..b8416ff173 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_up.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_up.svg new file mode 100644 index 0000000000..ca6a17bbde --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_contact_support.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_contact_support.svg new file mode 100644 index 0000000000..b06e925603 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_contact_support.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg new file mode 100644 index 0000000000..252afc1acc --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/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_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_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_person_add_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_filled.svg new file mode 100644 index 0000000000..928f1de9b2 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_filled.svg @@ -0,0 +1 @@ + \ 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_waving_hand.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_waving_hand.svg new file mode 100644 index 0000000000..28781faa07 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_waving_hand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index eeedcaa450..909c6c7cfe 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -103,16 +103,16 @@ 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, + Anda terhubung ke server yang digunakan untuk menerima pesan dari koneksi 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 + galat + Tautan 1-kali %1$d pesan yang terlewati %1$d pesan yang dilewati %s tidak didukung. Harap pastikan kamu menggunakan versi yang sama pada kedua perangkat.]]> @@ -216,7 +216,7 @@ TIdak pernah Mati Hanya 10 video dapat dikirim pada saat bersamaan - enkripsi ujung-ke-ujung 2 lapis.]]> + 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 @@ -307,7 +307,7 @@ Kontak %d pesan dihapus dihapus - Mencoba terhubung ke server untuk menerima pesan dari kontak ini. + Mencoba terhubung ke server untuk menerima pesan dari koneksi ini. disimpan diundang untuk terhubung Deskripsi @@ -341,7 +341,7 @@ %1$d berkas lainnya gagal. Kontak sudah ada menghubungkan… - kirim pesan langsung + kirim untuk terhubung Anda tidak memiliki obrolan Memuat obrolan… Ketuk untuk Hubungkan @@ -474,7 +474,7 @@ Izin Anda Izin kontak bawaan (%s) - Pesan sementara + Pesan menghilang Pesan pribadi Pesan suara Setel preferensi grup @@ -487,8 +487,8 @@ Hapus pesan tidak dapat dibatalkan dilarang dalam obrolan ini. Panggilan audio/video dilarang. Reaksi pesan dilarang. - Anggota grup dapat mengirim pesan sementara. - Pesan sementara dilarang di grup ini. + Anggota dapat mengirim pesan sementara. + Pesan sementara dilarang. %d minggu %d minggu Keamanan SimpleX Chat diaudit oleh Trail of Bits. @@ -561,11 +561,11 @@ Pesan sementara dilarang dalam obrolan ini. Pesan suara dilarang dalam obrolan ini. Anda dan kontak dapat menambahkan reaksi pesan. - Anggota grup dapat hapus pesan terkirim secara permanen. (24 jam) - Anggota grup dapat mengirim pesan suara. - Hapus pesan yang tidak dapat dibatalkan dilarang di grup ini. + 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 grup dapat kirim tautan SimpleX. + Anggota dapat kirim tautan SimpleX. %d jam %d jam %d hari @@ -664,7 +664,7 @@ Kirim pesan suara tidak diizinkan. Hingga 100 pesan terakhir dikirim ke anggota baru. Pesan suara - Pesan sementara + Pesan menghilang Pesan langsung Profil obrolan tersembunyi Moderasi grup @@ -692,7 +692,7 @@ 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. + Koneksi Anda dipindahkan ke %s tetapi gagal saat mengarahkan Anda ke profil. Kirim kami email Kirim pertanyaan dan ide Kunci SimpleX @@ -715,7 +715,7 @@ Kata sandi profil tersembunyi Mikrofon Privasi didefinisikan ulang - Ini dapat diubah nanti di pengaturan. + Hal yang mempengaruhi baterai Saat aplikasi sedang berjalan Notifikasi pribadi Instan @@ -768,19 +768,19 @@ koneksi %1$d koneksi terjalin menghubungkan… - Anda bagikan tautan sekali - Anda bagikan tautan sekali samaran + Anda bagikan tautan 1-kali + Anda bagikan tautan samaran 1-kali via tautan grup samaran via tautan grup via tautan alamat kontak samaran via tautan alamat kontak - via tautan sekali + via tautan 1-kali Alamat kontak SimpleX - samaran via tautan sekali + samaran via tautan 1-kali Tautan lengkap Tautan grup SimpleX Tautan SimpleX - Undangan sekali SimpleX + Undangan 1-kali SimpleX via %1$s Gagal simpan server XFTP Nama tampilan tidak valid! @@ -793,7 +793,7 @@ Teks ini tersedia di pengaturan Ketuk untuk memulai obrolan baru Anda diundang ke grup - gabung sebagai %s + Gabung sebagai %s menghubungkan… Tidak dapat kirim pesan Bagikan pesan… @@ -960,8 +960,8 @@ Ukuran huruf Kirim pesan pribadi ke anggota dilarang. Kirim tautan SimpleX dilarang - Reaksi pesan dilarang di grup ini. - Tautan SimpleX dilarang di grup ini. + Reaksi pesan dilarang. + Tautan SimpleX dilarang. Server Tak terlindungi Sesi aplikasi @@ -997,7 +997,7 @@ Panggilan pada layar terkunci: Speaker Izin dalam pengaturan - Generasi baru\ndari perpesanan pribadi + Perpesanan masa depan Temukan izin ini di pengaturan Android dan ubah secara manual. Earpiece Matikan @@ -1052,9 +1052,9 @@ Kirim hingga 100 pesan terakhir untuk anggota baru. Kirim pesan sementara dilarang. Jangan perlihat pesan riwayat ke anggota baru. - Anggota grup dapat mengirim pesan pribadi. - Pesan suara dilarang di grup ini. - Anggota grup dapat memberi reaksi pesan. + Anggota dapat mengirim pesan pribadi. + Pesan suara dilarang. + Anggota dapat memberi reaksi pesan. %d bulan pemilik %d dtk @@ -1064,8 +1064,8 @@ %db %dbln Apa yang baru - Anggota grup dapat kirim berkas dan media. - Berkas dan media dilarang di grup ini. + 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. @@ -1151,7 +1151,7 @@ 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 + Sidik jari pada alamat server tidak cocok dengan sertifikat. Gagal mengatur alamat Gagal hapus profil pengguna Hapus antrian @@ -1160,7 +1160,7 @@ Gagal perbarui privasi pengguna Fungsi lambat Notifikasi instan - izinkan SimpleX berjalan di latar belakang pada dialog berikutnya. Jika tidak, notifikasi akan dimatikan.]]> + Izinkan 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! @@ -1193,7 +1193,7 @@ 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 + Server perlu otorisasi untuk membuat antrean, periksa kata sandi. Gagal menghapus permintaan kontak Gagal menghapus koneksi kontak tertunda Gagal mengubah alamat @@ -1262,10 +1262,10 @@ 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 + Server perlu otorisasi untuk mengunggah, periksa kata sandi. Buat antrian Dapat dimatikan melalui pengaturan – notifikasi akan tetap ditampilkan saat aplikasi berjalan.]]> - layanan latar belakang SimpleX – yang gunakan beberapa persen baterai per hari.]]> + SimpleX berjalan di latar belakang 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 @@ -1339,12 +1339,1174 @@ Ketentuan diterima pada: %s. Ketentuan akan diterima pada: %s. Koneksi - Rol - Ganti rol + Peran + Ganti peran 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 - \ No newline at end of file + 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 %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 + permintaan koneksi + Koneksi ke desktop dalam kondisi buruk + Konfirmasi hapus kontak? + Waktu kustom + Pilih berkas + terhubung ke pengembang SimpleX Chat untuk mengajukan pertanyaan dan menerima pembaruan.]]> + Hapus obrolan? + Jangan gunakan kredensial dengan proxy. + Gunakan host .onion ke Tidak jika proxy SOCKS tidak mendukung.]]> + Harap diperhatikan: 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 peran %s menjadi %s + Dihapus pada: %s + Hapus profil + %1$s!]]> + 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! + (perangkat ini v%s)]]> + Perangkat + Koneksi terputus + Gagal salin + Temukan melalui jaringan lokal + Kode undangan desktop salah + Desktop terputus + %1$s.]]> + 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) + %s.]]> + 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 + Terbaik untuk baterai. Anda akan menerima notifikasi saat aplikasi sedang berjalan (TANPA layanan latar belakang).]]> + Baik untuk baterai. 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… + Harap diperhatikan: 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 + Perangkat Xiaomi: 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 peran grup? + %s.]]> + Ketentuan Penggunaan + %s.]]> + %s, terima ketentuan penggunaan.]]> + Menambahkan server pesan + Hapus profil obrolan + Obrolan bisnis + Terputus dikarenakan: %s + %s dengan alasan: %s]]> + Desktop sedang sibuk + grup dihapus + kontak %1$s diubah menjadi %2$s + Panggilan dilarang! + %s.]]> + %s.]]> + Teks ketentuan saat ini tidak dapat dimuat, Anda dapat meninjau ketentuan via tautan ini: + Pesan pribadi antar anggota dilarang. + Pindah dari perangkat lain 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 + Tambahkan kontak: 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. + %s 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 + Pindai kode QR.]]> + Buka di aplikasi seluler, lalu ketuk Hubungkan di aplikasi.]]> + Hapus + Hapus + batal pratinjau tautan + Tombol tutup + Email + pindai kode QR dalam panggilan video, atau kontak Anda dapat bagikan tautan undangan.]]> + Panduan Pengguna.]]> + 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 peran 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 + tunjukkan kode QR dalam panggilan video, atau bagikan tautan.]]> + Jangan buat alamat + Rincian + Koneksi + Terhubung ke ponsel + Jangan aktifkan + Gunakan dari desktop 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. + hanya dengan satu kontak - 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. + Buka di ponsel.]]> + Pengaturan alamat + Tambahkan anggota tim Anda ke percakapan. + Alamat bisnis + dengan enkripsi end-to-end, 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. + repositori GitHub kami.]]> + Nilai nanti + Perbarui + Rincian tautan unduhan + untuk setiap profil obrolan yang Anda miliki di aplikasi.]]> + untuk setiap kontak dan anggota grup.\nHarap diperhatikan: 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 + Mengonsumsi banyak baterai! 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. + %s.]]> + tidak 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. + Harap diperhatikan: menggunakan database yang sama pada dua perangkat akan merusak dekripsi pesan dari koneksi Anda, sebagai perlindungan keamanan.]]> + %1$s.]]> + mengubah alamat… + Hapus tautan? + Hapus tautan + Putuskan + Server terhubung + Hapus pesan anggota? + Tanggal pesan lebih baik. + (baru)]]> + %1$s.]]> + 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 peran + 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 + Peran 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 peran + 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 alamat SimpleX + 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 peran Anda menjadi %s + Anda mengubah peran %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. + Peran 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 + Peran 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 adalah pengamat + 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 + Peran 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 + Tautan koneksi tidak didukung + Tautan singkat + gagal mengirim pesan + tinjau + sedang ditinjau + Penerimaan anggota + Mengobrol dengan anggota + Tidak ada obrolan dengan anggota + Terima sebagai anggota + Terima sebagai pengamat + Ada kesalahan saat menerima anggota + 1 obrolan dengan anggota + %d obrolan + %d mengobrol dengan anggota + %d pesan + Anda dapat meninjau laporan anda di Obrolan dengan pengurus. + grup telah dihapus + Silakan tunggu pengurus grup untuk meninjau permintaanmu untuk bergabung ke grup + ditinjau oleh pengurus + Tingkatkan tautan + menerimamu + diterima %1$s + Atur penerimaan anggota + Tinjau anggota + Mengobrol dengan pengurus + Hapus obrolan + Hapus obrolan dengan anggota? + luring + Tolak + Tinjau anggota sebelum menerima (mengetuk) + Mengobrol dengan pengurus + semua + Gagal hapus obrolan + Laporan telah dikirim ke pengurus + Anda tidak dapat mengirim pesan! + kontak telah dihapus + kontak tidak siap + kontak telah dinonaktifkan + tidak diselaraskan + permintaan bergabung ditolak + anggota menggunakan versi lama + dihapus dari grup + Anda keluar + Simpan pengaturan penerimaan? + Anggota baru ingin bergabung ke grup. + kamu menerima anggota ini + Mengobrol dengan anggota + Terima + Terima anggota + Anggota akan bergabung ke grup, terima? + Tolak anggota? + 4 bahasa antarmuka baru + Terima permintaan kontak + Terima permintaan kontak + Tambahkan pesan + Izinkan berkas dan media jika kontak Anda mengizinkan. + Izinkan kontak Anda mengirim berkas dan media. + Bio: + Bio terlalu panjang + Bot + Anda dan kontak Anda dapat mengirim berkas dan media. + Koneksi bisnis + Tak dapat ubah profil + Bahasa Katalan, Indonesia, Rumania, dan Vietnam - terima kasih kepada pengguna kami! + enkripsi end-to-end.]]> + hanya setelah permintaan Anda diterima.]]> + Obrolan dengan admin + Chat dengan anggota sebelum mereka bergabung. + Hubungkan + Terhubung lebih cepat! 🚀 + PERMINTAAN KONTAK DARI GRUP + kontak harus menerima… + Buat alamat Anda + Opsi tidak berlaku + Deskripsi terlalu panjang + Aktifkan pesan menghilang secara default. + Gagal mengubah profil + Gagal membuka obrolan + Gagal membuka grup + Gagal menolak permintaan kontak + Berkas dan media dilarang dalam obrolan ini. + Grup + Gabung grup + Membersihkan obrolan Anda + Lalu lintas jaringan seluler lebih sedikit. + Memuat profil… + Anggota dihapus - tidak dapat menerima permintaan + Kirim pesan instan setelah tekan Hubungkan. + Peran grup baru: Moderator + Tidak ada sesi routing pribadi + Hanya Anda yang dapat kirim berkas dan media. + Hanya kontak Anda yang dapat kirim berkas dan media. + Buka obrolan + Buka tautan bersih + Buka tautan + Buka obrolan baru + Buka grup baru + Buka untuk terima + Buka untuk hubungkan + Buka untuk gabung + Buka untuk gunakan bot + Routing pribadi terlalu lama + Larang mengirim berkas dan media. + Latar belakang protokol terlalu lama + Tolak permintaan kontak + Hapus tautan pelacak + Menghapus pesan dan memblokir anggota. + permintaan koneksi dari grup %1$s + permintaan dikirim + Tinjau anggota grup + Kirim permintaan kontak? + Kirim permintaan + Kirim permintaan tanpa pesan + Kirimkan masukan pribadi Anda ke grup. + Dikirim ke kontak Anda setelah terhubung. + Pasang bio profil dan pesan sambutan. + Bagikan alamat lama + Bagikan tautan lama + Bagikan alamat Anda + Deskripsi singkat: + Alamat singkat SimpleX + Tautan relay SimpleX + Sambut kontak Anda 👋 + Bio Anda: + Kontak bisnis Anda + Kontak Anda + Grup Anda + Profil Anda + Ketuk Hubungkan untuk chat + Tekan Hubungkan untuk kirim permintaan + Tekan Hubungkan untuk gunakan bot + Tekan Gabung grup + Batas waktu koneksi TCP bg + Alamat akan singkat, dan profil Anda akan dibagikan melalui alamat tersebut. + Tautan akan singkat, dan profil grup akan dibagikan melalui tautan tersebut. + Pengirim TIDAK akan diberi tahu. + Pengaturan ini untuk profil Anda saat ini + Waktu menghilang hanya diatur untuk kontak baru. + Untuk mengirim perintah, Anda harus terhubung. + Untuk menggunakan profil lain setelah mencoba hubungkan, hapus chat dan gunakan tautan lagi. + Perbarui alamat Anda + Tingkatkan + Tingkatkan alamat? + Tingkatkan tautan grup + Tingkatkan tautan grup? + Gunakan profil samaran + Pesan sambutan + Gagal menandai dibaca + Sidik jari di alamat server tujuan tidak cocok dengan sertifikat: %1$s. + Sidik jari pada alamat server penerusan tidak cocok dengan sertifikat: %1$s. + Sidik jari di alamat server tidak cocok dengan sertifikat: %1$s. + tidak berlangganan + Anda tidak terhubung ke server yang digunakan untuk menerima pesan dari koneksi ini (tidak berlangganan). + 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 ffdc377ceb..1c7e39d51e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -18,8 +18,8 @@ connesso errore in connessione - Sei connesso al server usato per ricevere messaggi da questo contatto. - Tentativo di connessione al server usato per ricevere messaggi da questo contatto. + Sei connesso/a al server usato per ricevere messaggi da questa connessione. + Tentativo di connessione al server usato per ricevere messaggi da questa connessione. eliminato contrassegnato eliminato l\'invio di file non è ancora supportato @@ -71,9 +71,9 @@ Errore di eliminazione del gruppo Errore di eliminazione della richiesta di contatto Errore di eliminazione della connessione del contatto in attesa - Errore di modifica dell\'indirizzo + Errore cambiando l\'indirizzo Test fallito al passo %s. - Il server richiede l\'autorizzazione di creare code, controlla la password + Il server richiede l\'autorizzazione di creare code, controlla la password. Connetti Crea coda Coda sicura @@ -146,8 +146,8 @@ Benvenuto/a! Questo testo è disponibile nelle impostazioni Сhat - sei stato invitato in un gruppo - entra come %s + Sei stato/a invitato/a in un gruppo + Entra come %s in connessione… Tocca per iniziare una conversazione Scrivi agli sviluppatori @@ -201,7 +201,7 @@ Ripristina OK Connettere via indirizzo del contatto? - Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %1$s). + Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %1$s). Ti connetterai a tutti i membri del gruppo. connessione %1$d Descrizione @@ -210,7 +210,7 @@ Sei già connesso a %1$s. 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 + L\'impronta digitale nell\'indirizzo del server non corrisponde al certificato. SimpleX funziona in secondo piano invece di usare le notifiche push.]]> Consentilo nella prossima schermata per ricevere le notifiche immediatamente.]]> Servizio SimpleX Chat @@ -266,7 +266,7 @@ Nome completo del gruppo: scansionare il codice QR nella videochiamata, 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 @@ -274,7 +274,7 @@ Consuma più batteria! 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 cambio indirizzo… @@ -285,7 +285,7 @@ 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 @@ -399,7 +399,7 @@ AIUTO Chat fermata Errore del database - La password del database è diversa da quella salvata nel Keystore. + La password del database è diversa da quella salvata nell\'archivio chiavi. Database crittografato Inserisci la password giusta. Inserisci la password… @@ -497,7 +497,7 @@ 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. Nota bene: NON potrai recuperare o cambiare la password se la perdi.]]> Cambiare password del database\? Conferma nuova password… @@ -505,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 @@ -513,7 +513,7 @@ Attivare l\'eliminazione automatica dei messaggi\? Crittografare il database\? Crittografare - Errore nella modifica dell\'impostazione + Errore cambiando l\'impostazione Errore nella crittografia del database Le tue impostazioni Verrai connesso/a al gruppo quando il dispositivo dell\'host del gruppo sarà in linea, attendi o controlla più tardi! @@ -729,7 +729,7 @@ 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 @@ -871,8 +871,8 @@ 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 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. @@ -940,7 +940,7 @@ Errore nell\'aggiornamento del link del gruppo osservatore Contatta l\'amministratore del gruppo. - Non puoi inviare messaggi! + sei un osservatore Sistema Aggiungi messaggio di benvenuto Messaggio di benvenuto @@ -1015,7 +1015,7 @@ In attesa del video Errore nel caricamento dei server XFTP Errore nel salvataggio dei server XFTP - Il server richiede l\'autorizzazione per l\'invio, controlla la password + Il server richiede l\'autorizzazione per l\'invio, controlla la password. Confronta file Crea file Scarica file @@ -1115,7 +1115,7 @@ Ciao! \nConnettiti a me tramite SimpleX Chat: %s Invita amici - Salva le impostazioni di accettazione automatica + Salva le impostazioni dell\'indirizzo SimpleX Puoi crearlo più tardi Condividi indirizzo Inserisci il messaggio di benvenuto… @@ -1379,8 +1379,8 @@ Apri Errore di creazione del contatto Invia messaggio diretto per connetterti - invia messaggio diretto - si è connesso/a direttamente + invia per connettere + connessione richiesta Espandi Ripetere la richiesta di connessione? contatto eliminato @@ -1569,7 +1569,7 @@ Il desktop è inattivo Il desktop è stato disconnesso Tempo scaduto durante la connessione al desktop - Membro passato %1$s + Membro %1$s L\'esecuzione della funzione impiega troppo tempo: %1$d secondi: %2$s Funzione lenta Mostra chiamate API lente @@ -1586,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 @@ -1947,8 +1947,7 @@ Dimensione carattere Totale inviato Messaggio inoltrato - Inizio da %s. -\nTutti i dati sono privati, nel tuo dispositivo. + 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 @@ -2015,7 +2014,7 @@ connetti Contatto eliminato! Confermare l\'eliminazione del contatto? - Messaggio + Chatta Nessuna selezione Seleziona Selezionato %d @@ -2064,7 +2063,7 @@ 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. + La tua connessione è stata spostata a %s, ma si è verificato un errore cambiando il profilo. Non usare credenziali con proxy. Assicurati che la configurazione del proxy sia corretta. Autenticazione del proxy @@ -2135,7 +2134,7 @@ Operatori del server Seleziona gli operatori di rete da usare. Continua - Aggiorna + Aggiornamento Leggi più tardi Server preimpostati Condizioni accettate @@ -2178,7 +2177,7 @@ Trasparenza Decentralizzazione della rete Il secondo operatore preimpostato nell\'app! - Attiva Flux + Attiva Flux nelle impostazioni \"Rete e server\" per una migliore privacy dei metadati. Vedi le condizioni aggiornate Sfocatura Server dei messaggi aggiunti @@ -2248,5 +2247,298 @@ 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 - \ No newline at end of file + 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 + Tutti i server + Off + Server preimpostati + Usa la porta TCP 443 solo per i server preimpostati. + 1 chat con un membro + %d chat + %d chat con membri + %d messaggi + Salvare le impostazioni di ammissione? + ha accettato %1$s + ti ha accettato/a + Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo. + hai accettato questo membro + revisiona + Ammissione dei membri + Nessuna chat con membri + off + Revisiona i membri + Revisiona i membri prima di ammetterli (bussare). + Accetta + Chat con amministratori + Rimuovi + Il membro entrerà nel gruppo, accettarlo? + revisionato dagli amministratori + Accetta membro + Chatta con gli amministratori + Accetta come osservatore + Un nuovo membro vuole entrare nel gruppo. + tutti + Chatta con il membro + Chat con membri + Errore di accettazione del membro + Accetta come membro + Imposta l\'ammissione dei membri + Segnalazione inviata ai moderatori + in attesa di revisione + Puoi vedere le tue segnalazioni nella chat con gli amministratori. + Non puoi inviare messaggi! + contatto non pronto + contatto eliminato + contatto disattivato + non sincronizzato + richiesta di entrare rifiutata + impossibile inviare messaggi + il gruppo è eliminato + il membro ha una versione vecchia + rimosso dal gruppo + sei uscito/a + Eliminare la chat con il membro? + Rifiutare il membro? + Elimina chat + Errore di eliminazione della chat + Aggiorna l\'indirizzo + Accetta la richiesta di contatto + Aggiungi un messaggio + crittografia end-to-end.]]> + solo dopo che la tua richiesta verrà accettata.]]> + Connetti + il contatto dovrebbe accettare… + Errore cambiando l\'profilo + Errore di apertura della chat + Errore di apertura del gruppo + Errore nel rifiuto della richiesta di contatto + Entra nel gruppo + Apri la chat + Apri una chat nuova + Apri un gruppo nuovo + Apri per accettare + Apri per connettere + Apri per entrare + L\'indirizzo sarà breve e il tuo profilo verrà condiviso attraverso l\'indirizzo. + Rifiuta la richiesta di contatto + richiesta inviata + Inviare una richiesta di contatto? + Invia richiesta + Invia richiesta senza messaggio + Inviato al tuo contatto dopo la connessione. + Aggiornare il link del gruppo? + Aggiorna + Aggiornare l\'indirizzo? + Il mittente NON verrà avvisato. + Messaggio di benvenuto + Il tuo profilo + Impossibile cambiare profilo + Per usare un altro profilo dopo il tentativo di connessione, elimina la chat e usa di nuovo il link. + Chatta con gli amministratori + Chatta con i membri prima che si uniscano. + Connettiti più velocemente! 🚀 + Meno traffico sulle reti mobili. + Parla immediatamente appena tocchi Connetti. + Nuovo ruolo nei gruppi: Moderatore + Nessuna sessione di instradamento privato + Scadenza dell\'instradamento privato + Scadenza del protocollo in sec. piano + Rimuove i messaggi e blocca i membri. + Revisiona i membri del gruppo + Invia i tuoi commenti privati ai gruppi. + Scadenza conness. TCP in sec. piano + Caricamento del profilo… + Bio: + Descrizione breve: + La tua bio: + Bio troppo lunga + Descrizione troppo lunga + Accetta la richiesta di contatto + Connessione lavorativa + Gruppo + Tocca Connetti per chattare + Tocca Connetti per inviare la richiesta + Tocca Entra nel gruppo + Il tuo contatto lavorativo + Il tuo contatto + Il tuo gruppo + Il tempo di scomparsa è impostato solo per i contatti nuovi. + 4 nuove lingue + Catalano, indonesiano, rumeno e vietnamita - grazie ai nostri utenti! + Crea il tuo indirizzo + Attiva i messaggi a tempo in modo predefinito. + Mantieni le chat pulite + Imposta la bio del profilo e il messaggio di benvenuto. + Condividi il tuo indirizzo + Indirizzo breve di SimpleX + Aggiorna il tuo indirizzo + Usa profilo in incognito + Dai il benvenuto ai tuoi contatti 👋 + Condividi l\'indirizzo vecchio + Condividi il link vecchio + Il link sarà breve e il profilo del gruppo verrà condiviso attraverso il link. + Aggiorna il link del gruppo + RICHIESTE DI CONTATTO DAI GRUPPI + Il membro è eliminato - impossibile accettare la richiesta + connessione richiesta dal gruppo %1$s + Questa impostazione è per il tuo profilo attuale + Consenti file e contenuti multimediali solo se il tuo contatto li consente. + Consenti ai tuoi contatti di inviare file e contenuti multimediali. + Bot + Sia tu che il tuo contatto potete inviare file e contenuti multimediali. + File e contenuti multimediali sono vietati in questa chat. + Solo tu puoi inviare file e contenuti multimediali. + Solo il tuo contatto può inviare file e contenuti multimediali. + Apri per usare il bot + Proibisci l\'invio di file e contenuti multimediali. + Tocca Connetti per usare il bot + Per inviare comandi devi essere connesso/a. + Opzioni deprecate + Apri link pulito + Apri link completo + Rimuovi il tracciamento del link + Link del relay SimpleX + Errore nel segnare la lettura + L\'impronta digitale nell\'indirizzo del server di destinazione non corrisponde al certificato: %1$s. + L\'impronta digitale nell\'indirizzo del server di inoltro non corrisponde al certificato: %1$s. + L\'impronta digitale nell\'indirizzo del server non corrisponde al certificato: %1$s. + nessuna iscrizione + Non sei connesso/a al server usato per ricevere messaggi da questa connessione (nessuna iscrizione). + 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 2bb007b6e8..6b413c9bfa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -371,7 +371,7 @@ מופעל מופעל עבור איש הקשר מופעל עבורך - הודעות נעלמות אסורות בקבוצה זו. + הודעות נעלמות אסורות. %d דקה %d שנ׳ הודעות נעלמות @@ -489,10 +489,10 @@ הקבוצה תימחק עבורך – לא ניתן לבטל זאת! הסתר העדפות קבוצה - חברי קבוצה יכולים למחוק הודעות שנשלחו באופן בלתי הפיך. (24 שעות) - חברי הקבוצה יכולים לשלוח הודעות נעלמות. - חברי הקבוצה יכולים לשלוח הודעות ישירות. - חברי הקבוצה יכולים לשלוח הודעות קוליות. + משתמשים יכולים למחוק הודעות שנשלחו באופן בלתי הפיך. (24 שעות) + משתמשים יכולים לשלוח הודעות נעלמות. + משתמשים יכולים לשלוח הודעות ישירות. + יכולים לשלוח הודעות קוליות. אפשר השמדה עצמית אם תבחרו לדחות השולח לא יקבל התראה על כך. אם תאשרו, שרתי העברת ההודעות יוכלו לראות את ה־IP שלכם, וספק האינטרנט שלכם – את השרתים אליהם אתם מחוברים. @@ -505,7 +505,7 @@ התעלם מיד ייבא מסד נתונים - חסין מפני ספאם ושימוש לרעה + חסין מפני ספאם לייבא מסד נתונים של צ׳אט\? תמונה נשלחה התמונה תתקבל כאשר איש הקשר יסיים להעלות אותה. @@ -521,10 +521,10 @@ זהות נסתרת באמצעות קישור קבוצה זהות נסתרת באמצעות קישור חד־פעמי קישור חיבור לא תקין - אפשרו ל-SimpleX לפעול ברקע בתיבת הדו-שיח הבאה. אחרת, ההתראות יושבתו.]]> + אפשר זאת בתיבת הדו-שיח הבאה כדי לקבל התראות על הודעות חדשות באופן מיידי.]]> התראות מיידיות מושבתות! הזמן חברי קבוצה - הוזמן + הזמין את עקיף (%1$s) מצב זהות נסתרת מגן על הפרטיות שלך על ידי שימוש בפרופיל אקראי חדש עבור כל איש קשר. גרסת מסד נתונים לא תואמת @@ -556,14 +556,11 @@ הזמן לקבוצה הזמן חברי קבוצה מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה. - מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו. + מחיקת הודעות בלתי הפיכה אסורה. להצטרף בתור %s זה מאפשר חיבורים אנונימיים רבים ללא שום נתונים משותפים ביניהם בפרופיל צ׳אט יחיד. - זה יכול לקרות כאשר: -\n1. פג תוקפן של ההודעות בלקוח השולח לאחר 2 ימים או בשרת לאחר 30 ימים. -\n2. פיענוח הצפנת הודעה נכשל, מכיוון שאתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים. -\n3. החיבור נפגע. - ניתן לשנות זאת מאוחר יותר באמצעות ההגדרות. + זה יכול לקרות כאשר:\n1. פג תוקפן של ההודעות בלקוח השולח לאחר 2 ימים או בשרת לאחר 30 ימים.\n2. פיענוח הצפנת הודעה נכשל, מכיוון שאתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים.\n3. החיבור נפגע. + איך זה משפיע על הסוללה זה יכול לקרות כאשר אתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים. להצטרף לקבוצה\? הצטרף @@ -608,7 +605,7 @@ קישור הזמנה חד־פעמי מרקדאון בהודעות רשת ושרתים - הגדרות רשת + הגדרות מתקדמות ארכיון מסד נתונים חדש הודעות חבר קבוצה @@ -690,7 +687,7 @@ תגובות אמוג׳י להודעות אסורות בקבוצה זו. אפשר לאנשי הקשר להוסיף תגובות אמוג׳י להודעות. אפשר תגובות אמוג׳י להודעות רק אם איש הקשר מאפשר אותן. - חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות. + משתמשים יכולים להוסיף תגובות אמוג׳י להודעות. רק אתם יכולים להוסיף תגובות אמוג׳י להודעות. רק איש הקשר שלכם יכול להוסיף תגובות אמוג׳י להודעות. פתח @@ -737,7 +734,7 @@ שרת מוגדר מראש פרטיות מוגדרת מחדש אנשים יכולים להתחבר אליכם רק דרך הקישורים שאתם משתפים. - פרוטוקול וקוד פתוחים – כל אחד יכול להריץ את השרתים. + כל אחד יכול לארח שרתים. תקופתי נא להזין את הסיסמה הקודמת לאחר שחזור גיבוי מסד הנתונים, לא ניתן לבטל פעולה זו. לאסור מחיקה בלתי הפיכה של הודעות. @@ -746,7 +743,7 @@ אנא בידקו את חיבור האינטרנט שלכם עם %1$s ונסו שוב. ייתכן שטביעת האצבע של התעודה בכתובת השרת שגויה פתיחת מסוף צ׳אט - פתיחת פרופילי צ׳אט + שנה פרופילי צ׳אט ממתין כתובת שרת מוגדר מראש סיסמה להצגה @@ -1016,7 +1013,7 @@ יותר מדי תמונות! תודה שהתקנתם את SimpleX Chat! קישור זה אינו קישור חיבור תקין! - צבעי ערכת נושא + צבעי ממשק התפקיד ישתנה ל־"%s". החבר יקבל הזמנה חדשה. השרתים לחיבורים חדשים של פרופיל הצ׳אט הנוכחי שלך הפלטפורמה הראשונה ללא כל מזהי משתמש - פרטית בעיצובה. @@ -1054,7 +1051,7 @@ שדרג ופתח צ׳אט כדי להגן על אזור הזמן, קובצי תמונה/קול משתמשים ב־UTC. העלה קובץ - שירות רקע SimpleX – הוא משתמש בכמה אחוזים מהסוללה ביום.]]> + SimpleX רץ ברקע במקום להשתמש בpush notifications.]]> כדי לקבל התראות, יש להזין את סיסמת מסד הנתונים בטל נעילה שליחה לא מורשית @@ -1066,7 +1063,7 @@ כדי לאמת הצפנה מקצה־לקצה עם איש הקשר שלכם, יש להשוות (או לסרוק) את הקוד במכשירים שלכם. פרופילי צ׳אט לעדכן מצב בידוד תעבורה\? - מנסה להתחבר לשרת המשמש לקבלת הודעות מאיש קשר זה (שגיאה: %1$s). + מנסה להתחבר לשרת המשמש לקבלת הודעות מאיש קשר זה (שגיאה: %1$s). פורמט הודעה לא ידוע דרך הדפדפן בטל השתקה @@ -1088,7 +1085,7 @@ בטל השתקה ממתין לאישור… ממתין למענה… - איננו מאחסנים אף אחד מאנשי הקשר או ההודעות שלך (לאחר המסירה) בשרתים. + איננו מאחסנים את אנשי הקשר או ההודעות שלך (לאחר המסירה) בשרתים. SimpleX אתם באמצעות קישור לכתובת איש קשר @@ -1189,8 +1186,7 @@ אנשי הקשר שלך יכולים לאפשר מחיקת הודעות מלאה. הצ׳אטים איש הקשר שלך שלח קובץ גדול יותר מהגודל המרבי הנתמך כעת (%1$s). - איש הקשר שלך צריך להיות מקוון כדי שהחיבור יושלם. -\nניתן לבטל חיבור זה ולהסיר את איש הקשר (ולנסות מאוחר יותר עם קישור חדש). + איש הקשר שלך צריך להיות מקוון כדי שהחיבור יושלם.\nניתן לבטל חיבור זה ולהסיר את איש הקשר (ולנסות מאוחר יותר עם קישור חדש). פרופיל הצ׳אט שלך יישלח \nלאיש הקשר שלך מסד הנתונים שלך @@ -1222,8 +1218,7 @@ לא תאבדו את אנשי הקשר שלכם אם תמחקו מאוחר יותר את הכתובת שלכם. כתובת SimpleX שלך שרתי SMP שלך - הפרופיל שלך מאוחסן במכשירך ומשותף רק עם אנשי הקשר שלך. -\nשרתי SimpleX אינם יכולים לראות את הפרופיל שלך. + הפרופיל שלך מאוחסן במכשירך ומשותף רק עם אנשי הקשר שלך.\nשרתי SimpleX אינם יכולים לראות את הפרופיל שלך. הפרופיל, אנשי הקשר וההודעות שנמסרו מאוחסנים במכשיר שלך. אתם תפסיקו לקבל הודעות מקבוצה זו. היסטוריית הצ׳אט תישמר. הינך מנסה להזמין איש קשר איתו שיתפת פרופיל זהות נסתרת לקבוצה בה ישנו שימוש בפרופיל הראשי שלך @@ -1239,15 +1234,15 @@ כיבוי אפליקציה אפשר לשלוח קבצים ומדיה. - מועדף + הוסף למועדפים קבצים ומדיה אין צ\'אטים מסוננים לכבות\? קבצים ומדיה אסורים! - קבצים ומדיה אסורים בקבוצה זו. - חברי הקבוצה יכולים לשלוח קבצים ומדיה. + קבצים ומדיה אסורים. + משתמשים יכולים לשלוח קבצים ומדיה. איתחול - שנוא + הסר מהמועדפים כבוי קו חוצה לאסור שליחת קבצים ומדיה. @@ -1360,8 +1355,7 @@ אפליקציה חדשה למחשב השולחני! 6 שפות ממשק חדשות האפליקציה מצפינה קבצים מקומיים חדשים (למעט סרטונים). - ביטוי סיסמה אקראי מאוחסן בהגדרות כטקסט רגיל. -\nאתה יכול לשנות את זה מאוחר יותר. + ביטוי סיסמה אקראי מאוחסן בהגדרות כטקסט רגיל.\nאתה יכול לשנות את זה מאוחר יותר. שלח הודעה ישירה כדי להתחבר גלה והצטרף לקבוצות ביטוי הסיסמה להצפנת מסד הנתונים יעודכן ויישמר בהגדרות. @@ -1528,8 +1522,8 @@ הצג קריאות API איטיות אפשרויות למפתח צור פרופיל - ו %d שאר האירועים - הגדר כתובת איש קשר חדש + בנוסף ל- %d אירועים אחרים + איש הקשר הגדיר כתובת חדשה לחץ לחיבור דפדפן האינטרנט המוגדר כברירת מחדל נדרש לשיחות. אנא הגדר דפדפן ברירת מחדל במערכת, ושתף מידע נוסף עם המפתחים. השיחה הזו מוגנת באמצעות הצפנה קצה-אל-קצה. @@ -1609,7 +1603,7 @@ החיבור עצר נתיב קובץ לא חוקי שיתפת נתיב קובץ לא חוקי. דווח על הבעיה למפתחי האפליקציה. - %1$d הודעות שנערכו על ידי %2$s + %1$d הודעות נחסמו על ידי %2$s %d הודעות סומנו כנמחקות האם לחזור על בקשת החיבור? חסום @@ -1632,13 +1626,13 @@ טעינה של הקובץ שימוש ממחשב שולחני חסומים %s - מחק איש קשר + איש קשר נמחק %d אירועי קבוצה %s, %s ו-%d חברים איש הקשר %1$s השתנה ל-%2$s כתובת איש קשר הוסרה תמונת פרופיל הוסרה - הגדר תמונת פרופיל חדשה + הגדיר תמונת פרופיל חדשה עדכן פרופיל מצב לא ידוע נוצר ב @@ -1703,7 +1697,7 @@ מתי שהIP מוסתר השתמש במסלול פרטי עם שרתים לא ידועים אזהרה על אופן שליחת ההודעה - הראה סטטוס הודעה + הצג מצב הודעה מסלול פרטי להודעה 🚀 סטטוס הודעה:%s אנא וודא שהמכשיר והמחשב מחוברים לאותה רשת מקומית, ושהפיירוול של המחשב מאפשר את החיבור. @@ -1722,7 +1716,7 @@ מרובע, עיגול, או כל דבר ביניהם העבר ושמור הודעות מתי שמתחבר שחיות קוליות ווידאו. - לא מצליח לשלוח הודעה + לא ניתן לשלוח הודעה הודעות קוליות לא מאופשרות שגיאת קובץ זמני קבצים ומדיה לא מאופשרים @@ -1742,13 +1736,13 @@ אוזניות ערכת נושא לפרופיל צבעי הצא\'ט - הראה רשימת צא\'טים בחלון חדש + הצג רשימת שיחות בחלון חדש מצב הקובץ סטטוס הודעה מצב הקובץ:%s ריק כהה - מצב צבעוני + ערכת נושא שחור בהיר אפס צבע @@ -1766,9 +1760,9 @@ הגדרות מתקדמות הגדר ערכת נושא ברירת מחדל אפס ערכת נושא למשתמש - החל ל - מצב כל הצבעים - חברי הקבוצה יכולים לשלוח קישורי SimpleX + החל על + ערכת נושא + משתמשים יכולים לשלוח קישורי SimpleXצ עשה שהצאט\'ים שלך יראו אחרת! הגדרות רשת הקישור הזה שומש כבר במכשיר אחר, אנא צור קישור חדש במחשב. @@ -1785,7 +1779,7 @@ ערכת נושא צבעי מצב כהה שגיאה בשרת היעד:%1$s - "אל תשלח הודעות ישירות אפילו אם שרת היעד לא תומך במסלול פרטי" + אל תשלח הודעות ישירות אפילו אם שרת היעד לא תומך במסלול פרטי שיפור בשליחת הודעות מצלמה אפשר שליחת קישורי SimpleX @@ -1828,7 +1822,7 @@ אין עדיין חיבור ישיר, ההודעה תעובר ע"י מנהל. חבר לא פעיל שרתי XFTP אחרים - הראה אחוזים + הצג אחוזים מושבת יציבה הותקן בהצלחה @@ -1918,4 +1912,230 @@ שרת XFTP חלש אנשי קשר בארכיון - \ No newline at end of file + דיווח בארכיון + הפרה של הנחיות קהילתיות + %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.]]> + צ\'אט + צא\'טים עסקיים + "הזבל נמחק" + אנשי קשר + איש הקשר יימחק - לא ניתן לבטל זאת! + איש הקשר נמחק. + ספאם + ניתן לשנות זאת בהגדרות המראה. + אין שרתי הודעות. + חיבור נחסם + ממתין לאישור + ממתין + סרגל כלים נגיש לצ\'אט + שימוש בפורט אינטרנט + לשליחה + צ\'אט אחד עם חבר + הודעה חדשה + הגדרת מפעילי שרת + ניתן להגדיר שרתים דרך הגדרות. + אישר אותך + ממתין לסקירה + אשר + אישור חבר + הוסף קישור קצר + קצוות + צורת ההודעה + סקירה + ממתין + אין צ\'אטים ברשימה %s. + שמור רשימה + גרסת השרת אינה תואמת לאפליקציה שלך: %1$s. + סרגלי כלים נגישים לאפליקציות + כל + דחיה + אשר כחבר + לדחות את החבר? + החבר יצטרף לקבוצה, לקבל אותו? + קבל כצופה + שימוש באפליקציה ביד אחת. + החל מ-%s.\nכל הנתונים נשמרים פרטיים במכשיר שלך. + כל השרתים + צ\'אטים פרטיים, קבוצות ואנשי הקשר שלך אינם נגישים למפעילי השרת. + לדוגמה, אם איש הקשר שלך מקבל הודעות דרך שרת SimpleX Chat, האפליקציה שלך תעביר אותן דרך שרת Flux. + מדיניות פרטיות ותנאי שימוש. + ניתן להגדיר מפעילים בהגדרות רשת ושרתים. + לא + זנב לבועה + סרגל כלים נגיש לצ\'אט + %1$s אושר + חבר חדש רוצה להצטרף לקבוצה. + אנא המתן עד שמנהלי הקבוצה יבדקו את בקשתך להצטרף לקבוצה. + שימוש בהודעות + צורת הודעה הניתנת להתאמה אישית. + שמור וחבר מחדש + שקיפות + אנא בדוק שקישור SimpleX תקין. + פתח שיחה + פתח שיחה חדשה + פתח קבוצה חדשה + שלח את המשוב הפרטי שלך לקבוצות. + 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 ff6b4e456c..cc85e49a8c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -111,8 +111,8 @@ チャット読み込みに失敗 チャット読み込みに失敗 アプリを更新し、開発者にご連絡ください。 - 即時通知! - 即時通知が無効になってます! + 通知の常時受信! + 通知の常時受信が無効になってます! パスフレーズが必要 プライベート 連絡先の名前 @@ -150,7 +150,7 @@ グループのプロフィールはサーバではなく、メンバーの端末に保存されます。 グループのプロフィールが更新されました。 連絡先とメッセージ内容をプライベートにする。 - 即時通知 + 通知の常時受信 SMPサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 SimpleX の仕様 @@ -480,7 +480,7 @@ 新しい繋がりのリクエスト コピー メッセージを削除しますか? - 編集する + 編集 プライベートにする メッセージが削除されます (※元に戻せません※)! メッセージが削除対象となります。宛先にはメッセージの解読ができます。 @@ -529,8 +529,8 @@ 応答 分散型 スパム耐性 - 即時 - 定期的 + 常時受信 + 定期的に受信 通話は既に終了してます! エンドツーエンド暗号化済みの音声通話 無視 @@ -740,7 +740,7 @@ 接続中… 次世代のプライベートメッセンジャー ビデオ通話 - アプリが稼働中に + アプリがアクティブ時のみ WebRTC ICEサーバ あなたのICEサーバ 設定 @@ -887,7 +887,7 @@ あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。 あなたのチャットプロフィールが他のグループメンバーに公開されます。 エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。 - このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: %1$s)。 + このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: %1$s)。 使用済みリンク、または連絡先による接続の削除ではなければ、バッグの可能性があります。開発者にお伝えください。 \n繋がるには、連絡先に新しくリンクを発行してもらって、電波が安定かどうかご確認ください。 接続を完了するには、連絡相手がオンラインになる必要があります。 @@ -1267,7 +1267,7 @@ KB あたりのプロトコル タイムアウト グループメンバーはファイルやメディアを送信できます。 アドレス変更の中止 - このグループでは、ファイルとメディアは禁止されています。 + ファイルとメディアは禁止されています。 終了しますか? アプリを再起動するまで通知は機能しません。 未読とお気に入りをフィルターします。 @@ -1961,4 +1961,85 @@ 設定 情報がありません、リロードしてください SMPサーバ - \ No newline at end of file + メッセージ + ウェブサイト + ビデオ + 設定画面からサーバを構成できます。 + 改定履歴を開く + 全てのチャットが %s から削除され、リスト自体も削除されます + アプリのツールバー + ログを有効化 + メッセージ受信を10分毎に確認します + バックグラウンドでアプリが常時動作します + SimpleX ChatとFluxは、Fluxが運営するサーバをアプリに組み込むことに合意しました。 + 利用条件をレビュー + プリセットサーバ + 利用条件を承諾 + 透過度 + %s の利用条件が受け入れられます。]]> + 新しいメッセージ + メッセージの形 + リストを保存 + 削除 + リスト名... + 編集 + リストを削除しますか? + 利用条件 + 続ける + リストを作成 + サーバオペレータ + 利用条件を開く + バックグラウンドサービスを使用しません + 利用条件の承諾 + %s の利用条件に承諾しています。]]> + 後で作成する場合はメニューから「SimpleXのアドレスを作成」を選択してください。 + 運営者について + 1年 + 1 件のレポート + モデレーターにメッセージを報告することを許可する + これらのメンバーからの新しいメッセージはすべて非表示になります! + リストに追加 + アドレス設定 + チームのメンバーを会話に追加する + 追加されたメッセージサーバー + アドレスか使い捨てのリンク? + 友達を追加 + チームメンバーを追加 + リストを追加 + すべて + ワンタイムリンクを生成 + %d 件を選択中 + グループのパフォーマンス向上 + 選択 + プライバシーとセキュリティの向上 + 承諾 + プライバシーポリシーと利用条件 + サーバオペレータの設定 + 承諾 + サーバオペレータは、プライベートチャット・グループ・連絡先にはアクセスできません。 + SimpleX Chat を利用することで、以下の事項に同意したものと見なされます:\n- パブリックグループでは合法なコンテンツのみを送信すること。\n- 他のユーザを尊重すること、またスパムメッセージを送信しないこと。 + チャットを開く + 新しいチャットを開始 + 新しいグループを開始 + 無効なリンク + SimpleXのリンクが正しいか確認してください + あなたとモデレーターのみが見ることができます + 送信者とモデレーターのみが見ることができます + アーカイブされたレポート + %sによってアーカイブされたレポート + E2E暗号化.]]> + 接続を要求されました + 招待を受け入れました + SimpleXチャンネルのリンク + スパム + 不適切なコンテンツ + コミュニティガイドライン違反 + 不適切なプロフィール + 他の理由 + サーバーの保存中にエラーが発生しました + メッセージサーバーがありません + メッセージを受信するサーバーがありません + プライベートメッセージルーティング用のサーバーがありません。 + メディアおよびファイルサーバーは存在しません。 + ファイルを送信するサーバーがありません。 + 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 bf07c10a6f..651d32518f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -281,7 +281,7 @@ %d 개의 파일 총 크기 %s 다이렉트 이 채팅에서는 사라지는 메시지를 사용할 수 없습니다. - 이 그룹에서는 사라지는 메시지를 사용할 수 없습니다. + 사라지는 메시지를 사용할 수 없습니다. %d분 %d 개월 %d 분 @@ -966,7 +966,7 @@ 활성 연결 모든 프로필 적용 - 모든 메시지가 삭제됩니다. 이 결정은 취소할 수 없습니다! + 모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 파일 및 미디어 전송을 허용합니다. 새로운 무작위 프로필이 공유됩니다. 모든 색상 모드 @@ -1444,10 +1444,10 @@ %s.]]> %s 이 현재 사용 중]]> %s.]]> - SimpleX 백그라운드 서비스를 제공합니다. - 이 기능은 하루에 몇 퍼센트의 배터리를 소모합니다.]]> + 개인 정보를 보호하기 위해 SimpleX는 푸시 알림을 사용하는 대신 백그라운드에서 실행됩니다.]]> %s 의 서버를 사용하려면 사용 약관에 동의하십시오.]]> %1$s 에 연결 중입니다.]]> - SimpleX의 백그라운드에서 실행되도록 허용하십시오. 그렇지 않으면 알림을 사용할 수 없습니다.]]> + 허용을 선택하면 알림을 즉시 받을 수 있습니다.]]> 앱 설정에서 앱 배터리 사용량 / 제한 없음 을 선택하십시오.]]> %s 이 현재 비활성화됨]]> SimpleX Chat 개발자에게 연결하여 질문하고 업데이트를 받을 수 있습니다.]]> @@ -1457,4 +1457,69 @@ %1$s 그룹에 가입하는 중 입니다.]]> %s 버전이 지원되지 않습니다. 두 기기에서 동일한 버전을 사용하는지 확인하십시오.]]> %1$s 그룹에 속해 있습니다.]]> - \ No newline at end of file + 약관을 수락하는 중 오류 발생 + 연결 보안 + 로그 활성화 + 비즈니스 주소 + 일회용 링크 생성 + 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 fbad2dc4e9..1e71459df9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -1404,7 +1404,7 @@ Jūsų profilis bus išsiųstas kontaktui iš kurio gavote šią nuorodą. Prisijungsite prie visų grupės narių. Esate prisijungę prie serverio skirto gauti žinutes iš šio kontakto. - Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto (klaida: %1$s). + Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto (klaida: %1$s). Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto. nėra detalių SimpleX fono tarnybą - ji naudoja kelis procentus akumuliatoriaus per dieną.]]> @@ -1774,4 +1774,4 @@ %1$d failas (-ai, -ų) vis dar atsisiunčiamas (-i, -a). Nepavyko atsisiųsti %1$d failo (-ų). %d pasirinkta - \ No newline at end of file + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lv/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lv/strings.xml new file mode 100644 index 0000000000..2843b2cf8d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lv/strings.xml @@ -0,0 +1,2503 @@ + + + 1 minūte + 1 mēnesis + tūkst. + Vai izveidot savienojumu, izmantojot vienreizēju saiti? + Pievienoties grupai? + Izmantot pašreizējo profilu + Izmantot jaunu inkognito profilu + Izmantot inkognito profilu + Jūsu profils tiks nosūtīts kontaktpersonai, no kuras saņēmāt šo saiti. + Jūs pievienosities visiem grupas dalībniekiem. + Pievienoties + Atvērt tērzēšanu + Atvērt jaunu tērzēšanu + Atvērt grupu + Atvērt jaunu grupu + Nederīga saite + Lūdzu, pārbaudiet, vai SimpleX saite ir pareiza. + Tiek atvērta datubāze… + Notiek datubāzes migrācija.\nTas var aizņemt dažas minūtes. + Nederīgs faila ceļš + Jūs kopīgojāt nederīgu faila ceļu. Ziņojiet par problēmu lietotnes izstrādātājiem. + Skats pārstāja darboties + savienots + kļūda + notiek savienošana + Jūs esat izveidojis savienojumu ar serveri, kas tiek izmantots ziņojumu saņemšanai no šīs kontaktpersonas. + Mēģina izveidot savienojumu ar serveri, kas tiek izmantots ziņojumu saņemšanai no šīs kontaktpersonas. + izdzēsts + atzīmēts kā izdzēsts + %d ziņojumi atzīmēti kā izdzēsti + moderē %s + %1$d ziņojumus moderē %2$s + To redzat tikai Jūs un moderatori + 1 diena + 1 nedēļa + 1 gads + 30 sekundes + 4 jaunas saskarnes valodas + 5 minūtes + 6 jaunas saskarnes valodas + a + b + Pārtraukt + Atcelt adreses maiņu + Vai atcelt adreses maiņu? + bloķēts + bloķējis administrators + %d ziņojumi bloķēti + Administrators bloķējis %d ziņojumus + failu sūtīšana vēl netiek atbalstīta + failu saņemšana vēl netiek atbalstīta + Jūs + pārsūtīts + saglabāts + saglabāts no %s + nederīga tērzēšana + nederīgi dati + kļūda, rādot ziņojumu + kļūda, rādot saturu + Atšifrēšanas kļūda + Šifrēšanas atkārtotas saskaņošanas kļūda + Šī tērzēšana ir aizsargāta ar pilnīgu šifrēšanu. + Šo tērzēšanu aizsargā kvantu izturīga pilnīga šifrēšana. + Privātās piezīmes + pieprasīts savienojums + pieņemts ielūgums + savienojas… + Jūs kopīgojāt vienreizēju saiti + Pilnā saite + Saites atvēršana pārlūkprogrammā var samazināt savienojuma privātumu un drošību. Neuzticamas SimpleX saites būs sarkanas. + uzaicināts pievienoties + Savienoties, izmantojot kontaktpersonas saiti + Savienoties, izmantojot saiti inkognito režīmā + Ziņot par vienuma redzamību moderatoriem + Ziņot par vienuma arhivēšanu + Ziņot par vienuma arhivēšanu, ko veica + Nezināms ziņojuma formāts + Nederīgs ziņojuma formāts + Tiešraide + Moderēts apraksts + E2ee Info E2ee + E2ee Info No Pq + E2ee Info Pq + Savienojuma lokālais displeja vārds + Displeja vārds Savienojums izveidots + Apraksts Jūs kopīgojāt vienreizējo saiti inkognito režīmā + Apraksts Izmantojot grupas saiti + Apraksts Izmantojot grupas saiti inkognito režīmā + Apraksts Izmantojot kontaktadreses saiti + Apraksts Izmantojot kontaktadreses saiti inkognito režīmā + Apraksts, izmantojot vienreizēju saiti + Apraksts, izmantojot vienreizēju saiti inkognito režīmā + Simplex saites kontakts + Simplex saites uzaicinājums + Simplex saites grupa + Simplex saites kanāls + Simplex saites relejs + Simplex saites savienojums + Simplex saites režīms + Simplex saites režīma apraksts + Simplex Link Mode Pārlūkprogramma + Ziņošanas iemesls: Spams + Ziņošanas iemesls: Nelegāls saturs + Ziņošanas iemesls: Kopienas noteikumu pārkāpums + Ziņošanas iemesls: Profils + Ziņošanas iemesls: Cits + Kļūda, saglabājot Smp serverus + Kļūda, saglabājot Xftp serverus + Pārliecinieties, vai Smp serveru adreses ir pareizā formātā un unikālas + Pārliecinieties, vai Xftp serveru adreses ir pareizā formātā un unikālas + Kļūda ielādējot Smp serverus + Kļūda ielādējot Xftp serverus + Kļūda iestatot tīkla konfigurāciju + Neizdevās parsēt čata nosaukumu + Neizdevās parsēt čatu nosaukumus + Sazinieties ar izstrādātājiem + Neizdevās izveidot lietotāju + Neizdevās izveidot lietotāju (dublikāts) + Neizdevās izveidot lietotāju (dublikāts) + Neizdevās izveidot lietotāju (nepareizs) + Neizdevās izveidot lietotāju, nederīgs apraksts + Neizdevās aktivizēt lietotāju + Neizdevās saglabāt serverus + Nav konfigurēti ziņojumu serveri + Nav konfigurēti ziņojumu serveri saņemšanai + Nav konfigurēti ziņojumu serveri privātai maršrutēšanai + Nav konfigurēti mediju serveri + Nav konfigurēti mediju serveri sūtīšanai + Nav konfigurēti mediju serveri privātai maršrutēšanai + Čata profilam + Kļūdas serveru konfigurācijā + Kļūda, pieņemot operatora nosacījumus + Bloķēšanas iemesls: spams + Bloķēšanas iemesls: saturs + Savienojuma taimauts + Savienojuma kļūda + Tīkla kļūda: nezināms CA + Tīkla kļūdas apraksts + Tīkla kļūda: brokera resursdatora apraksts + Tīkla kļūda: brokera versijas apraksts + Privātās maršrutēšanas taimauts + Privātās maršrutēšanas kļūda + Privātās maršrutēšanas sesijas nav + Smp Proxy kļūda, nezināms CA + Smp Proxy kļūda, savienojuma izveide + Smp Proxy kļūda, brokera resursdators + Smp Proxy kļūda, brokera versija + Proxy galamērķa kļūda, nezināms CA + Proxy galamērķa kļūda, neizdevās izveidot savienojumu + Proxy galamērķa kļūda, brokera resursdators + Proxy Destination Error Broker Version + Lūdzu, mēģiniet vēlāk + Kļūda, sūtot ziņojumu + Kļūda, pārsūtot ziņojumus + Kļūda, veidojot ziņojumu + Kļūda, veidojot atskaiti + Kļūda, ielādējot detaļas + Kļūda, pievienojot dalībniekus + Kļūda, pievienojoties grupai + Kļūda, apstiprinot dalībnieku + Kļūda, atzīmējot dalībnieka atbalsta čatu kā lasītu + Kļūda, dzēšot dalībnieka atbalsta čatu + Nevar saņemt failu + Sūtītājs atcēla faila pārsūtīšanu + Fails Nav Apstiprināts (Nosaukums) + Fails Nav Apstiprināts (Apraksts) + %d Citas Failu Kļūdas + Kļūda, saņemot failu + %d Failu Kļūdas + Kļūda, veidojot adresi + Kontakts jau eksistē + Jūs jau esat savienots ar %s caur šo saiti + Nederīga savienojuma saite + Lūdzu, pārbaudiet pareizu saiti un, iespējams, pieprasiet jaunu + Neatbalstīta savienojuma saite + Saite prasa jaunāku lietotnes versiju, lūdzu, atjauniniet + Savienojuma kļūda Auth + Savienojuma kļūda Auth apraksts + Savienojuma kļūda Bloķēts + Savienojuma kļūda Bloķēts apraksts + Auth Open Migration uz citu ierīci + Bloķēšana nav iespējota + Jūs varat ieslēgt bloķēšanu + Ziņojuma piegādes kļūdas virsraksts + Ziņojuma piegādes brīdinājuma virsraksts + Ziņojuma piegādes kļūdas apraksts + Ziņojums ir dzēsts vai nav saņemts, kļūdas virsraksts + Ziņojums ir dzēsts vai nav saņemts, kļūdas apraksts + Ziņošanas iemesla brīdinājuma virsraksts + Ziņošanas arhīva brīdinājuma virsraksts + Pārsūtīt Brīdinājumu Pārsūtīt Ziņas Bez Failiem + Pārsūtīt Failus Ziņas Tiek Dzēstas Pēc Izvēles Apraksta + Pārsūtīt Failus Nav Akceptēts Apraksts + Pārsūtīt Failus Notiek Apraksts + Pārsūtīt Failus Neizdevās Saņemt Apraksts + Pārsūtīt Failus Trūkst Apraksts + Pārsūtīt Failus Nav Akceptēts Saņemt Failus + Pārsūtīt Failus Ziņas Tiek Dzēstas Pēc Izvēles Nosaukuma + Čata Saraksts Izlase + Čata Saraksts Kontakti + Čatu saraksta grupas + Čatu saraksta uzņēmumi + Čatu saraksta piezīmes + Čatu saraksta grupu atskaites + Paziņojumu grupas atskaite + Čatu saraksts Visi + Čatu saraksts Pievienot sarakstu + Grupu atskaites Aktīva viena + Grupu atskaites Aktīvas + Grupu atskaites Dalībnieku atskaites + Jauni atbalsta grupas ziņojumi + Jaunas atbalsta grupas čata sarunas + Jauna atbalsta grupas čata saruna + Jaunas atbalsta grupas čata sar. + Pievienoties čata sarunai + Nosūtīt pieprasījumu, lai pievienotos + Pievienoties, lai izmantotu botu + Apstiprināt kontaktpersonas pieprasījumu + Jūsu kontaktpersona + Bots + Čata baneris - pievienoties grupai + Čata baneris - tava grupa + Čata baneris - grupa + Čata baneris - biznesa savienojums + Čata baneris - tavs biznesa kontakts + Dalīties ar ziņu + Dalīties ar attēlu + Dalīties ar failu + Pārsūtīt ziņu + Pārsūtīt vairākas + Nevar koplietot ziņas brīdinājuma virsraksts + Nevar koplietot ziņas brīdinājuma teksts + Pievienot + Ikonas apraksts kontekstam + Ikonas apraksts attēla priekšskatījuma atcelšanai + Ikonas apraksts faila priekšskatījuma atcelšanai + Attēlu limita virsraksts + Video limita virsraksts + Attēlu limita apraksts + Video limita apraksts + Attēla atkodēšanas izņēmums + Attēla atkodēšanas izņēmuma apraksts + Video atkodēšanas izņēmuma apraksts + Faili un multivide ir aizliegti + Tikai īpašnieki var atļaut failus un multividi + Rakstiet un sūtiet tiešo ziņojumu, lai izveidotu savienojumu + Pārsūtīt ziņojumus (%d) + Saglabāt ziņojumus (%d) + SimpleX saites nav atļautas + Faili un multivide nav atļauti + Balss ziņas nav atļautas + Ierakstiet ziņu + Maksimālais ziņas lielums + Ir sasniegts maksimālais ziņas lielums + Ir sasniegts maksimālais ziņas lielums (ne teksts) + Ir sasniegts maksimālais ziņas lielums (pārsūtīšana) + Ziņot par iemeslu: spams + Ziņot par iemeslu: profils + Ziņot par iemeslu: kopiena + Ziņot par iemeslu: nelikumīgs saturs + Ziņošanas iemesla virsraksts - cits + Ziņojums Nosūtīts - Brīdinājuma Virsraksts + Ziņojums Nosūtīts - Brīdinājuma Ziņa - Skatīt Atbalsta Čatā + Pievienoties grupai + Pievienot ziņu + Savienoties + Vai sūtīt kontakta pieprasījumu? + Sūtot kontakta pieprasījumu, jūs atklāsiet savu SimpleX lietotājvārdu šim kontaktam. Vai vēlaties turpināt? + Sūtīt pieprasījumu bez ziņas + Sūtīt pieprasījumu + Nevar nosūtīt ziņu + Nevar nosūtīt ziņu – kontaktpersona nav gatava + Nevar nosūtīt ziņu – pieprasījums ir nosūtīts + Nevar nosūtīt ziņu – kontaktpersona ir izdzēsta + Nevar nosūtīt ziņu – kontaktpersona nav sinhronizēta + Nevar nosūtīt ziņu – kontaktpersona ir atspējota + Novērotājs nevar nosūtīt ziņu + Novērotājs nevar nosūtīt ziņu + Nevar nosūtīt ziņu – noraidīts + Nevar nosūtīt ziņu – grupa ir izdzēsta + Nevar nosūtīt ziņu, dalībnieks ir noņemts + Nevar nosūtīt ziņu, jūs esat izgājis + Nevar nosūtīt ziņu + Jūs esat vērotājs + Pārbaudīts ar administratoriem + Nevar nosūtīt ziņu, dalībniekam ir veca versija + Nevar Nosūtīt Komandas Brīdinājuma Teksts + Attēla Apraksts + Ikonas Apraksts Gaidot Attēlu + Ikonas Apraksts Lūgts Saņemt + Ikona Apraksts Attēls Nosūtīts Pabeigts + Gaida Attēlu + Attēls Tiks Saņemts, Kad Kontaktpersona Pabeigs Augšupielādi + Attēls Tiks Saņemts, Kad Kontaktpersona Būs Tiešsaistē + Attēls Saglabāts + Video Apraksts + Ikona Apraksts Gaida Video + Ikona Apraksts Video Pieprasīts Saņemt + Ikona Apraksts Video Nosūtīts Pabeigts + Gaida Video + Izmantot kameras pogu + No galerijas + Izvēlēties failu + Izvēlēties faila nosaukumu + Galerijas attēla poga + Galerijas video poga + Paldies, ka instalējāt SimpleX + Jūs varat sazināties ar SimpleX čata dibinātāju + Lai sāktu jaunu čatu, palīdzības virsraksts + Čata palīdzības pieskāriena poga + Saglabāt neizmantoto uzaicinājuma jautājumu + Jūs varat vēlreiz apskatīt uzaicinājuma saiti + Saglabāt uzaicinājuma saiti + Izveido saiti + Mēģināt vēlreiz + Kopīgojiet šo vienreizējo saiti + Ielīmējiet saņemto saiti + Ielīmētais teksts nav saite + Pieskarieties, lai ielīmētu saiti + Ielādē profilu + Nederīgs Qr kods + Noskenētais kods nav SimpleX Link Qr kods + Dzēstās sarunas + Nav filtrētu kontaktu + Kontaktu saraksta galvenes nosaukums + Konteksta lietotāja atlasītājs - Tavs profils + Konteksta lietotāja atlasītājs - Nevar mainīt profila brīdinājuma virsrakstu + Konteksta lietotāja atlasītājs - Nevar mainīt profila brīdinājuma ziņojums + Skenēt kodu + Nepareizs kods + Skenēt kodu no kontaktu lietotnes + Drošības kods + Atzīmēt kodu kā verificētu + Notīrīt verifikāciju + Salīdzināt, lai verificētu + Ir verificēts + Nav verificēts + Tavi iestatījumi + Tava SimpleX kontaktadreses + Tavi čata profili + Izveidot čata profilu + Datubāzes parole un eksportēšana + Par SimpleX Chat + Kā lietot SimpleX Chat + Markdown palīdzība + Markdown ziņās + Čats ar dibinātāju + Sūtiet mums e-pastu + Čata slēdzene + Čata konsole + Ziņojumu serveri + Smp serveri + Konfigurēti Smp serveri + Citi Smp serveri + Smp serveru iepriekš iestatīta adrese + Pievienot iepriekš iestatītu Smp serveri + Pievienot Smp serveri + Smp serveru testa serveris + Smp serveru testēšana + Saglabāt Smp serverus + Smp serveru tests neizdevās + Dažu Smp serveru tests neizdevās + Smp serveru QR koda skenēšana + Smp serveru ievadīšana manuāli + Smp serveru jauns serveris + Smp serveru iepriekš iestatīts serveris + Smp serveru jūsu serveris + Smp serveru jūsu servera adrese + Smp serveru izmantot serveri + Smp serveru izmantot serveri jaunam savienojumam + Smp Serveru pievienošana citai ierīcei + Smp Serveru nederīga adrese + Smp Serveru adreses pārbaude + Smp Servera dzēšana + Smp Serveri katram lietotājam + Vai saglabāt Smp Serverus? + Multivides un failu serveri + Xftp Serveri + Konfigurēti Xftp Serveri + Citi Xftp Serveri + Abonēšanas Procentuālais Daudzums + Instalēt SimpleX Chat Terminālim + Atiestatīt Visus Padomus + Atzīmēt Ar Zvaigzni Github + Ziedot + Novērtēt Aplikāciju + Izmantot SimpleX Chat Serverus? + Jūsu SMP Serveri + Jūsu XFTP Serveri + Izmantojot SimpleX Chat Serverus + Kā to darīt + Kā lietot savus serverus + Saglabātie ICE serveri tiks noņemti + Jūsu ICE serveri + Konfigurēt ICE serverus + Ievadiet vienu ICE serveri katrā rindā + Kļūda, saglabājot ICE serverus + Pārliecinieties, vai ICE serveru adreses ir pareizā formātā un unikālas + Poga \"Saglabāt serverus + Tīkls un serveri + Tīkla iestatījumi + Tīkla iestatījumu nosaukums + Tīkla Socks Proxy + Tīkla Socks Proxy iestatījumi + Tīkla Socks ieslēdzējs izmantot Socks Proxy + Tīkla Proxy autentifikācija + Tīkla Proxy nejauši akreditācijas dati + Tīkla Proxy autentifikācijas režīms izolēt pēc autentifikācijas lietotāja + Tīkla Proxy autentifikācijas režīms izolēt pēc autentifikācijas entītijas + Tīkla Proxy autentifikācijas režīms bez autentifikācijas + Tīkla starpniekservera autentifikācijas režīms - lietotājvārds un parole + Tīkla starpniekservera lietotājvārds + Tīkla starpniekservera parole + Tīkla starpniekservera ports + Nepareiza tīkla starpniekservera konfigurācija + Nepareiza tīkla starpniekservera konfigurācija + Resursdatora darbība + Porta darbība + Iespējot SOCKS tīklu + SOCKS tīkla informācija + Slīpsvītra Teksts + Pārsvītrots Teksts + Krāsains Teksts + Slepenais Teksts + Zvana Statuss Zvana + Zvana Statuss Garām Palaists + Zvana Statuss Noraidīts + Zvana Statuss Pieņemts + Zvana Statuss Savienojas + Zvana Statuss Notiek + Kontakts vēlas savienoties, izmantojot zvanu + Videozvans bez šifrēšanas + Šifrēts videozvans + Audiozvans bez šifrēšanas + Šifrēts audiozvans + Pieņemt + Noraidīt + Ignorēt + Zvans jau ir beidzies + Ikonas apraksts videozvanam + Ikona Audio zvans + Zvana piekļuves atļauja darbvirsmai noraidīta + Zvana piekļuves atļauja darbvirsmai noraidīta Chrome pārlūkā + Zvana piekļuves atļauja darbvirsmai noraidīta Safari pārlūkā + Audio un video zvanu iestatījumi + Tavi zvani + Vienmēr izmantot releju + Zvans bloķēšanas ekrānā + Pieņemt zvanu bloķēšanas ekrānā + Rādīt zvanu bloķēšanas ekrānā + Zvans nav atļauts bloķēšanas ekrānā + Jūsu Ice Serveri + Webrtc Ice Serveri + Relay Serveris Aizsargā Ip + Relay Serveris Ja Nepieciešams + Atveriet SimpleX Chat, lai pieņemtu zvanu + Atļaut zvanu pieņemšanu no bloķēšanas ekrāna + Atvērt + Statuss E2E Šifrēts + Statuss Bez E2E Šifrēšanas + Statusa kontaktam ir E2E šifrēšana + Statusa kontaktam nav E2E šifrēšanas + Zvana savienojums starp lietotājiem (Peer To Peer) + Zvana savienojums caur releju + Ikonas apraksts - pārtraukt zvanu + Ikonas apraksts - izslēgt video + Ikonas apraksts - ieslēgt video + Ikonas apraksts - izslēgt audio + Ikonas apraksts - ieslēgt audio + Ikonas apraksts - izslēgt skaļruni + Ikona Apraksts Skaļrunis Ieslēgts + Ikona Apraksts Skaņa Izslēgta + Ikona Apraksts Apgriezt Kameru + Ikona Apraksts Zvans Gaida Nosūtīšanu + Ikona Apraksts Neatbildēts Zvans + Ikona Apraksts Zvans Noraidīts + Ikona Apraksts Zvans Savienojas + Ikona Apraksts Zvans Notiek + Ikona Apraksts Zvans Beidzās + Atbildēt uz Zvanu + Izlaista integritātes ziņa + Bojāts integritātes ziņas jaucējkods (hash) + Bojāts integritātes ziņas ID + Dublēta integritātes ziņa + Brīdinājums par izlaistām ziņām + Brīdinājums par izlaistām ziņām. Tas var notikt, kad + Brīdinājums par ziņu ar bojātu jaucējkodu (hash) + Brīdinājums par ziņu ar bojātu jaucējkodu (hash) + Brīdinājums par ziņu ar bojātu ID + Brīdinājums par ziņu ar bojātu ID + Brīdinājums: Atšifrēšanas kļūda, neizdevās atšifrēt %d ziņas + Brīdinājums: Atšifrēšanas kļūda, pārāk daudz izlaistu ziņu + Brīdinājums: Fragmentu šifrēšana nav sinhronizēta, vecā datubāze + Brīdinājums: Neizdevās šifrēšanas atkārtota vienošanās + Brīdinājums: Lūdzu, ziņojiet par šo kļūdu izstrādātājiem + Privātums un drošība + Tavs privātums + Aizsargāt lietotnes ekrānu + Šifrēt lokālās datnes + Automātiski pieņemt attēlus + Aizsargāt IP adresi + Lietotne lūgs apstiprināt nezināmus failu serverus + Bez Tor vai VPN IP adrese būs redzama failu serveriem + Sūtīt saišu priekšskatījumus + Notīrīt saišu pārslēgu + Privātums Rādīt pēdējās ziņas + Privātums Ziņas melnraksts + Pilna rezerves kopija + Ieslēgt bloķēšanu + Bloķēšanas režīms + Bloķēt pēc + Iesniegt piekļuves kodu + Apstiprināt piekļuves kodu + Nepareizs piekļuves kods + Jauns piekļuves kods + Autentifikācija atcelta + La Mode System + La Mode Passcode + La App Passcode + La Mode Off + Parole uzstādīta + Parole mainīta + Parole nav mainīta + Mainīt bloķēšanas režīmu + Pašiznīcināšanās + Aktivizēta pašiznīcināšanās parole + Mainīt pašiznīcināšanās režīmu + Mainīt pašiznīcināšanās paroli + Pašiznīcināšanās parole aktivizēta + Pašiznīcināšanās parole mainīta + Pašiznīcināšanās parole + Ieslēgt pašiznīcināšanos + Pašiznīcināšanās Jauns Parādāmais Vārds + Ja ievadīsiet pašiznīcināšanās kodu + Visi lietotnes dati tiks dzēsti + Lietotnes parole aizstāta ar pašiznīcināšanos + Tiek izveidots tukšs čata profils + Ja ievadīsiet paroli, dati tiks izdzēsti + Uzstādīt paroli + Šis iestatījums ir paredzēts jūsu pašreizējam profilam + Saņemts grupas notikums: 1 dalībnieks pievienojies + Saņemts grupas notikums: 2 dalībnieki pievienojušies + Saņemts grupas notikums: 3 dalībnieki pievienojušies + Saņemts grupas notikums: N dalībnieki pievienojušies + Saņemto grupas notikumu skaits + Saņemti grupas un citi notikumi + Grupas dalībnieki 2 + Grupas dalībnieki N + Saņemts grupas notikums: Atvērt čatu + Profila atjaunināšanas notikums: Kontakta vārds ir mainīts + Snd Conn Event Switch Queue Phase Completed + Snd Conn Event Switch Queue Phase Changing + Conn Event Ratchet Sync Ok + Conn Event Ratchet Sync Allowed + Conn Event Ratchet Sync Required + Conn Event Ratchet Sync Started + Conn Event Ratchet Sync Agreed + Snd Conn Event Ratchet Sync Ok + Snd Conn Event Ratchet Sync Allowed + Snd Conn Event Ratchet Sync Required + Snd Conn Event Ratchet Sinhronizācija Sākta + Snd Conn Event Ratchet Sinhronizācija Apstiprināta + Rcv Conn Event Verifikācijas Kods Atiestatīts + Conn Event Iespējots Pq + Conn Event Atspējots Pq + Grupas Dalībnieka Loma Vērotājs + Grupas Dalībnieka Loma Autors + Grupas Dalībnieka Loma Dalībnieks + Grupas Dalībnieka Loma Moderators + Grupas Dalībnieka Loma Administrators + Grupas dalībnieka loma - īpašnieks + Grupas dalībnieka statuss - noraidīts + Grupas dalībnieka statuss - izņemts + Grupas dalībnieka statuss - atstājis + Grupas dalībnieka statuss - grupa izdzēsta + Grupas dalībnieka statuss - nezināms + Grupas dalībnieka statuss - uzaicināts + Grupas dalībnieka statuss - gaida apstiprinājumu + Grupas dalībnieka statuss - gaida apstiprinājumu (saīsināts) + Grupas dalībnieka statuss - gaida pārskatīšanu + Grupas dalībnieka statuss Gaida apstiprinājumu (īsais) + Grupas dalībnieka statuss Iepazīstināts + Grupas dalībnieka statuss Iepazīstināšanas uzaicinājums + Grupas dalībnieka statuss Pieņemts + Grupas dalībnieka statuss Paziņots + Grupas dalībnieka statuss Savienots + Grupas dalībnieka statuss Pabeigts + Grupas dalībnieka statuss Izveidotājs + Grupas dalībnieka statuss Savienojas + Grupas dalībnieka statuss Nezināms (īsais) + Iepriekšējais dalībnieks Vārds + Nav kontaktu, ko pievienot + Jaunā dalībnieka loma + Sākotnējā dalībnieka loma + Ikonas apraksts - izvērst lomu + Uzaicināt uz grupu + Uzaicināt uz čatu + Izlaist uzaicināšanu + Izvēlieties kontaktus + Ikonas apraksts - kontakts atzīmēts + Notīrīt kontaktu atlases pogu + Atlasīto kontaktu skaits + Nav atlasītu kontaktu + Uzaicinājums aizliegts + Uzaicinājuma aizlieguma apraksts + Poga Pievienot dalībniekus + Poga Pievienot komandas dalībniekus + Poga Pievienot draugus + Grupas informācijas sadaļas virsraksts - dalībnieku skaits + Grupas informācija - Tu + Poga Dzēst grupu + Poga Dzēst čatu + Vai dzēst grupu? + Vai dzēst čatu? + Dzēšot grupu visiem dalībniekiem, šo darbību nevarēs atsaukt. + Dzēšot čatu visiem dalībniekiem, šo darbību nevarēs atsaukt. + Dzēšot grupu sev, šo darbību nevarēs atsaukt. + Dzēšot čatu sev, šo darbību nevarēs atsaukt. + Poga Atstāt grupu + Poga Atstāt čatu + Poga Rediģēt grupas profilu + Poga Pievienot sagaidīšanas ziņu + Poga Sagaidīšanas ziņa + Grupas saite + Izveidot grupas saiti + Poga Izveidot grupas saiti + Dzēst saites jautājumu + Dzēst saiti + Jūs varat dalīties ar grupas saiti, un jebkurš varēs pievienoties + Visi grupas dalībnieki paliks savienoti + Kļūda, veidojot saiti grupai + Kļūda, atjauninot saiti grupai + Kļūda, dzēšot saiti grupai + Kļūda, veidojot dalībnieka kontaktu + Kļūda, sūtot ziņu kontakta uzaicinājumu + Tikai grupas īpašnieki var mainīt iestatījumus + Tikai čata īpašnieki var mainīt iestatījumus + Adreses sadaļas nosaukums + Dalīties ar adresi + Jūs varat dalīties ar šo adresi ar saviem kontaktiem + Koplietojamā teksta atjaunināšanas laiks + Koplietojamā teksta ziņojuma statuss + Koplietojamā teksta faila statuss + Koplietojamā teksta nosūtīšanas laiks + Koplietojamā teksta izveides laiks + Koplietojamā teksta saņemšanas laiks + Koplietojamā teksta dzēšanas laiks + Koplietojamā teksta moderēšanas laiks + Koplietojamā teksta pazušanas laiks + Pašreizējā vienuma informācija + Sūtītājs Ts laikā + Pašreizējās versijas laika zīmogs + Vienuma informācija bez teksta + Saņēmējs: Piegādes statuss + Saglabātās ziņas nosaukums + Poga \"Noņemt dalībnieku? + Poga \"Noņemt dalībniekus? + Poga \"Noņemt dalībnieku + Poga \"Atbalsta čata dalībnieks + Poga \"Sūtīt tiešo ziņu + Gaišā tēma + Tumšā tēma + SimpleX tēma + Melnā tēma + Sistēmas valoda + Tēma + Krāsu režīms + Tumšā tēma + Tumšā režīma krāsas + Importēt tēmu + Motīva importēšanas kļūda + Motīva importēšanas kļūdas apraksts + Eksportēt motīvu + Atiestatīt krāsu + Atiestatīt atsevišķu krāsu + Motīva galamērķa lietotnes motīvs + Primārā krāsa + Primārās krāsas variants + Sekundārā krāsa + Sekundārās krāsas variants + Fona krāsa + Virsmas krāsa + Virsraksta krāsa + Primary Variant2 krāsa + Nosūtītās ziņas krāsa + Nosūtītā citāta krāsa + Saņemtās ziņas krāsa + Saņemtā citāta krāsa + Fona tapetes krāsa + Fona tapetes tonis + Motīva Attēla Noņemšana + Izskata Fonta Izmērs + Izskata Tālummaiņa + Izskata Lietotnes Rīkjoslas + Izskata Lietotnē Iebūvēto Joslu Alfa + Izskata Joslu Izplūšanas Rādiuss + Sistēmas Režīma Uznirstošais Paziņojums + Fona Attēla Priekšskatījums - Sveika, Alise! + Fona Attēla Priekšskatījums - Sveiks, Bob! + Fona Attēla Mērogs + Fona Attēla Mērogošana Atkārtoti + Fona Attēla Mērogošana Aizpildīt + Fona Attēla Mērogošana Ietilpināt + Fona Attēla Paplašinātie Iestatījumi + Čata Tēmas Atiestatīšana uz Lietotnes Tēmu + Čata Tēmas Atiestatīšana uz Lietotāja Tēmu + Iestatīt Čata Tēmu uz Noklusējuma Tēmu + Lietot Čata Tēmu Režīmam + Lietot Čata Tēmu Visiem Režīmiem + Lietot Čata Tēmu Gaišajam Režīmam + Tērzēšanas tēma tumšajam režīmam + Tavas atļautās tērzēšanas preferences + Kontakta atļautās tērzēšanas preferences + Tērzēšanas preferenču noklusējums + Tērzēšanas preferences Jā + Tērzēšanas preferences Nē + Tērzēšanas preferences Vienmēr + Tērzēšanas preferences Ieslēgts + Tērzēšanas preferences Izslēgts` + Tērzēšanas preferences + Kontaktu preferences + Grupu preferences + Iestatīt grupu preferences + Iestatīt dalībnieku uzņemšanu + Tavas preferences + Ziņas ar taimeri + Tiešās ziņas + Pilnīga dzēšana + Ziņu reakcijas + Balss ziņas + Faili un Multivide + SimpleX Saites + Nesenā Vēsture + Audio Video Zvani + \nPieejams V51 versijā + Funkcija Ieslēgta + Funkcija Ieslēgta Jums + Funkcija Ieslēgta Kontaktam + Funkcija Izslēgta + Funkcijas Saņemšana Aizliegta + Pieņemt funkciju + Pieņemt funkciju kopumu 1 dienu + Atļaut saviem kontaktiem sūtīt pazūdošus ziņojumus + Atļaut pazūdošus ziņojumus tikai tad, ja + Pazūšanas laiks ir iestatīts tikai jauniem kontaktiem + Aizliegt sūtīt pazūdošus ziņojumus + Atļaut saviem kontaktiem neatgriezeniski dzēst + Atļaut neatgriezenisku ziņojumu dzēšanu tikai tad, ja + Kontakti var atzīmēt ziņojumus dzēšanai + Atļaut saviem kontaktiem sūtīt balss ziņojumus + Atļaut balss ziņas tikai tad, ja + Aizliegt balss ziņu sūtīšanu + Atļaut jūsu kontaktiem sūtīt failus un multivides saturu + Atļaut failus un multivides saturu tikai tad, ja + Aizliegt failu un multivides satura sūtīšanu + Atļaut jūsu kontaktiem pievienot ziņu reakcijas + Atļaut ziņu reakcijas tikai tad, ja + Aizliegt ziņu reakcijas + Atļaut jūsu kontaktiem zvanīt + Atļaut zvanus tikai tad, ja + Aizliegt zvanus + Gan jūs, gan jūsu kontaktpersona var sūtīt pazūdošus ziņojumus + Tikai jūs varat sūtīt pazūdošus ziņojumus + Tikai jūsu kontaktpersona var sūtīt pazūdošus ziņojumus + Pazūdoši ziņojumi šajā čatā ir aizliegti + Gan jūs, gan jūsu kontaktpersonas var dzēst + Tikai jūs varat dzēst ziņojumus + Tikai jūsu kontaktpersona var dzēst + Ziņojumu dzēšana ir aizliegta + Gan jūs, gan jūsu kontaktpersona var sūtīt balss ziņojumus + Aizliegt ziņu dzēšanu + Atļaut sūtīt balss ziņas + Aizliegt sūtīt balss ziņas + Atļaut ziņu reakcijas + Aizliegt ziņu reakcijas grupā + Atļaut sūtīt failus + Aizliegt sūtīt failus + Atļaut sūtīt SimpleX saites + Aizliegt sūtīt SimpleX saites + Iespējot nesenās vēstures sūtīšanu + V6 3 Reports Descr + V6 3 Organizēt čatu sarakstus + V6 3 Organizēt čatu sarakstu apraksts + V6 3 Labāka privātums un drošība + V6 3 Privāti multivides failu nosaukumi + V6 3 Iestatīt ziņojumu derīguma termiņu čatos + V6 3 Labāka grupu veiktspēja + V6 3 Ātrāka ziņojumu sūtīšana + V6 3 Ātrāka grupu dzēšana + V6 4 Savienoties ātrāk + V6 4 Ātrāks savienojums Descr + V6 4 Pārskatīt dalībniekus + V6 4 Pārskatīt dalībniekus Descr + V6 4 Atbalsta čats + V6 4 Atbalsta čats Descr + V6 4 Moderatora loma + V6 4 Moderatora loma Descr + V6 4 Ziņojumu piegāde Descr + V6 4 1 Sveicināti kontakti + V6 4 1 Sveicināti kontakti Descr + V6 4 1 Uzturiet tērzēšanas tīras + V6 4 1 Uzturiet tērzēšanas tīras Apraksts + V6 4 1 Īsā adrese + V6 4 1 Īsās adreses izveide + V6 4 1 Īsās adreses atjaunināšana + V6 4 1 Īsās adreses kopīgošana + V6 4 1 Jaunu saskarnes valodu apraksts + Skatīt atjauninātos nosacījumus + Pielāgota laika vienība sekundēs + Pielāgota laika vienība minūtēs + Pielāgots laika vienības stundas + Pielāgots laika vienības dienas + Pielāgots laika vienības nedēļas + Pielāgots laika vienības mēneši + Pielāgota laika izvēles atlasīšana + Pielāgota laika izvēles pielāgošana + Piegādes apstiprinājumu nosaukums + Ieslēgt apstiprinājumus visiem + Piegādes apstiprinājumu sūtīšana tiks ieslēgta visiem profiliem + Piegādes apstiprinājumu sūtīšana tiks ieslēgta + Neieslēgt kvītis + Jūs varat ieslēgt piegādes kvītis vēlāk + Piegādes kvītis ir atspējotas + Jūs varat ieslēgt piegādes kvītis vēlāk (brīdinājums) + Kļūda, ieslēdzot piegādes kvītis + Saistīt mobilo ierīci + Saistītās mobilās ierīces + Skenēt no mobilās ierīces + Pārbaudīt savienojumu + Pārbaudīt kodu mobilajā ierīcē + Šīs ierīces nosaukums + Šīs ierīces versija + Savienots mobilais tālrunis + Savienots ar mobilo tālruni + Ievadiet šīs ierīces nosaukumu + Šīs ierīces nosaukums, kas koplietots ar mobilo tālruni + Kļūda + Šī ierīce + Ierīces + Jauna mobilā ierīce + Atvienot darbvirsmas jautājums + Atvienot darbvirsmu + Atvienot attālo resursdatoru + Atvienot attālos resursdatorus + Attālais resursdators tika atvienots (paziņojums) + Attālais resursdators tika atvienots (virsraksts) + Attālā vadība tika atvienota (virsraksts) + Attālais resursdators atvienots no + Attālā vadība atvienota ar iemeslu + Attālās vadības savienojums apturēts (apraksts) + Remote Ctrl savienojums ir pārtraukts Identity Desc + Kopēšanas kļūda + Vai atvienoties no darbvirsmas? + Vienlaicīgi var darboties tikai viena ierīce + Atveriet mobilajā ierīcē un skenējiet QR kodu + Gaida, kad mobilā ierīce pieslēgsies + Nepareiza darbvirsmas adrese + Nesaderīga darbvirsmas versija + Darbvirsmas lietotnes versija nav saderīga + Darbvirsmas savienojums ir pārtraukts + Sesijas kods + Savienojamies ar darbvirsmu + Gaidām darbvirsmu + Atrasta darbvirsma + Savienoties ar darbvirsmu + Savienots ar darbvirsmu + Savienota darbvirsma + Pārbaudiet kodu ar darbvirsmu + Jauna darbvirsma + Saistītās darbvirsmas + Datoru Ierīces + Saistīto Datoru Iestatījumi + Skenēt QR Kodu No Datora + Datora Adrese + Pārbaudīt Savienojumus + Atklāt Tīklā + Multicast Atklājams Vietējā Tīklā + Multicast Automātiski Savienoties + Ielīmēt Datora Adresi + Datora Ierīce + Nav Saderīgs + Atjaunot QR Kodu + Nav Pievienots Mobilais Tālrunis + Nejaušs Ports + Atvērt Portu Ugunsmūrī + Atvērt Portu Ugunsmūrī Apraksts + Attālā Saimniekdatora Kļūda - Trūkst + Attālā Saimniekdatora Kļūda - Nav Aktīvs + Attālā Saimniekdatora Kļūda - Aizņemts + Attālā Saimniekdatora Kļūda - Noildze + Migrate To Device Imports Neizdevās + Migrate To Device Atkārtot Importu + Migrate To Device Ievadiet Paroli + Migrate To Device Faila Dzēšana Vai Saite Ir Nederīga + Migrate To Device Kļūda Arhīva Lejupielādē + Migrate To Device Čats Ir Pārcelts + Migrate To Device Pabeigt Migrāciju + Migrate To Device Apstipriniet Tīkla Iestatījumus + Migrate To Device Apstipriniet Tīkla Iestatījumu Kājeni + Migrate To Device Lietot Onion + Savienojuma kvotas kļūda + Savienojums neizdevās servera kvotas dēļ. + Kļūda, pieņemot kontaktu pieprasījumu + Kļūda, noraidot kontaktu pieprasījumu + Sūtītājs, iespējams, ir izdzēsis savienojuma pieprasījumu. + Kļūda, dzēšot kontaktu + Kļūda, dzēšot grupu + Kļūda, dzēšot piezīmju mapi + Kļūda, dzēšot kontaktu pieprasījumu + Kļūda, dzēšot gaidošo kontaktu savienojumu + Kļūda, mainot adresi + Kļūda, pārtraucot adreses maiņu + Kļūda, sinhronizējot savienojumu + Kļūda smp testa posmā + Kļūda smp testa servera autentifikācijā + Kļūda xftp testa servera autentifikācijā + Kļūda smp testa sertifikātā + Kļūda, iestādot adresi + Kļūda + Savienot + Atslēgt + Izveidot rindu + Droša rinda + Dzēst rindu + Izveidot failu + Augšupielādēt failu + Lejupielādēt failu + Salīdzināt failu + Dzēst failu + Kļūda, dzēšot lietotāju + Kļūda, atjauninot lietotāja privātumu + Iespējami lēni procesi + Iespējami lēni procesi + Kļūda, atjauninot čata tagus + Kļūda, izveidojot čata tagus + Kļūda, ielādējot čata tagus + Kļūda, sagatavojot kontaktu + Kļūda, sagatavojot grupu + Kļūda, mainot lietotāju + Tūlītējas paziņojumi + Pakalpojumu paziņojumi + Pakalpojumu paziņojumi atslēgti + Lai saglabātu privātumu, Simplex izmanto fona pakalpojumu, nevis uznirstošos paziņojumus, tas patērē mazāk datora akumulatora. + To var atslēgt caur iestatījumiem, paziņojumi joprojām tiek rādīti. + Izslēgt akumulatora optimizāciju + Izslēdzot pakalpojumu un periodiskos paziņojumus + Periodiskie paziņojumi + Periodiskie paziņojumi atslēgti + Periodiski paziņojumi + Izslēgt akumulatora optimizāciju + Izslēgt sistēmas ierobežojumu + Atslēgt paziņojumus + Sistēmas ierobežots fons + Brīdinājums par sistēmas ierobežotu fonu + Sistēmas ierobežots fons zvanā + Sistēmas ierobežots fons zvanā + Brīdinājums par sistēmas ierobežotu fonu zvanā + Ievadiet paroli + Ievadiet paroli + Datu bāzes inicializācijas kļūda + Neizdevās inicializēt datu bāzi. + Xiaomi ignorēt akumulatora optimizāciju + Simplex pakalpojumu paziņojums + Simplex pakalpojumu paziņojuma teksts + Zvana pakalpojumu paziņojums audio zvanam + Zvana pakalpojumu paziņojums video zvanam + Zvana pakalpojumu paziņojums par zvana beigām + Paslēpt paziņojumu + Paziņojumu kanāla ziņas + Paziņojumu kanāla zvanus + Iestatījumi par paziņojumu režīmu + Iestatījumi par paziņojumu priekšskatījuma režīmu + Iestatījumi par paziņojumu priekšskatījumu + Paziņojumu režīms izslēgts + Paziņojumu režīms periodisks + Paziņojumu režīms pakalpojums + Paziņojumu režīms izslēgts + Paziņojumu režīms periodisks + Paziņojumu režīms pakalpojums + Paziņojumu priekšskatījuma režīms + Paziņojuma priekšskatījuma režīms kontaktam + Paziņojuma priekšskatījuma režīms slēpts + Paziņojuma priekšskatījuma režīms + Paziņojuma priekšskatījuma režīms kontaktam + Paziņojuma attēlošanas režīms slēpts + Paziņojuma priekšskatījums kādam + Jauna ziņa + Jauns kontaktu pieprasījums + Kontakts savienots + Kļūda, rādot darbvirsmas paziņojumu + SimpleX Bloķēšana + Lai aizsargātu jūsu informāciju, ieslēdziet SimpleX bloķēšanu; jums tiks lūgts pabeigt autentifikāciju, pirms šī funkcija tiks aktivizēta. + Ieslēgt + Bloķēšanas režīms + Izmantot ierīces bloķēšanu + Izmantot lietotnes piekļuves kodu + Autentifikācija neizdevās + Nevarēja pārbaudīt + Nav lietotnes paroli + Ievadiet lietotnes piekļuves kodu + Pašreizējais lietotnes piekļuves kods + Mainīt lietotnes piekļuves kodu + Autentificēt + Uzreiz + sekundes + minūtes + Lūdzu, atcerieties saglabāt paroli + SimpleX bloķēšana ieslēgta + Jums būs jāveic autentifikācija, kad sākat vai atsākat + Atbloķēt + Pieslēgties, izmantojot akreditācijas datus + Ieslēgt SimpleX Bloķēšanu + Izslēgt SimpleX Bloķēšanu + Apstiprināt akreditācijas datus + Autentifikācija nav pieejama + Ierīces autentifikācija nav iespējota; to varat ieslēgt iestatījumos, kad tā būs iespējota. + Ierīces autentifikācija ir atspējota; izslēdzam. + Apturēt sarunu + Atvērt sarunu konsoli + Atvērt sarunu profilus + Ziņojumu arhīvs %dth + Ziņojumu arhīvs visi + Ziņojumu arhīvs + Ziņojumu arhīvs apraksts visi + Ziņojumu arhīvs man + Ziņojumu arhīvs visiem moderātoriem + Ci cita kļūda + Sūtīšanas autentifikācijas kļūda + Sūtīšanas kvotas kļūda + Sūtīšanas derīguma termiņš beidzies + Sūtīšanas pārsūtīšanas kļūda + Sūtīšanas starpniekservera kļūda + Sūtīšanas starpniekservera pārsūtīšanas kļūda + Servera hosta kļūda + Servera versijas kļūda + Faila autentifikācijas kļūda + Faila bloķēšanas kļūda + Faila nav + Faila pārsūtīšanas kļūda + Atbildēt + Kopīgot + Kopēt + Saglabāt + Rediģēt + Informācijas izvēlne + Meklēt + Arhivēt + Arhivēt ziņojumu + Arhivēt ziņojumus + Dzēst ziņojumu + Nosūtīta ziņa + Saņemta ziņa + Rediģēšanas vēsture + Nav vēstures + Atbilde uz + Saglabāts + Pārsūtīts + Saglabāts no + Pārsūtīts no + Saņēmēji nevar redzēt, no kura ir ziņa. + Piegāde + Nav piegādes informācijas + Dzēst + Atklāt + Paslēpt + Atļaut + Moderēt + Ziņot + Izvēlēties + Paplašināt + Dzēst ziņu? + Dzēst ziņas? + Šo darbību nevar atcelt. + Šo darbību nevar atcelt. + Ziņas dzēšanas atzīme dzēsta + Ziņu dzēšanas atzīme dzēsta + Dzēst? + Dzēst dalībniekus? + Moderētā ziņa tiks dzēsta + Moderētās ziņas tiks dzēstas + Moderētā ziņa tiks atzīmēta + Moderētās ziņas tiks atzīmētas + Tikai man + Visiem + Apturēt pārsūtīšanu + Apturēt faila sūtīšanu + Vai vēlaties apturēt šī faila sūtīšanu? + Apturēt faila saņemšanu + Vai vēlaties apturēt šī faila saņemšanu? + Apturēt + Atcelt failu + Atcelt failu + Vai vēlaties atcelt piekļuvi šim failam? + Atcelt + Pārsūtīt + Lejupielādēt failu + Saraksta izvēlne + Ziņa pārsūtīta + Šī ziņa tika pārsūtīta no citas sarunas. + Dalībnieks neaktīvs + Dalībnieks ir neaktīvs + Rediģēts + Ziņa nosūtīta + Ziņa nosūtīta bez atļaujas + Ziņas nosūtīšana neizdevās + Saņemtā ziņa nav izlasīta + Laipni lūdzam + Laipni lūdzam + Šis teksts ir pieejams iestatījumos + Jūsu sarunas + Rīkjoslas iestatījumi + Kontakta savienojums gaida apstiprinājumu + Dalībnieka kontakts nosūtīt tiešo ziņu + Grupas priekšskatījums ir atvērts pievienošanai + Grupas priekšskatījums, jūs esat aicināts + Grupas priekšskatījums, pievienojieties kā + Grupas priekšskatījums noraidīts + Grupas savienojums gaida apstiprinājumu + Noklikšķiniet, lai uzsāktu jaunu sarunu + Sarunājieties ar izstrādātājiem + Jums nav nevienas sarunas + Ielādēju sarunas… + Nav sarunu, kas atbilst filtram + Sarunu sarakstā nav + Nav neizlasītu sarunu + Nav sarunu + Nav atrastas sarunas + Kontakts, noklikšķiniet, lai savienotos + Atvērts savienojumam + Atvērts, lai izmantotu robotu + Atvērts, lai pieņemtu + Kontaktam jāpieņem + Savienoties ar kontaktu %s? + Meklējiet vai ielīmējiet simplex saiti + Adreses izveides instrukcija + Nav izvēlēta saruna + Izvēlēto sarunu vienības nav izvēlētas + Izvēlēto sarunu vienības izvēlētas %d + Pārsūtīt ziņas + Nav ko pārsūtīt + Video tiks saņemts, kad kontakts pabeigs augšupielādi. + Video tiks saņemts, kad kontakts būs tiešsaistē. + Fails + Liels fails + Kontakts nosūtīja lielu failu + Maksimālais atbalstītais faila izmērs + Gaidām failu + Fails tiks saņemts, kad kontakts pabeigs augšupielādi. + Fails tiks saņemts, kad kontakts būs tiešsaistē. + Fails saglabāts + Fails nav atrasts + Kļūda, saglabājot failu + Attālinātā faila ielāde + Attālinātā faila ielāde. + Faila kļūda + Pagaidu faila kļūda + Atvērt ar lietotni + Balss ziņa + Balss ziņa ar ilgumu + Nosūtīt + Paziņojumi + Atspējot automātisko dzēšanu? + Mainīt automātisko dzēšanu? + Atspējot automātisko dzēšanu + Mainīt automātisko sarunu dzēšanu + Atspējot automātisko dzēšanu + Sarunu laika ierobežojuma opciju apakšdaļa + Skatīt savienojumu + Skatīt atvērt + Skatīt + Skatīt zvanu + Skatīt meklēšanu + Skatīt video + Dzēst kontaktu? + Visas ziņas tiks dzēstas. To nevar atcelt. + Šo darbību nevar atcelt. + Saglabāt sarunu + Tikai dzēst sarunu + Apstiprināt kontakta dzēšanu? + Dzēst un paziņot kontaktam + Dzēst bez paziņojuma + Dzēst kontaktu + Saruna dzēsta + Jūs joprojām varat sūtīt ziņas kontaktam + Kontakts dzēsts + Jūs joprojām varat skatīt sarunu ar kontaktu + Ievadiet kontakta vārdu + Ievadiet čata nosaukumu + Serveris ir savienots + Serveris nav savienots + Servera kļūda + Serveris gaida + Vai vēlaties mainīt saņemšanas adresi? + Mainīt saņemšanas adresi + Pārtraukt saņemšanas adreses maiņu + Vai vēlaties piespiedu sinhronizāciju? + Piespiedu sinhronizācija + Apstiprināt piespiedu sinhronizāciju + Vai vēlaties sinhronizēt savienojumu? + Sinhronizēt savienojumu + Apstiprināt savienojuma sinhronizāciju + Šifrēšanas pārrunāšana notiek + Skatīt drošības kodu + Verificēt drošības kodu + Sūtīt ziņu + Ierakstīt balss ziņu + Vai vēlaties atļaut balss ziņas? + Jums jāatļauj balss ziņas, lai tās sūtītu. + Balss ziņas šajā čatā ir aizliegtas. + Lūdziet kontaktam iespējot balss ziņas + Tikai grupas īpašnieki var iespējot balss ziņas + Sūtīt tiešo ziņu + Izbeidzoša ziņa + Sūtīt izbeidzošu ziņu + Pielāgots laiks + Sūtīt + Tiešā ziņa + Sūtīt tiešo ziņu + Sūtīt + Atcelt tiešo ziņu + Atpakaļ + Atcelt + Apstiprināt + Atjaunot + Labi + Nav detaļu + Pievienot kontaktu + Kopēts + Pievienot kontaktu vai izveidot grupu + Kopīgot vienreizēju saiti + Savienot, izmantojot saiti vai QR + Nolasīt QR kodu + Izveidot grupu + Lai kopīgotu ar savu kontaktu + Savienot, izmantojot saiti vai QR no starpliktuves vai klātienē + Tikai saglabāts dalībnieku ierīcēs + Iespējot kameras piekļuvi + Noklikšķiniet, lai nolasītu + Kamera nav pieejama + Atļauja noraidīta + Augstāk minētais, tad prievārds turpinājums + Pievienot kontaktu, lai izveidotu saiti vai savienotu, izmantojot saiti + Izveidot grupu, lai izveidotu jaunu grupu + Lai savienotu, izmantojot saiti + Ja esat saņēmis simplex ielūguma saiti, varat to atvērt pārlūkā + Datorā nolasīt QR kodu no lietotnes, izmantojot QR koda nolasīšanu + Mobilajā ierīcē noklikšķiniet uz atvērt mobilajā lietotnē, tad noklikšķiniet uz savienot lietotnē + Pieņemt savienojuma pieprasījumu? + Ja izvēlēsieties noraidīt, sūtītājs netiks informēts + Pieņemt kontaktu + Pieņemt kontaktu inkognito + Noraidīt kontaktu + Pieņemt kontaktu pieprasījumu + Noraidīt kontaktu pieprasījumu + Sūtītājs netiks informēts + Dalībnieks ir izdzēsts, nevar pieņemt pieprasījumu + Notīrīt sarunu? + Notīrīt piezīmju mapi? + Notīrīt sarunu + Notīrīt piezīmju mapi + Notīrīt + Notīrīt sarunu + Notīrīt sarunas darbība + Dzēst kontaktu darbība + Dzēst grupu darbība + Atzīmēt kā izlasītu + Atzīmēt kā neizlasītu + Iestatīt kontakta vārdu + Atskaņot sarunu + Atskaņot visas sarunas + Atjaunot sarunu + Atzīmēt kā iecienītu + Noņemt no iecienītajiem + Neizlasītie minējumi + Izveidot sarakstu + Pievienot sarakstam + Mainīt sarakstu + Saglabāt sarakstu + Saraksta nosaukums + Saraksts ar šo nosaukumu jau pastāv + Dzēst sarakstu + Dzēst sarakstu? + Šo darbību nevar atcelt. + Rediģēt sarakstu + Mainīt secību + Jūs esat aicinājis kontaktu + Jūs esat pieņēmis savienojumu + Dzēst gaidošo? + Kontakts, ar kuru jūs dalījāties ar saiti, nevarēs izveidot savienojumu + Savienojums, ko jūs esat pieņēmis, tiks atcelts + Kontakta savienojums gaida + Savienojums gaida, viņiem jābūt tiešsaistē, varat dzēst un mēģināt vēlreiz + Kontakts vēlas izveidot savienojumu ar jums + Profila attēla vietturis + Profila attēls + Aizvērt + Saites priekšskatījums + Atcelt saites priekšskatījumu + Iestatījumi + QR kods + Adrese + Palīdzība + Simplex komanda + SimpleX logo + E-pasts + Vairāk + Rādīt QR kodu + Nederīgs QR kods + Šis QR kods nav saite + Nederīga kontaktu saite + Šī saite nav derīga savienojuma saite + Savienojuma pieprasījums nosūtīts + Jūs tiksiet savienots, kad grupas saimnieka ierīce būs tiešsaistē + Jūs tiksiet savienots, kad jūsu savienojuma pieprasījums tiks pieņemts + Jūs tiksiet savienots, kad jūsu kontaktu ierīce būs tiešsaistē + Ja jūs nevarat tikties klātienē, rādiet QR videozvanā vai caur citu kanālu + Jūsu čata profils tiks nosūtīts jūsu kontaktam + Ja jūs nevarat tikties klātienē, skenējiet QR videozvanā vai lūdziet ielūguma saiti + Kopīgot ielūguma saiti + Ielīmējiet saiti, ko saņēmāt, lai savienotos ar savu kontaktu + Uzzināt vairāk + Uzzināt vairāk par adresi + Savienojiet, tiks kopīgots jauns nejaušs profils + Savienojiet, jūsu profils tiks kopīgots + Skenējiet QR, lai savienotos ar kontaktu + Ja jūs nevarat tikties klātienē + Kopīgot adresi publiski + Kopīgot simplex adresi sociālajos tīklos + Jūs varat kopīgot savu adresi + Jūs nezaudēsiet savus kontaktus, ja izdzēsīsiet adresi + Kopīgot vienreizēju saiti ar draugu + Vienreizēju saiti var izmantot tikai ar vienu kontaktu + Jūs varat iestatīt savienojuma nosaukumu, lai atcerētos + Savienojuma drošība + Simplex adrese un vienreizējās saites ir drošas kopīgošanai + Lai pasargātu no jūsu saites aizvietošanas, salīdziniet kodus + Jūs varat pieņemt vai noraidīt savienojumu + Lasiet vairāk lietotāja rokasgrāmatā ar saiti + Adrese vai vienreizēja saite + Savienoties caur saiti + Savienoties + Ielīmēt + Šī virkne nav savienojuma saite + Jūs varat arī savienoties, noklikšķinot uz saites + Jauna saruna + Jauns + Pievienot kontaktu cilni + Skatīt ielīmēto saiti + Ielīmēt saiti + Vienreizēja saite + Vienreizēja saite īsi + Simplex adrese + Vai arī parādiet šo QR kodu + Pilna saite + Īsa saite + Jauna saruna dalīties profilā + Izvēlēties sarunas profilu + Profila maiņas kļūda + Profila maiņas kļūda + Vai arī skenējiet QR kodu + Tīkls atspējot SOCKS + Tīkls atspējot SOCKS + Tīkls izmantot sīpolu viesus + Tīkls izmantot sīpolu viesus priekšroka + Tīkls izmantot sīpolu viesus nē + Tīkls izmantot sīpolu viesus nepieciešams + Dodiet priekšroku sīpolu viesiem tīklā. + Sīpolu viesi netiks izmantoti tīklā. + Sīpolu viesi ir obligāti tīklā. + Tīkla sesijas režīms transporta izolācija + Tīkla sesijas režīms lietotājs + Tīkla sesijas režīms sesija + Tīkla sesijas režīms serveris + Tīkla sesijas režīms entitāte + Tīkla sesijas režīms lietotājs. + Tīkla sesijas režīms sesija. + Tīkla sesijas režīms serveris. + Tīkla sesijas režīms entitāte. + Atjaunināt tīkla sesijas režīmu? + Atspējot sīpolu viesus, ja nav atbalsta + Socks proxy iestatījumu ierobežojumi + Tīkla smp proxy režīms privātā maršrutēšana + Tīkla smp proxy režīms vienmēr + Tīkla smp proxy režīms nezināms + Tīkla smp proxy režīms neaizsargāts + Tīkla smp proxy režīms nekad + Tīkla smp proxy režīms vienmēr + Tīkla smp proxy režīms nezināms + Tīkla smp proxy režīms neaizsargāts + Tīkla smp proxy režīms nekad + Atjaunināt tīkla smp proxy režīmu? + Tīkla smp proxy rezerves atļaut samazināšanu + Tīkla smp proxy rezerves atļaut + Tīkla smp proxy rezerves atļaut aizsargātu + Tīkla smp proxy rezerves aizliegt + Tīkla smp proxy rezerves atļaut + Tīkla smp proxy rezerves atļaut aizsargātu + Tīkla smp proxy rezerves aizliegt + Atjaunināt tīkla smp proxy rezerves? + Privātā maršrutēšana rādīt + Privātā maršrutēšana skaidrojums + Tīkla smp tīmekļa ports + Tīkla smp tīmekļa porta slēdzis + Tīkla smp tīmekļa porta kājenes + Tīkla smp tīmekļa porta iepriekš iestatītā kājenes + Tīkla smp tīmekļa ports viss + Tīkla smp tīmekļa porta iepriekš iestatījums + Tīkla smp tīmekļa ports izslēgts + Izskata iestatījumi + Pielāgot tēmu + Tēmas krāsas + Lietotnes versija + Lietotnes versijas nosaukums + Lietotnes versijas kods + Pamatversija + Pamat simplexmq versija + Pārbaudīt atjauninājumus + Lietotnes atjauninājumu pārbaude atspējota + Lietotnes atjauninājumu pārbaude stabila + Lietotnes atjauninājumu pārbaude beta + Ir pieejams atjauninājums + Lejupielādēt + Izlaist + Lejupielāde uzsākta + Lejupielāde pabeigta + Atvērt + Instalēt + Veiksmīgi instalēts + Atjauninājums veiksmīgi instalēts. + Lejupielāde atcelta + Atgādināt vēlāk + Paziņojums par atjauninājumu + Saņemiet paziņojumus par pieejamiem atjauninājumiem. + Atspējot paziņojumus par atjauninājumiem + Rādīt izstrādātāja opcijas + Paslēpt izstrādātāja opcijas + Rādīt izstrādātāja opcijas + Kļūdu žurnāli + Izstrādātāja opcijas + Izstrādātāja opcijas + Novecojušas opcijas + Rādīt iekšējās kļūdas + Rādīt lēnus API izsaukumus + Izslēgt? + Izslēgšana + Kļūda saglabājot iestatījumus + Izveidot adresi + Dzēst adresi? + Jūsu kontakti paliks savienoti. + Visi jūsu kontakti paliks savienoti. + Visi jūsu kontakti paliks savienoti, atjauninājums nosūtīts. + Kopīgot saiti + Pievienot adresi savai profilam + Izveidot adresi un ļaut cilvēkiem savienoties + Izveidot simplex adresi + Kopīgot ar kontaktiem + Kopīgot adresi ar kontaktiem? + Profila atjauninājums tiks nosūtīts kontaktiem. + Pārtraukt adreses kopīgošanu + Pārtraukt kopīgošanu + Automātiski pieņemt kontaktus + Nosūtīts jūsu kontaktam pēc savienojuma izveides + Sveiciena ziņa + Ievadiet sveiciena ziņu (pēc izvēles) + Saglabāt iestatījumus? + Saglabāt automātiskā pieņemšanas iestatījumus + Dzēst adresi + Aicināt draugus + E-pasta aicinājuma temats + E-pasta aicinājuma saturs + Sociālajiem tīkliem + Vai arī, lai dalītos privāti + Simplex adrese vai vienreizējs saite + Izveidot vienreizēju saiti + Adreses iestatījumi + Uzņēmuma adrese + Pievienojiet savus komandas locekļus sarunām + Pievienot īsu saiti + Dalīties ar profilu, izmantojot saiti + Dalīties ar profilu, izmantojot saiti (teksts) + Dalīties + Uzlabot grupas saiti + Dalīties ar grupas profilu, izmantojot saiti + Dalīties ar grupas profilu, izmantojot saiti (teksts) + Dalīties ar veco adresi + Dalīties ar veco saiti + Turpināt uz nākamo soli + Nekādā gadījumā neveidot adresi + Jūs varat to izveidot vēlāk + Jūs varat padarīt adresi redzamu caur iestatījumiem + Aicināt draugus (īsi) + Rādāmais vārds + Pilns vārds + Īsa apraksts + Biogrāfija ir pārāk liela + Jūsu pašreizējais profils + Jūsu profils tiek glabāts ierīcē un tiek dalīts tikai ar kontaktiem, Simplex to neredz + Rediģēt attēlu + Dzēst attēlu + Saglabāt uzņemšanu? + Vai vēlaties saglabāt preferences? + Saglabāt un paziņot kontaktam + Saglabāt un paziņot kontaktiem + Saglabāt un paziņot grupas dalībniekiem + Iziet bez saglabāšanas + Paslēpt profilu + Parole, lai parādītu + Saglabāt profila paroli + Paslēptā profila parole + Apstiprināt paroli + Lai atklātu profilu, ievadiet paroli + Kļūda, saglabājot lietotāja paroli + Jūs kontrolējat savu sarunu + Ziņojumu un lietotņu platforma, kas aizsargā jūsu privātumu un drošību + Mēs nesaglabājam kontaktus vai ziņas serveros + Izveidot profilu + Jūsu profils tiek saglabāts jūsu ierīcē + Profils tiek koplietots tikai ar jūsu kontaktiem + Parādāmā vārda laukā nedrīkst būt atstarpes + Parādāmais vārds + Īss + Izveidot profilu + Izveidot citu profilu + Izveidot adresi + Nederīgs vārds + Labot vārdu uz + Par SimpleX + Kā izmantot Markdown + Jūs varat izmantot Markdown, lai formatētu ziņas + Trekns teksts + Zvana statuss beidzies + Zvana statuss kļūda + Zvana stāvoklis sākas + Zvana stāvoklis gaida atbildi + Zvana stāvoklis gaida apstiprinājumu + Zvana stāvoklis saņēmis atbildi + Zvana stāvoklis saņēmis apstiprinājumu + Zvana stāvoklis savienojas + Zvana stāvoklis savienots + Zvana stāvoklis beidzies + Nevar atvērt pārlūkprogrammu + Nevar atvērt pārlūkprogrammu + Nepieciešamas atļaujas + Atļaujas audio ierakstīšanai + Atļaujas kamerai + Atļaujas kamerai un audio ierakstīšanai + Piešķirt atļaujas + Piešķirt atļaujas iestatījumos + Atrast iestatījumos un piešķirt atļaujas + Atvērt iestatījumus + Audio ierīce - austiņas + Audio ierīce - skaļrunis + Audio ierīce - vadu austiņas + Audio ierīce - Bluetooth + Kļūda, inicializējot tīmekļa skatu + WebView nav atbalstīts šajā ierīces arhitektūrā. + Nākamā paaudze privātajai ziņošanai + Privātums pārdefinēts + Pirmā platforma bez lietotāju ID + Imūna pret surogātpastu un ļaunprātīgu izmantošanu + Cilvēki var savienoties tikai caur saites, ko jūs kopīgojat + Decentralizēts + Atvērtā koda protokols un kods, ko ikviens var palaist serveros + Izveidot savu profilu + Izveidot privātu savienojumu + Migrēt no citas ierīces + Kā tas darbojas + Kā darbojas SimpleX + Lai aizsargātu privātumu, SimpleX izmanto ID rindām + Tikai klientu ierīces glabā kontaktu grupas un e2e šifrētas ziņas + Visas ziņas un faili ir e2e šifrēti + Lasiet vairāk GitHub krātuvē. + Izmantot čatu + Ievada paziņojumu režīms + Ievada paziņojumu režīma apakšvirsraksts + Ievada paziņojumu režīms izslēgts + Ievada paziņojumu režīms periodisks + Ievada paziņojumu režīms pakalpojums + Ievada paziņojumu režīms izslēgts + Ievada paziņojumu režīms izslēgts, īss apraksts + Ievada paziņojumu režīms periodisks + Ievada paziņojumu režīms periodisks apraksts īsi + Ievada paziņojumu režīms pakalpojums + Ievada paziņojumu režīms pakalpojuma apraksts īsi + Ievada paziņojumu režīms akumulators + Iestatīt datu bāzes paroli + Jūs varat to mainīt vēlāk + Izmantot nejaušu paroli + Ievada nosacījumi privātās sarunas nav pieejamas + Ievada nosacījumi, izmantojot jūs piekrītat + Ievada nosacījumi privātuma politika un lietošanas noteikumi + Ievada nosacījumi pieņemt + Ievada nosacījumi konfigurēt servera operatorus + Ievada izvēlēties servera operatorus + Ievada tīkla operatori + Ievada tīkla operatori simplex flux vienošanās + Ievada tīkla operatori lietotne izmantos citus operatorus + Ievada tīkla operatori nevar redzēt, kas ar ko runā + Ievada tīkla operatori lietotne izmantos maršrutēšanai + Ievada tīkls par operatoriem + Ievada izvēlēties tīkla operatorus, ko izmantot + Kā tas palīdz privātumam + Ievada tīkla operatori konfigurēt caur iestatījumiem + Ievada tīkla operatori nosacījumi tiks pieņemti + Ievada tīkla operatori nosacījumi, ko jūs varat konfigurēt + Ievada tīkla operatori pārskatīt vēlāk + Ievada tīkla operatori atjaunināt + Ievada tīkla operatori turpināt + Ienākošais video zvans + Ienākošais audio zvans + Čeki + Čeku apraksts 1 + Čeku kontakti + Čeku kontakti iespējot + Čeku kontakti atspējot + Čeku kontakti pārsniegšana iespējota + Čeku kontakti pārsniegšana atspējota + Čeku kontakti iespējot saglabāt pārsniegumus + Čeku kontakti atspējot saglabāt pārsniegumus + Čeku kontakti iespējot visiem + Rēķinu kontaktu atslēgšana visiem + Rēķinu grupas + Rēķinu grupu aktivizēšana + Rēķinu grupu atslēgšana + Rēķinu grupu pārsniegšana aktivizēta + Rēķinu grupu pārsniegšana atslēgta + Rēķinu grupu aktivizēšana ar saglabātām pārsniegšanām + Rēķinu grupu atslēgšana ar saglabātām pārsniegšanām + Rēķinu grupu aktivizēšana visiem + Rēķinu grupu atslēgšana visiem + Privātuma mediju izplūšanas rādiuss + Privātuma mediju izplūšanas rādiuss izslēgts + Privātuma mediju izplūšanas rādiuss mīksts + Privātuma mediju izplūšanas rādiuss vidējs + Privātuma mediju izplūšanas rādiuss spēcīgs + Privātuma čata saraksta atvērtie saites + Privātuma čata saraksta atvērtie saites jā + Privātuma čata saraksta atvērtie saites nē + Privātuma čata saraksta atvērtie saites jautāt + Vai vēlaties atvērt tīmekļa saiti? + Privātuma čata saraksta atvērt tīmekļa saiti + Privātuma čata saraksta atvērt pilnu tīmekļa saiti + Privātuma čata saraksta atvērt tīru tīmekļa saiti + Iestatījumi jūs + Iestatījumi + Iestatījumi čata datubāze + Iestatījumi palīdzība + Iestatījumi atbalsts + Iestatījumi lietotne + Iestatījumi ierīce + Iestatījumi čati + Iestatījumi faili + Iestatījumi piegādes rēķini + Iestatījumi kontaktu pieprasījumi no grupām + Iestatījumi restartēt lietotni + Iestatījumi izslēgt + Iestatījumi izstrādātāja rīki + Iestatījumi eksperimentālās funkcijas + Iestatījumi zeķes + Iestatījumi + Tēmas + Profila attēli + Ziņu forma + Ziņu formas stūris + Ziņu formas aste + Sarunu tēmas iestatījumi + Lietotāja tēmas iestatījumi + Sarunu krāsu iestatījumi + Ziņu iestatījumi + Privāto ziņu maršrutēšanas iestatījumi + Zvanīšanas iestatījumi + Tīkla savienojuma iestatījumi + Incognito iestatījumi + Eksperimentālie iestatījumi + Izmantot no darbvirsmas + Jūsu sarunu datubāze + Palaist sarunu + Attālie hosti + Saruna notiek + Saruna ir apstājusies + Sarunu datubāze + Datubāzes parolfrāze + Eksportēt datubāzi + Importēt datubāzi + Jauns datubāzes arhīvs + Vecs datubāzes arhīvs + Atvērt datubāzes mapi + Dzēst datubāzi + Kļūda, sākot sarunu + Apstāt sarunu? + Apstāt sarunu, lai eksportētu, importētu vai dzēstu sarunu datubāzi + Apstiprināt sarunas apstāšanu + Iestatīt paroli eksportēšanai + Iestatiet paroli, lai eksportētu + Kļūda, apstādot sarunu + Kļūda, eksportējot sarunu datubāzi + Importēt datubāzi? + Jūsu pašreizējā sarunu datubāze tiks dzēsta un aizvietota ar importēto + Apstiprināt datubāzes importēšanu + Kļūda, dzēšot datubāzi + Kļūda, importējot datu bāzi + Sarunu datu bāze importēta + Restartējiet lietotni, lai izmantotu importēto sarunu datu bāzi + Notikušas nenozīmīgas kļūdas importēšanas laikā + Vai vēlaties dzēst sarunu? + Sarunu dzēšanas darbību nevar atcelt + Sarunu datu bāze dzēsta + Restartējiet lietotni, lai izveidotu jaunu sarunu profilu + Jums jāizmanto jaunākā datu bāzes versija + Faili un mediji + Dzēst failus un medijus visiem lietotājiem + Dzēst visus failus un medijus + Vai vēlaties dzēst failus un medijus? + Dzēst failus un medijus + Nav saņemtu lietotnes failu + Kopējais failu skaits un izmērs + Sarunu vienības derīguma termiņš nav + Sarunu vienības derīguma termiņš sekundēs + Sarunu vienības derīguma termiņš pēc noklusējuma + Ziņas + Ziņas + Dzēst ziņas pēc + Vai vēlaties iespējot automātisko dzēšanu? + Iespējot automātisko dzēšanu + Dzēst ziņas + Kļūda, mainot dzēšanu + Sarunu datu bāze eksportēta + Sarunu datu bāze eksportēta un saglabāta + Sarunu datu bāze eksportēta un migrēta + Sarunu datu bāze eksportēta, ne visi faili + Sarunu datu bāze eksportēta, turpināt + Kļūda, saglabājot datu bāzi + Saglabāt paroli atslēgu glabātājā + Saglabāt paroli iestatījumos + Datu bāze ir šifrēta + Kļūda, šifrējot datu bāzi + Noņemt paroli no atslēgu glabātāja + Noņemt paroli no iestatījumiem + Paziņojumi tiks slēpti + Noņemt paroli + Šifrēt datubāzi + Atjaunot datubāzi + Pašreizējā frāze + Jaunā frāze + Apstiprināt jauno frāzi + Atjaunot datubāzes frāzi + Iestatīt datubāzes frāzi + Iestatīt frāzi + Ievadiet pareizo pašreizējo frāzi + Datubāze nav šifrēta + Atslēgu glabātuve tiek droši glabāta + Iestatījumi tiek glabāti parastā tekstā + Šifrēts ar nejaušu frāzi + Nav iespējams atgūt frāzi + Atslēgu glabātuve ļauj saņemt ntfs + Frāze tiks saglabāta iestatījumos + Jums katru reizi jāievada frāze + Šifrēt datubāzi? + Mainīt datubāzes frāzi? + Datubāze tiks šifrēta + Datubāze tiks šifrēta un frāze tiks saglabāta + Datubāze tiks šifrēta un frāze tiks saglabāta iestatījumos + Datubāzes šifrēšana tiks atjaunināta + Datubāzes šifrēšana tiks atjaunināta iestatījumos + Datubāzes frāze tiks atjaunināta + Droši glabāt frāzi + Droši glabāt frāzi bez atgūšanas + Nepareiza frāze + Kļūda, lasot frāzi + Šifrēta datubāze + Datubāzes kļūda + Atslēgu glabātuves kļūda + Frāze ir atšķirīga + Fails ar ceļu + Nepieciešama datubāzes frāze + Kļūda ar + Nav iespējams piekļūt atslēgu glabātuvei + Nezināma datubāzes kļūda ar + Nepareiza frāze + Ievadiet pareizo frāzi + Nezināma kļūda + Ievadiet atslēgvārdu + Saglabāt atslēgvārdu un atvērt sarunu + Atvērt sarunu + Datu bāzes dublējumu var atjaunot. + Atjaunot datu bāzi + Atjaunot datu bāzi + Vai tiešām vēlaties atjaunot datu bāzi? + Apstiprināt datu bāzes atjaunošanu + Datu bāzes atjaunošanas kļūda + Atslēgvārds nav atrasts + Atslēgvārdu nevar nolasīt + Atslēgvārdu nevar nolasīt, lūdzu, ievadiet to manuāli + Datu bāzes jaunināšana + Datu bāzes samazināšana + Nesaderīga datu bāzes versija + Apstiprināt datu bāzes jauninājumus + Vienas rokas saskarne + Sarunu apakšējā josla + Vienas rokas saskarnes karte + Vienas rokas saskarnes maiņas instrukcija + Terminālis vienmēr redzams + Sarunu saraksts vienmēr redzams + Nederīga migrācijas apstiprināšana + Jaunināt un atvērt sarunu + Samazināt un atvērt sarunu + Mtr kļūda: nav lejupvērstas migrācijas + Mtr kļūda: atšķirīgs + Datu bāzes migrācijas + Datu bāzes samazināšana + Saruna ir apturēta + Jūs varat uzsākt sarunu, izmantojot iestatījumus vai restartējot lietotni + Sākt sarunu? + Saruna ir apturēta, jums jāveic datu bāzes pārsūtīšana + Grupas ielūguma vienums + Pievienoties grupai? + Jūs esat aicināts pievienoties grupai, lai sazinātos ar grupas dalībniekiem + Pievienoties grupai + Pievienoties grupai incognito + Pievienojos grupai + Jūs esat pieņēmuši grupas ielūgumu, savienojoties ar grupas dalībnieku, kurš jūs aicināja. + Atstāt grupu + Vai vēlaties atstāt grupu? + Vai vēlaties atstāt čatu? + Jūs pārtrauksiet saņemt ziņas no šīs grupas, bet sarunu vēsture tiks saglabāta. + Jūs pārtrauksiet saņemt ziņas no šī čata, bet sarunu vēsture tiks saglabāta. + Pievienot dalībniekus + Grupa ir neaktīva + Grupas ielūgums ir beidzies + Grupas ielūgums ir beidzies + Nav grupas + Nav grupas + Nav iespējams aicināt kontaktus + Nav iespējams aicināt kontaktus, jo nav pieejamu kontaktu. + Jūs nosūtījāt grupas ielūgumu + Jūs esat aicināts uz grupu + Grupas ielūgums - pieskarieties, lai pievienotos + Grupas ielūgums - pieskarieties, lai pievienotos incognito + Jūs pievienojāties šai grupai + Jūs noraidījāt grupas ielūgumu + Grupas ielūgums ir beidzies + Saņemts tiešais notikums: kontakts izdzēsts + Saņemts tiešais notikums: grupas ielūguma saite saņemta + Saņemts grupas notikums: dalībnieks pievienots + Saņemts grupas notikums: dalībnieks savienots + Saņemts grupas notikums: dalībnieks pieņēmis + Saņemts grupas notikums: lietotājs pieņēmis + Saņemts grupas notikums: dalībnieks atstājis + Saņemts grupas notikums: dalībnieka loma mainīta + Saņemts grupas notikums: dalībnieks bloķēts + Saņemts grupas notikums: dalībnieks atbloķēts + Saņemts grupas notikums: jūsu loma mainīta + Saņemts grupas notikums: dalībnieks izdzēsts + Saņemts grupas notikums: lietotājs izdzēsts + Saņemts grupas notikums: grupa izdzēsta + Saņemts grupas notikums: grupas profils atjaunināts + Saņemts grupas notikums: aicināts caur jūsu grupas saiti + Saņemts grupas notikums: dalībnieks izveidojis kontaktu + Saņemts grupas notikums: jauns dalībnieks gaida apstiprinājumu + Nosūtīts grupas notikums: dalībnieka loma mainīta + Grupas notikums mainījis lomu jums + Grupas notikums: dalībnieks bloķēts + Grupas notikums: dalībnieks atbloķēts + Grupas notikums: dalībnieks dzēsts + Grupas notikums: lietotājs pametis + Grupas notikums: grupas profils atjaunināts + Grupas notikums: dalībnieks pieņemts + Grupas notikums: lietotājs gaida pārskatīšanu + Profila atjauninājuma notikums: attēls noņemts + Profila atjauninājuma notikums: jauns attēls iestatīts + Profila atjauninājuma notikums: adrese noņemta + Profila atjauninājuma notikums: jauna adrese iestatīta + Profila atjauninājuma notikums: profils atjaunināts + Profila atjauninājuma notikums: dalībnieka vārds mainīts + Saņemšanas savienojuma notikums: maiņas rinda fāze pabeigta + Saņemšanas savienojuma notikums: maiņas rinda fāze mainās + Sūtīšanas savienojuma notikums: maiņas rinda fāze pabeigta dalībniekam + Sūtīšanas savienojuma notikums: maiņas rinda fāze mainās dalībniekam + Sūtīt kvītis + Kvīšu sūtīšana atspējota + Kvīšu sūtīšana atspējota + Kvīšu sūtīšana ir atspējota + Pievienot dalībniekus + Atbalsta čats + Konsolei + Rindas vietējais nosaukums + Rindas datu bāzes ID + Rindas debug piegāde + Rindas atjaunināts + Rindas ziņas statuss + Rindas faila statuss + Rindas nosūtīts + Rindas izveidots + Rindas saņemts + Rindas dzēsts + Rindas moderēts + Rindas pazūd + Kopīgojiet teksta datu bāzes ID + Dalībnieks tiks noņemts no grupas, šo darbību nevar atcelt + Dalībnieki tiks noņemti no grupas, šo darbību nevar atcelt + Dalībnieks tiks noņemts no sarunas, šo darbību nevar atcelt. + Dalībnieki tiks noņemti no sarunas, šo darbību nevar atcelt. + Apstiprināt dalībnieka noņemšanu + Noņemt dalībnieku + Bloķēt dalībnieku? + Bloķēt dalībnieku + Apstiprināt dalībnieka bloķēšanu + Bloķēt visiem? + Bloķēt dalībniekus visiem? + Bloķēt visiem + Bloķēt dalībnieku + Bloķēt dalībniekus + Atbloķēt dalībnieku? + Atbloķēt dalībnieku + Apstiprināt dalībnieka atbloķēšanu + Atbloķēt visiem? + Atbloķēt dalībniekus visiem? + Atbloķēt visiem + Atbloķēt dalībnieku + Atbloķēt dalībniekus + Dalībnieks ir bloķēts no administratora puses + Dalībnieks ir bloķēts + Dalībnieks ir atspējots + Dalībnieks ir neaktīvs + Dalībnieks + Loma grupā + Mainīt lomu + Mainīt + Pārslēgt + Mainīt dalībnieka lomu? + Dalībnieka loma tiks mainīta ar paziņojumu + Dalībnieka loma tiks mainīta ar paziņojumu sarunā + Dalībnieka loma tiks mainīta ar ielūgumu + Savienot caur dalībnieka adresi + Savienot caur dalībnieka adresi + Kļūda, noņemot dalībnieku + Kļūda, mainot lomu + Kļūda, bloķējot dalībnieku visiem + Rinda grupā + Rinda sarunā + Savienojuma rinda + Tiešais savienojuma līmenis + Netiešais savienojuma līmenis + Rinda + Nav rindas + Rindas serveris + Nevar zvanīt kontaktam + Nevar zvanīt kontaktam, lūdzu, gaidiet savienojumu. + Nevar zvanīt kontaktam, jo tas ir dzēsts. + Atļaut zvanus? + Jums jāatļauj zvanīt. + Zvanīšana aizliegta + Zvanīšana ir aizliegta, lūdzu, atļaujiet zvanus. + Nevar zvanīt dalībniekam + Nevar zvanīt dalībniekam, lūdzu, nosūtiet ziņu. + Nevar nosūtīt dalībniekam + Savienojums nav gatavs + Laipni lūdzam ziņa + Saglabāt laipni lūdzam? + Laipni lūdzam ziņa ir pārāk gara + Saglabāt un atjaunināt grupas profilu + Grupas laipni lūdzam priekšskatījums + Ievadiet laipni lūdzam ziņu + Pārāk liels + Savienojuma statistika serveriem + Saņemšana caur + Sūtīšana caur + Tīkla statuss + Mainīt saņemšanas adresi + Labot savienojumu + Labot savienojumu? + Apstiprināt savienojuma labošanu + Savienojuma labošana nav atbalstīta no kontakta puses + Savienojuma labošana nav atbalstīta no grupas dalībnieka puses + Pārrunāt šifrēšanu + Izveidot slepenu grupu + Grupa ir decentralizēta + Grupas redzamā nosaukuma lauks + Grupas pilnā nosaukuma lauks + Grupas īsā apraksta lauks + Grupa ir pārāk liela + Grupas galvenais profils nosūtīts + Sarunas galvenais profils nosūtīts + Izveidot grupu + Grupas profils tiek glabāts dalībnieku ierīcēs. + Saglabāt grupas profilu + Kļūda, saglabājot grupas profilu + Tīkla iepriekš iestatītie serveri + Operatora pārskata nosacījumi + Operatora nosacījumi pieņemti + Operatora nosacījumi pieņemti aktivizētiem operatoriem + Jūsu serveri + Operatoru nosacījumi pieņemti + Operatoru nosacījumi tiks pieņemti + Operators + Operatora serveri + Operators + Operatora mājaslapa + Operatora nosacījumi pieņemti + Operatora nosacījumi tiks pieņemti + Operatora izmantošanas slēdzis + Izmantot operatora x serverus + Operatora nosacījumi neizdevās ielādēt + Operatora nosacījumi pieņemti dažiem + Operatora tie paši nosacījumi tiks piemēroti + Operatora tie paši nosacījumi tiks piemēroti operatoriem + Operatora nosacījumi tiks piemēroti + Operatora nosacījumi tiks pieņemti dažiem + Operatoru nosacījumi arī tiks piemēroti + Skatīt nosacījumus + Pieņemt nosacījumus + Operatora lietošanas nosacījumi + Operatora atjaunotie nosacījumi + Operatora, lai izmantotu, pieņemiet nosacījumus + Operatora izmantošana ziņām + Operatora izmantošana ziņu saņemšanai + Operatora izmantošana ziņu privātai maršrutēšanai + Operatora pievienotie serveri + Operatora izmantošana failiem + Operatora izmantošana nosūtīšanai + Xftp serveri uz lietotāju + Operatora pievienotie xftp serveri + Operatora atvērtie nosacījumi + Operatora atvērtās izmaiņas + Kļūda servera atjaunināšanā + Kļūda servera protokola maiņā + Kļūda servera operatora maiņā + Operatora serveris + Serveris pievienots operatora nosaukumam + Kļūda servera pievienošanā + Tīkla opcija TCP savienojums + Tīkla opcijas atiestatītas uz noklusējumu + Tīkla opcija sekundes + Tīkla opcija TCP savienojuma laika limits + Tīkla opcija TCP savienojuma laika limits fons + Tīkla opcija protokola laika limits + Tīkla opcija protokola laika limits fons + Tīkla opcija protokola laika limits uz kb + Tīkla opcija RCV vienlaicība + Tīkla opcija ping intervāls + Tīkla opcija ping skaits + Tīkla opcija iespējot TCP keep alive + Tīkla opcijas saglabāt + Tīkla opcijas saglabāt un atkārtoti savienot + Atjaunināt tīkla iestatījumus? + Iestatījumu atjaunināšana atkārtoti savienos klientu ar visiem serveriem + Atjaunināt iestatījumus? + Pievienot lietotājus + Dzēst lietotājus? + Lietotāji dzēš visas sarunas + Lietotāji dzēš profilu priekš + Lietotāji dzēš ar savienojumiem + Lietotāji dzēš tikai datus + Paslēpt lietotāju + Atklāt lietotāju + Noklusināt lietotāju + Atcelt lietotāja noklusināšanu + Ievadiet paroli, lai parādītu + Noklikšķiniet, lai aktivizētu profilu + Padarīt profilu privātu + Jūs varat paslēpt vai izslēgt lietotāja profilu + Nerādīt atkal + Izslēgts, kad neaktīvs + Jūs joprojām saņemsiet zvanus un paziņojumus + Dzēst profilu + Dzēst sarunas profilu + Atvērt profilu + Atvērt sarunas profilu + Profila parole + Incognito + Incognito nejaušs profils + Incognito aizsargā + Incognito ļauj + Incognito kopīgo + Krāsu režīms: sistēmas + Krāsu režīms: gaišs + Krāsu režīms: tumšs + Tēmas režīms: sistēmas + Tikai jūs varat sūtīt balsi + Tikai jūsu kontakts var sūtīt balsi + Balsi aizliegts šajā sarunā + Gan jūs, gan jūsu kontakts var sūtīt failus + Tikai jūs varat sūtīt failus + Tikai jūsu kontakts var sūtīt failus + Faili aizliegti šajā sarunā + Gan jūs, gan jūsu kontakts var pievienot reakcijas + Tikai jūs varat pievienot reakcijas + Tikai jūsu kontakts var pievienot reakcijas + Reakcijas aizliegtas šajā sarunā + Gan jūs, gan jūsu kontakts var veikt zvanus + Tikai jūs varat veikt zvanus + Tikai jūsu kontakts var veikt zvanus + Zvanus aizliegts ar šo kontaktu + Atļaut sūtīt iznīkstošas ziņas + Aizliegt sūtīt iznīkstošas ziņas + Atļaut tiešās ziņas + Aizliegt tiešās ziņas + Atļaut dzēst ziņas + Atspējot neseno vēstures sūtīšanu + Iespējot dalībnieku ziņojumu sūtīšanu + Aizliegt locekļu ziņojumu sūtīšanu + Grupas locekļi var sūtīt iznīkstošas ziņas + Iznīkstošas ziņas ir aizliegtas + Grupas locekļi var sūtīt tiešās ziņas + Tiešās ziņas ir aizliegtas + Tiešās ziņas grupā ir aizliegtas + Tiešās ziņas čatā ir aizliegtas + Grupas locekļi var dzēst + Dzēšana čatā ir aizliegta + Grupas locekļi var sūtīt balss ziņas + Balss ziņas ir aizliegtas + Grupas locekļi var pievienot reakcijas + Reakcijas ir aizliegtas + Grupas locekļi var sūtīt failus + Faili grupā ir aizliegti + Grupas locekļi var sūtīt simplex saites + Simplex saites grupā ir aizliegtas + Jauniem locekļiem tiek nosūtīta nesenā vēsture + Jauniem locekļiem netiek nosūtīta nesenā vēsture + Grupas locekļi var sūtīt ziņojumus + Dalībnieku ziņojumi ir aizliegti + Dzēst pēc + sekundes + s + minūtes + mēnesis + mēneši + m + mth + stunda + stundas + h + diena + dienas + d + nedēļa + nedēļas + w + Piedāvātā funkcija + Piedāvātā funkcija ar parametru + Funkcija atcelta + Funkcijas lomas visiem dalībniekiem + Funkcijas lomas moderatoriem + Funkcijas lomas administratoriem + Funkcijas lomas īpašniekiem + Funkcija iespējota + Dalībnieku uzņemšana + Uzņemšanas posma pārskats + Pārskats par uzņemšanas posmu. + Dalībnieku kritēriji izslēgti + Dalībnieku kritēriji visi + Dalībnieku atbalsts + Nav atbalsta sarunu + Dzēst dalībnieku atbalsta sarunu + Dzēst dalībnieku atbalsta sarunu + Atbalsta saruna + Noraidīt gaidošo dalībnieku + Noraidīt gaidošo dalībnieku + Pieņemt gaidošo dalībnieku + Pieņemt gaidošo dalībnieku + Vai pieņemt gaidošo dalībnieku? + Apstiprinājums par gaidošā dalībnieka pieņemšanu kā dalībniekam + Apstiprinājums par gaidošā dalībnieka pieņemšanu kā novērotājam + Kas jauns + Jaunumi šajā versijā + Lasiet vairāk par jaunumiem + Drošības novērtējums + Drošības novērtējums. + Grupu saites + Grupu saites. + Automātiski pieņemt kontaktu pieprasījumus + Automātiski pieņemt kontaktu pieprasījumus. + Balss ziņas + Balss ziņas. + Neatgriezeniska dzēšana + Neatgriezeniska ziņu dzēšana. + Uzlabota servera konfigurācija + Uzlabota servera konfigurācija. + Uzlabota privātums un drošība + Uzlabota privātums un drošība. + Izbeigušās ziņas + Izbeigušās ziņas, kas pazūd pēc noteikta laika. + Tiešraides ziņas + Ziņas, kas tiek nosūtītas un saņemtas reālajā laikā. + Pārbaudīt savienojuma drošību + Pārbaudiet, vai jūsu savienojums ir drošs. + Franču saskarne + Izvēlieties franču valodu kā saskarnes valodu. + Vairāki čata profili + Izveidojiet un pārvaldiet vairākus čata profilus. + Melnraksts + Saglabājiet ziņu kā melnrakstu, lai to vēlāk pabeigtu. + Transporta izolācija + Izolējiet transporta slāni, lai uzlabotu drošību. + Privāti failu nosaukumi + Izmantojiet privātus failu nosaukumus, lai aizsargātu jūsu datus. + Samazināta akumulatora lietošana + Uzlabota akumulatora efektivitāte, lai pagarinātu lietošanas laiku. + Itāļu saskarne + Izvēlieties itāļu valodu kā saskarnes valodu. + Slēptie čata profili + Pārvaldiet slēptos čata profilus, kas nav redzami citiem. + Audio un video zvanīšana + Veiciet audio un video zvanus ar citiem lietotājiem. + Grupas moderēšana + Moderējiet grupas sarunas un saturu. + Grupas sveiciena ziņa + Sveiciena ziņa, kas tiek nosūtīta jaunajiem grupas dalībniekiem. + Samazināta akumulatora lietošana + Uzlabota akumulatora efektivitāte, lai pagarinātu lietošanas laiku. + Ķīniešu un spāņu saskarne + Izvēlieties ķīniešu vai spāņu valodu kā saskarnes valodu. + Lielu failu atbalsts + Atbalstiet lielu failu nosūtīšanu un saņemšanu. + Lietotnes piekļuves kods + Iestatiet piekļuves kodu, lai aizsargātu lietotni. + Poļu saskarne + Izvēlieties poļu valodu kā saskarnes valodu. + Ziņu reakcijas + Izteikiet savas reakcijas uz ziņām. + Pašiznīcinošs piekļuves kods + Piekļuves kods, kas nodrošina pašiznīcināšanos. + Pielāgotas tēmas + Izvēlieties un pielāgojiet tēmas pēc savas gaumes. + Uzlabotas ziņas + Saņemiet labākas un skaidrākas ziņas. + Japāņu-portugāļu saskarne + Jaunumi, pateicoties lietotāju ieguldījumam Weblate. + Ziņu piegādes apstiprinājumi + Saņemiet apstiprinājumus par ziņu piegādi. + Iemīļoto filtrs + Filtrējiet savas iecienītākās ziņas. + Šifrēšanas labojums + Uzlabojiet šifrēšanas drošību. + Izzust viena ziņa + Ziņa izzudīs pēc noteikta laika. + Vairāk iespēju + Atklājiet vēl vairāk funkciju. + Jauna darbvirsmas lietotne + Izmantojiet jauno un uzlaboto darbvirsmas lietotni. + Šifrēt vietējās failus + Aizsargājiet savus failus ar šifrēšanu. + Atklājiet grupas + Pievienojieties interesantām grupām. + Vienkāršāks inkognito režīms + Izmantojiet inkognito režīmu vieglāk. + Jaunas saskarnes valodas + Saistīt mobilo un darbvirsmas lietotni + Savienojiet mobilo un darbvirsmas lietotni. + Uzlabotas grupas + Izveidojiet un pārvaldiet grupas efektīvāk. + Inkognito grupas + Izveidojiet grupas, kas ir privātas un anonīmas. + Bloķēt grupas dalībniekus + Bloķējiet nevēlamus grupas dalībniekus. + Atklājiet vēl vairāk iespēju. + Privātas piezīmes + Saglabājiet savas piezīmes drošībā. + Vienkāršāka savienojuma saskarne + Izmantojiet savienojuma saskarni vieglāk. + Pievienoties grupas sarunai + Pievienojieties grupas sarunai. + Piegāde + Ziņu piegāde. + Jaunas saskarnes valodas + Kvantumam izturīga šifrēšana + Kvantumam izturīga šifrēšana. + Lietotnes datu migrācija + Lietotnes datu migrācija. + Attēls attēlā zvani + Attēls attēlā zvani. + Drošākas grupas + Drošākas grupas. + Kvantumam izturīga šifrēšana. + Pārsūtīt + Pārsūtīt. + Zvana skaņas + Zvana skaņas. + Profila attēlu forma + Profila attēlu forma. + Tīkls + Tīkls. + Jaunas saskarnes valodas + Privāta maršrutēšana + Privāta maršrutēšana. + Sarunu tēmas + Sarunu tēmas. + Droši faili + Droši faili. + Piegāde + Ziņu piegāde. + Persiešu saskarne + Jauna sarunu pieredze + Jaunas mediju iespējas + Privāta maršrutēšana. + Jūsu kontakti. + Pieejamā sarunu rīkjosla + Pieejamā sarunu rīkjosla. + Savienojieties ātrāk. + Dzēst daudz ziņu. + Sarunu saraksta multivide + Privātuma izplūšana + Palielināt fonta izmēru + Atjaunināt lietotni + Atjauniniet lietotni, lai iegūtu jaunākās funkcijas un uzlabojumus. + Savienojuma serveri + Pārbaudiet savienojuma serveru statusu. + Labāka drošība + Uzlabota drošība jūsu datiem. + Labāki zvani + Uzlabota zvanu kvalitāte. + Labāka lietotāja pieredze + Mainiet sarunu profilu. + Pielāgojamas ziņas + Ziņu datumi + Pārsūtīt vairākas ziņas + Dzēst vairākas ziņas + Tīkla decentralizācija + Decentralizējiet tīklu, lai uzlabotu drošību. + Tīkla decentralizācija, iespējot plūsmu + Tīkla decentralizācija, iespējot plūsmu iemesls + Uzlabota sarunu navigācija + Vieglāka navigācija sarunās. + Uzņēmumu sarunas + Sarunas uzņēmumiem. + Atsauces + Atsauces uz jums sarunās. + Ziņojumi + Attālinātā hosta kļūda: slikts stāvoklis + Attālinātā hosta kļūda: slikta versija + Attālinātā hosta kļūda: atslēgts + Attālinātā kontrole: neaktīva + Attālinātā kontrole: slikts stāvoklis + Attālinātā kontrole: aizņemta + Attālinātā kontrole: laika ierobežojums + Attālinātā kontrole: atslēgta + Attālinātā kontrole: slikta ielūgums + Attālinātā kontrole: slikta versija + Izstrādē + Šī funkcija ir izstrādē. + Savienojiet plānu, lai savienotos ar sevi + Šis ir jūsu personīgais vienreizējais saite + Jūs jau savienojaties ar %1$s + Jūs jau savienojaties + Jūs jau savienojaties, izmantojot šo vienreizējo saiti + Šis ir jūsu personīgais simplex adrese + Atkārtot savienojuma pieprasījumu + Jūs jau esat pieprasījis savienojumu, izmantojot šo adresi + Pievienojieties savai grupai + Šis ir jūsu saite grupai %1$s + Atkārtot pievienošanās pieprasījumu + Grupa jau pastāv + Čats jau pastāv + Jūs jau pievienojaties grupai %1$s + Jūs jau pievienojaties grupai + Jūs jau pievienojaties grupai, izmantojot šo saiti + Jūs jau esat grupā %1$s + Jūs jau esat savienots ar %1$s + Savienojiet, izmantojot saiti + Aģenta kritiska kļūda + Notikusi kritiska kļūda aģentā. + Aģenta iekšēja kļūda + Notikusi iekšēja kļūda aģentā. + Restartēt čatu + Migrēt uz ierīci + Vai nu ielīmējiet arhīva saiti + Ielīmējiet arhīva saiti + Nederīga faila saite + Čata arhīvs + Migrē uz ierīci + Migrē uz ierīci, datu bāzes inicializācija + Migrē uz ierīci, lejupielādējot detaļas + Migrē uz ierīci, lejupielādējot arhīvu + Migrē uz ierīci, lejupielādēti %d baiti + Migrē uz ierīci, lejupielāde neizdevās + Atkārtot lejupielādi + Mēģiniet vēlreiz + Migrē uz ierīci, importējot arhīvu + Migrēt no ierīces + Migrēt no ierīces uz citu ierīci + Kļūda, migrējot no ierīces, saglabājot iestatījumus + Migrēt no ierīces, eksportētā faila nav + Kļūda, migrējot no ierīces, eksportējot arhīvu + Migrēt no ierīces, datu bāzes inicializācija + Kļūda, migrējot no ierīces, augšupielādējot arhīvu + Kļūda, migrējot no ierīces, dzēšot datu bāzi + Migrēt no ierīces, apturot sarunu + Migrēt no ierīces, sarunai jābūt apturētai + Migrēt no ierīces, arhivēt un augšupielādēt + Migrēt no ierīces, apstiprināt augšupielādi + Migrēt no ierīces, visi dati tiks augšupielādēti + Migrēt no ierīces, arhivējot datu bāzi + Migrēt no ierīces, augšupielādētie biti + Migrēt no ierīces, augšupielādējot arhīvu + Migrēt no ierīces, augšupielāde neizdevās + Migrēt no ierīces, atkārtot augšupielādi + Migrēt no ierīces, mēģiniet vēlreiz + Migrēt no ierīces, veidojot arhīva saiti + Migrēt no ierīces, atcelt migrāciju + Migrēt no ierīces, pabeigt migrāciju + Migrēt no ierīces, vai dzēst arhīvu? + Migrēt no ierīces, augšupielādētais arhīvs tiks dzēsts + Migrēt no ierīces, izvēlieties migrēt no citas ierīces + Migrēt no ierīces, vai kopīgot šo faila saiti + Migrēt no ierīces, dzēst datu bāzi no ierīces + Migrēt no ierīces, sarunas uzsākšana vairākās ierīcēs nav atbalstīta + Migrēt no ierīces, uzsākt sarunu + Migrēt no ierīces, migrācija pabeigta + Migrēt no ierīces, nedrīkstat uzsākt datu bāzi divās ierīcēs + Migrēt no ierīces, izmantošana divās ierīcēs pārtrauc šifrēšanu + Migrēt no ierīces, pārbaudīt datu bāzes paroli + Migrēt no ierīces, pārbaudīt paroli + Migrēt no ierīces, apstipriniet, ka atceraties paroli + Migrēt no ierīces, pārbaudiet savienojumu un mēģiniet vēlreiz + Migrēt no ierīces, arhīvs tiks dzēsts + Kļūda, migrējot no ierīces, pārbaudot paroli + Tīkla veids: nav tīkla savienojuma + Tīkla veids: mobilais + Tīkla veids: Wi-Fi + Tīkla veids: Ethernet + Cita tīkla veida + Serveri + Serveru failu cilne + Trūkstošie serveri + Serveru mērķis + Visi lietotāji + Pašreizējais lietotājs + Serveru transporta sesiju virsraksts + Savienotās serveru sesijas + Savienojamās serveru sesijas + Serveru sesiju kļūdas + Serveru statistikas virsraksts + Nosūtītās ziņas no serveriem + Saņemtās ziņas no serveriem + Serveru detaļas + Serveru privāto datu atruna + Serveru abonementu virsraksts + Abonētās serveru savienojumu + Gaidošie serveru abonementu savienojumi + Kopējais serveru abonementu skaits + Savienoto serveru virsraksts + Iepriekš savienoto serveru virsraksts + Proksēto serveru virsraksts + Proksēto serveru kājenes + Pārsavienot serverus + Pārsavienot serverus + Pārsavienot serveri + Pārsavienot serveri + Kļūda pārsavienojot serverus + Kļūda pārsavienojot serveri + Serveru modalitātes kļūda + Pārsavienot visus serverus + Atjaunot serveru statistiku + Atjaunot serveru statistiku + Atjaunot serveru statistiku + Apstiprināt serveru statistikas atjaunošanu + Kļūda atjaunojot serveru statistiku + Augšupielādēti serveri + Lejupielādēti serveri + Detalizēta serveru statistika + Serveru detalizētā statistika par nosūtītajām ziņām + Serveru detalizētā statistika par kopējo nosūtīto ziņu skaitu + Serveru detalizētā statistika par saņemtajām ziņām + Serveru detalizētā statistika par kopējo saņemto ziņu skaitu + Serveru detalizētā statistika par saņemšanas kļūdām + Serveri sākot no + Smp serveris + Xftp serveris + Pārlādēt + Mēģinājumi + Nosūtīts tieši + Nosūtīts caur proxy + Proxy + Nosūtīšanas kļūdas + Beidzies + Citi + Dublikāti + Atšifrēšanas kļūdas + Citas kļūdas + Apstiprināts + Apstiprināšanas kļūdas + Savienojumi + Izveidots + Aizsargāts + Pabeigts + Izdzēsts + Izdzēšanas kļūdas + Abonēts + Abonēšanas rezultāti ignorēti + Abonēšanas kļūdas + Augšupielādētie faili + Izmērs + Augšupielādītie fragmenti + Augšupielādes kļūdas + Izdzēstie fragmenti + Lejupielādētie fragmenti + Lejupielādētie faili + Lejupielādes kļūdas + Servera adrese + Atvērt servera iestatījumus + Sasniegts maksimālais grupas pieminējumu skaits ziņā. + 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 dc692c1968..d21b8b8f83 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml @@ -416,4 +416,4 @@ %s വാഗ്ദാനം ചെയ്തു തിരയുക ഓഫാണ് - \ No newline at end of file + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml new file mode 100644 index 0000000000..a6385a5ce0 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml @@ -0,0 +1,220 @@ + + + 1 dag + 1 minutt + 1 måned + 1 rapport + Engangslenke + 1 uke + 1 år + 30 sekunder + 5 minutter + a + b + Avbryt + Avbryt adresseendring + Avbryt adresseendring? + Om operatører + Om SimpleX + Om SimpleX-adressen + Om SimpleX Chat + Aksentfarge + Aksepter + Aksepter + Aksepter + Aksepter + Aksepter + Aksepter + Aksepter som medlem + Aksepter som observatør + Aksepter vilkår + Aksepter tilkoblingsforespørsel? + Aksepter kontaktforespørsel + Aksepter kontaktforespørsel + Akseptert %1$s + Akseptert anrop + Akseptert vilkår + Akseptert invitasjon + Aksepterte deg + Aksepter inkognito + Aksepter medlem + Koble til servere via SOCKS proxy på port %d? Proxy må startes før du skrur på dette valget. + Bekreftelsesfeil + Aktive tilkoblinger + Legg til en adresse i profilen din slik at kontaktene dine kan dele denne med andre. Profiloppdateringen vil bli sendt til dine kontakter. + Legg til kontakt + Lagt til medie- og filservere + Meldingsservere er lagt til + Legg til venner + Legg til en liste + Legg til en melding + Legg til forhåndsvalgte servere + Legg til en profil + Adresse + Adresseendringen vil bli avbrutt. Den gamle mottakeradressen vil bli brukt. + Adresse eller engangslenke? + Adresseinnstillinger + Legg til en server + Legg til servere ved å skanne QR-koder. + Legg til teammedlemmer + Legg til en annen enhet + Legg til listen + Legg til velkomstmelding + Legg til dine teammedlemmer i samtalene. + administrator + administratorer + Administratorer kan blokkere ett medlem for alle. + Administratorer kan lage lenker for å bli med i grupper. + Avanserte nettverksinnstillinger + Avanserte innstillinger + Avanserte innstillinger + godkjenner krypteringen… + alle + Alle + All appdata er slettet. + Alle chatter og meldinger vil bli slettet - dette kan ikke angres! + Alle chatter vil bli fjernet fra listen %s, og listen vil bli slettet + Alle gruppemedlemmer vil forbli tilkoblet. + alle medlemmer + Alle meldinger vil bli slettet - dette kan ikke angres! + Alle meldinger vil bli slettet – dette kan ikke angres! Meldingene vil KUN bli slettet for deg. + Alle nye meldinger fra %s vil bli skjult! + Alle nye meldinger fra disse medlemmene vil bli skjult! + Tillat + Tillate + Tillat anrop? + Tillat samtaler kun vis kontakten din tillater dem. + Tillat forsvinnende meldinger kun vis kontakten din tillater dem. + Tillat nedgradering + Tillat filer og medier kun vis kontakten din tillater det. + Tillat irreversibel sletting av meldinger kun hvis kontakten din tillater det. (24 timer) + Tillat meldingsreaksjoner. + Tillat meldingsreaksjoner kun vis kontakten din tillater det. + Tillat direktemeldinger til medlemmer. + Tillat å slette sendte meldinger irreversibelt. (24 timer) + Tillat å rapportere meldinger til moderatorer. + Tillat å sende forsvinnende meldinger. + Tillat å sende filer og medier. + Tillat sending av SimpleX-lenker. + Tillat sending av talemeldinger. + Tillat talemeldinger? + Tillat talemeldinger kun vis kontakten din tillater det. + Tillat kontaktene dine å sende meldingsreaksjoner. + Tillat kontaktene dine å ringe deg. + Tillat kontaktene dine å irreversibelt slette sendte meldinger. (24 timer) + Tillat kontaktene dine å sende forsvinnende meldinger. + Tillat kontaktene dine å sende filer og medier. + Tillat kontaktene dine å sende talemeldinger. + Alle profiler + Alle rapporter vil bli arkivert for deg. + Alle servere + Alle dine kontakter, samtaler og filer vil bli kryptert og lastet opp i deler til konfigurerte XFTP-reléer. + Alle kontaktene dine vil forbli tilkoblet. + Alle kontaktene dine vil forbli tilkoblet. Profiloppdatering vil bli sendt til kontaktene dine. + Kobler allerede til! + alltid + Alltid + Alltid på + Bruk alltid privat ruting. + Bruk alltid relé + og %d andre hendelser + Android Keystore brukes til å lagre passord på en sikker måte – det gjør at varslingstjenesten fungerer. + Android Keystore brukes til å trygt lagre passordet ditt etter at du restarter appen eller bytter passord - det gjør at du kan motta varsler. + En tom chat-profil med navnet du har valgt vil bli laget, og appen åpnes som vanlig. + En ny tilfeldig profil vil bli delt. + En annen grunn + Svar anrop + Hvem som helst kan være vert for servere. + APP + Appen kjører alltid i bakgrunnen + App build: %s + Appen kan bare motta varsler når den er åpen, ingen bakgrunnstjeneste vil bli startet. + Backup av appdata + Migrering av appdata + Utseende + Appen krypterer nye lokale filer (unntatt videoer). + Appikon + Bruk + Bruk på + App-passord + App-passord + App-passordet byttes med det selvdestruerende passordet. + Appøkt + Apptema + App-verktøylinjer + Appoppdatering er lastet ned. + Appversjon + Appversjon: v%s + Arabisk, bulgarsk, finsk, hebraisk, thai og ukrainks - takk til brukerne og Weblate. + Arkiv + Arkiver alle rapporter? + Arkiver og last opp + Arkiver kontakter for å chatte senere. + Arkiverte kontakter + arkivert rapport + arkivert rapport av %s + Arkiver %d rapporter? + Arkiver rapport + Arkiver rapport? + Arkiver rapporter + Arkiverer database + Spør + Bedt om å motta bildet + Bedt om å motta videoen + Legg ved + forsøk + Lyd- og videosamtaler + lydanrop + Lydanrop + lydanrop (ikke E2E-kryptert) + Lyd av + Lyd på + Lyd- og videosamtaler + Lyd/videosamtaler + Lyd/videosamtaler er ikke tillatt. + Autentisering avbrutt + Autentisering mislyktes + Autentisering er ikke tilgjengelig + forfatter + Godta automatisk + Godta kontaktforespørsler automatisk + Godta bilder automatisk + \nTilgjengelig i v5.1 + Tilbake + Bakgrunn + Bakgrunnstjenesten kjører alltid - varsler vises så snart meldingene er tilgjengelige. + Batterioptimalisering er aktivert, og bakgrunnstjenesten og periodiske forespørsler om nye meldinger er slått av. Du kan aktivere det igjen i innstillingene. + Beta + Bedre anrop + Bedre grupper + Bedre gruppeytelse + Bedre meldingsdatoer. + Bedre meldinger + Bedre personvern og sikkerhet + Bedre sikkerhet ✅ + Bedre brukeropplevelse + Bio: + Bio er for stor + Svart + Blokker + blokkert + blokkert + blokkert av administrator + Blokkert av administrator + blokkert %s + Blokker for alle + Blokker gruppemedlemmer + Blokker medlem + Blokker medlem? + Blokker medlem for alle? + Blokker medlemmer for alle? + Bluetooth + fet + Bot + Både du og kontakten din kan legge til reaksjoner på meldinger. + Både du og kontakten din kan slette sendte meldinger irreversibelt. (24 timer) + Både du og kontakten din kan ringe. + Både du og kontakten din kan sende forsvinnende meldinger. + Både du og kontakten din kan sende filer og medier. + Både du og kontakten din kan sende talemeldinger. + Firmaadresse + 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 ced3b9a3b0..655b1cedbd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -445,7 +445,7 @@ Berichten Nieuw wachtwoord… Keychain fout - Word lid van + Word lid Groep verlaten\? Nieuwe leden rol Geen contacten om toe te voegen @@ -694,7 +694,7 @@ Server test Beoordeel de app Gebruik server - Gebruik van SimpleX Chat servers. + Gebruik SimpleX Chat servers. Uw server adres Uw server Transport isolation @@ -811,7 +811,7 @@ Dank aan de gebruikers – draag bij via Weblate! Transport isolation SimpleX - U bent verbonden met de server die wordt gebruikt om berichten van dit contact te ontvangen. + U bent verbonden met de server die wordt gebruikt om berichten van deze verbinding te ontvangen. Je profiel wordt verzonden naar het contact van wie je deze link hebt ontvangen. Je maakt verbinding met alle groepsleden. Uitvoeren bij geopende app @@ -854,7 +854,7 @@ 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 chats + 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\? @@ -912,8 +912,8 @@ SimpleX groep link SimpleX links Eenmalige SimpleX uitnodiging - Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen. - Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s). + Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van deze verbinding te ontvangen. + Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s). onbekend berichtformaat Via browser via contact adres link @@ -936,7 +936,7 @@ Fout bij bijwerken van groep link Initiële rol Waarnemer - Je kunt geen berichten versturen! + jij bent waarnemer je bent waarnemer Systeem Audio en video oproepen @@ -1047,7 +1047,7 @@ Je kunt SimpleX Vergrendeling aanzetten via Instellingen. Vergrendeling inschakelen Vergrendeling modus - Indienen + Bevestigen Verificatie geannuleerd Wijzig de vergrendelings modus Bevestig toegangscode @@ -1448,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 @@ -1596,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 @@ -1832,7 +1832,7 @@ Systeem Wallpaper achtergrond Stel het standaard thema in - Toon chatlijst in nieuw venster + Toon chat-lijst in nieuw venster geen Foutopsporing bezorging Informatie over berichtenwachtrij @@ -1872,8 +1872,7 @@ Details Berichten ontvangen Bericht ontvangst - Beginnend vanaf %s. -\nAlle gegevens zijn privé op uw apparaat. + Vanaf %s.\nAlle gegevens blijven privé op uw apparaat. Verbonden servers In behandeling Eerder verbonden servers @@ -2040,11 +2039,11 @@ Verwijder maximaal 20 berichten tegelijk. Sommige bestanden zijn niet geëxporteerd Alle hints resetten - Chatlijst wisselen: + Chat-lijst wisselen: U kunt dit wijzigen in de instellingen onder uiterlijk Creëren Vervagen voor betere privacy. - Afspelen via de chatlijst. + Afspelen via de chat-lijst. Download nieuwe versies van GitHub. Vergroot het lettertype. App automatisch upgraden @@ -2184,7 +2183,7 @@ 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. - Flux inschakelen + Schakel Flux in bij Netwerk- en serverinstellingen voor betere privacy van metagegevens. Geen bericht App-werkbalken Vervagen @@ -2244,7 +2243,204 @@ 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. - gevraagd om verbinding te maken + 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. - \ No newline at end of file + 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 + Korte link + Serveroperators configureren + Privacybeleid en gebruiksvoorwaarden. + Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders. + contact verwijderd + Chat met beheerders + Chats met leden + lid heeft oude versie + Controleer de leden voordat u ze toelaat (knocking). + Gebruik TCP-poort 443 alleen voor vooraf ingestelde servers. + Fout bij het accepteren van lid + 1 chat met een lid + %d chat(s) + %d chats met leden + %d berichten + geaccepteerde %1$s + beoordeling + Chat met lid + Accepteren als lid + Accepteren als waarnemer + beoordeeld door beheerders + Korte link toevoegen + Nieuw lid wil zich bij de groep aansluiten. + Chat met beheerders + Chat verwijderen + Chat met lid verwijderen? + Geen chats met leden + Afwijzen + Lid afwijzen? + kan geen berichten versturen + verwijderd uit de groep + je bent weggegaan + Uit + alle + uit + Leden beoordelen + Fout bij het verwijderen van chat met lid + Rapport verzonden naar moderators + Je kunt geen berichten versturen! + U kunt uw rapporten bekijken in Chat met beheerders. + contact uitgeschakeld + contact niet klaar + groep is verwijderd + niet gesynchroniseerd + verzoek tot toetreding afgewezen + Alle servers + Vooraf ingestelde servers + Toegangsinstellingen opslaan? + heb je geaccepteerd + Wacht totdat de moderators van de groep uw verzoek tot lidmaatschap van de groep hebben beoordeeld. + je hebt dit lid geaccepteerd + in afwachting van beoordeling + Toegang voor leden instellen + Toelating van leden + Accepteer + Lid accepteren + Lid zal toetreden tot de groep, lid accepteren? + Incognitoprofiel gebruiken + Open chat + Open een nieuwe chat + Nieuwe groep openen + geen abonnement + U bent niet verbonden met de server die u gebruikt om berichten van deze verbinding te ontvangen (geen abonnement). + end-to-end-encryptie.]]> + 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 a748bf2741..0b59cc1b06 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -26,7 +26,7 @@ wysyłanie plików nie jest jeszcze obsługiwane odbieranie plików nie jest jeszcze obsługiwane Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu. - Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %1$s). + Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %1$s). nieznany format wiadomości SimpleX Ty @@ -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! @@ -418,7 +418,7 @@ odrzucone połączenie sekret uruchamianie… - strajk + przekreślenie Brak identyfikatorów użytkownika. Następna generacja \nprywatnych wiadomości oczekiwanie na odpowiedź… @@ -641,7 +641,7 @@ Zaproszenie do grupy wygasło zaktualizowano profil grupy zaproszony przez Twój link grupy - zaproszony %1$s + zaprosił %1$s opuścił opuścił członek @@ -1129,9 +1129,9 @@ 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ć. @@ -1373,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ć @@ -1499,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. @@ -1523,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 @@ -1851,8 +1849,7 @@ 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ę podzielić się innymi problemami z deweloperami. + 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 @@ -1881,7 +1878,7 @@ Połączony Bieżący profil Otrzymane wiadomości - Odebranie wiadomości + Odbiór wiadomości Błąd resetowania statystyk duplikaty Zakończono @@ -1932,8 +1929,7 @@ Wyświetlanie informacji dla Statystyki Sesje transportowe - Zaczynanie od %s. -\nWszystkie dane są prywatne na Twoim urządzeniu. + 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. @@ -2114,4 +2110,83 @@ Usuń lub moderuj do 200 wiadomości. Przekazywanie do 20 wiadomości jednocześnie. Przełącz profil czatu dla zaproszeń jednorazowych. - \ No newline at end of file + 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. + Zmień listę + 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. + przyjął %1$s + przyjął cię + Nowy członek chce dołączyć do grupy. + 1 rok + Akceptuj + 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 3b139013fc..c0bbe4d6bb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -87,7 +87,7 @@ Aceitar solicitações de contato automaticamente Aparência O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis. - Uma conexão TCP separada (e credencial SOCKS) será usada para cada contato e membro do grupo. + 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 aplicativo procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]> chamda encerrada %1$s @@ -323,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 @@ -341,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 @@ -360,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 @@ -377,7 +377,7 @@ 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 @@ -391,7 +391,7 @@ 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. @@ -406,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 @@ -534,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! @@ -552,7 +552,7 @@ 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 @@ -587,7 +587,7 @@ Servidores SMP Endereço do servidor pré-definido 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 @@ -641,7 +641,7 @@ \nAtenção: você não será capaz de se conectar aos servidores sem um endereço .onion 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 @@ -739,7 +739,7 @@ Chamada perdida Salvar senha na Keystore Seu banco de dados de bate-papo não está criptografado - defina uma senha para protegê-lo. - convite para o grupo%1$s + convite para o grupo %1$s Você esta usando um perfil anônimo para este grupo - para evitar compartilhar seu perfil principal, convidar contatos não é permitido Confirmação de migração inválida Migrações: %s @@ -849,7 +849,7 @@ Para começar um novo bate-papo Ligar Bem-vindo(a)! - A próxima geração \nde 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. @@ -862,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 @@ -971,7 +971,7 @@ Toque para ativar o perfil. Mostrar perfil de chat Mostrar perfil - Tentando se conectar ao servidor utilizado para receber mensagens deste contato (erro:%1$s). + Tentando se conectar ao servidor utilizado para receber mensagens deste contato (erro:%1$s). Tentando se conectar ao servidor utilizado para receber mensagens deste contato. Você está conectado ao servidor usado para receber mensagens desse contato. Seu servidor @@ -1061,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. @@ -1149,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. @@ -1171,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 @@ -1199,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 @@ -1249,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 @@ -1265,7 +1265,7 @@ 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 @@ -1445,7 +1445,7 @@ Modo anônimo simplificado Desktop encontrado Aleatório - Migração do banco de dados em progresso. + Migração do banco de dados em progresso. \nIsso pode levar alguns minutos. %1$d mensagens moderadas por %2$s %d mensagens bloqueadas @@ -1800,7 +1800,7 @@ 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 do grupo podem enviar link SimpleX + Membros podem enviar links SimpleX. Importando arquivo Modo claro Ativado para @@ -1897,7 +1897,7 @@ Escala Preencher Ajustar - Links SimpleX são proibidos neste grupo. + Links SimpleX são proibidos. Migrar para outro dispositivo via QR code. Chamadas picture-in-picture Use o aplicativo enquanto está em chamada. @@ -1982,8 +1982,7 @@ Sessões de transporte Recepção de mensagem Pendente - Começando de %s. -\nTodos os dados são privados do seu dispositivo. + Começando em %s.\nTodos os dados são mantidos privados em seu dispositivo. Total Servidores proxiados Servidores conectados anteriormente @@ -2056,7 +2055,7 @@ 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. - Barra de ferramentas de conversa acessível + 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 @@ -2066,15 +2065,15 @@ 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 erro de arquivo. + %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 arquivo(s) ainda estão sendo baixados. - %1$d arquivos foram excluidos. - %1$d arquivos não foram baixados. + %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. @@ -2094,4 +2093,305 @@ Senha Nome de usuário Sessão do aplicativo - \ No newline at end of file + 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! + Sair 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. + Aceitar como membro + Adicionar link curto + 1 conversa com um membro + Todos servidores + %1$s aceito + aceitou você + Aceitar + não pode enviar mensagens + Aceitar como observador + Aceitar membro + todos + Conversas com membros + 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 c9db7de2e6..5f12e762aa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -268,7 +268,7 @@ O contacto permite Preferências de conversa SimpleX - m + mil Conectar através do endereço de contacto? Conectar via link de convite? Conectar através da ligação do grupo\? @@ -971,4 +971,9 @@ Todos os perfis Já conectando! Já entrando no grupo! - \ No newline at end of file + Não foi possível descarregar %1$d ficheiros(s). + %1$d erro(s) nos ficheiros:\n%2$s + Ainda a descarregar %1$d ficheiros(s). + %1$d ficheiros(s) eliminado(s). + Não foi possível descarregar %1$d ficheiro(s). + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index c02e17f568..2f19492237 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -1,74 +1,74 @@ %1$d mesaje moderate de %2$s - Întrerupi schimbarea adresei? + Anulezi schimbarea adresei? 30 secunde Acceptă Acceptă incognito Adaugă server - Setări de rețea avansate + Setări avansate de rețea %1$s dorește să se conecteze cu tine prin Acceptă %1$d mesaje omise. 1 lună 1 săptămână administrator - Accent adițional - Adițional secundar + Accent suplimentar + Suplimentar secundar Acceptă - 6 limbi noi pentru interfață + 6 noi limbi de interfață %1$d mesaje nu au putut fi decriptate. %1$d mesaj(e) omis(e) %1$s MEMBRI 1 zi 1 minut - link de unică folosință + Link unic 5 minute Despre SimpleX Despre adresa SimpleX Despre SimpleX Chat - deasupra, apoi: + mai sus, apoi: Accent Acceptă - Accepți cererea de conexiune? + Accepți cererea de conectare? Adaugă contact apel acceptat - Adaugă servere prestabilite + Adaugă servere presetate Adaugă profil Adresă - 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… - Administratorii pot crea linkuri de participare la grupuri. - Câteva lucruri în plus + Adaugă servere prin scanarea codurilor QR. + Adaugă pe alt dispozitiv + Adaugă mesaj de bun venit + se agreează criptarea… + se agreează criptarea pentru %s… + Administratorii pot crea linkurile pentru a se alătura grupurilor. + Încă câteva lucruri Toate datele aplicației sunt șterse. - Schimbarea de adresă va fi întreruptă. Se va folosi vechea adresa de primire. - Adaugă adresa la profil, astfel încât contactele să o poată partaja cu alte persoane. Profilul reînoit va fi comunicat contactelor. - Accesezi serverele prin proxy SOCKS cu portul %d? Proxyul trebuie pornit înaintea activării acestei opțiuni. - Î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ă! + Schimbarea adresei va fi anulată. Vechea adresă de primire va fi folosită. + Adaugă o adresă la profilul tău, pentru ca persoanele din lista ta de contacte să o poată partaja cu alții. Actualizarea profilului va fi trimisă contactelor tale. + Accesați serverele via proxy SOCKS pe portul %d? Proxy-ul trebuie pornit înainte de a activa această opțiune. + Anulează schimbarea adresei + Anulează + Toate conversațiile și mesajele vor fi șterse – această acțiune nu poate fi anulată! + Toate mesajele vor fi șterse - această acțiune 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 ștergerea definitivă a mesajelor trimise. (24 de ore) Permite trimiterea de fișiere și media. - Toate datele sunt șterse când este introdusă. + Toate datele sunt șterse la introducerea lor. Toți membrii grupului vor rămâne conectați. - Toate mesajele vor fi șterse - operațiunea este definitivă! Mesajele vor fi șterse DOAR pentru tine. + Toate mesajele vor fi șterse - această acțiune nu poate fi anulată! Mesajele vor fi șterse NUMAI pentru tine. Toate mesajele noi de la %s vor fi ascunse! Permite Permite Permite apeluri doar dacă le permite contactul tău. - Permiți mesaje vocale? - Permite mesajele vocale numai dacă le permite și contactul tău. - Permite ștergerea mesajelor ireversibile doar dacă și contactul tău îți permite ție. (24 ore) + Permiteți mesaje vocale? + Permite mesaje vocale doar dacă contactul tău le permite. + Permite ștergerea definitivă a mesajelor doar dacă și contactul tău o permite. (24 de ore) Permite reacții la mesaje. Permite trimiterea de mesaje directe membrilor. Permite trimiterea de mesaje efemere. Permite trimiterea de mesaje vocale. - Permite mesaje efemere doar dacă le permite contactul tău. + Permite mesaje autodistructive doar dacă și contactul tău le permite. Permite contactelor tale să trimită mesaje care dispar. Aspect Versiunea aplicației @@ -88,55 +88,54 @@ Camera și microfon Atenție: arhiva va fi ștearsă.]]> Anulează previzualizarea fișierului - Un profil de conversație gol cu numele furnizat este creat, și aplicația se deschide ca de obicei. + Un profil de conversație gol cu numele furnizat este creat, iar aplicația se deschide ca de obicei. Se conectează deja! Aplicația poate primi notificări doar când rulează, niciun serviciu în fundal nu va fi lansat. Se alătură deja grupului! Mereu pornit Un nou profil aleatoriu va fi distribuit. - Android Keystore este folosit pentru a stoca în siguranță fraza de acces - permite serviciului de notificare să funcționeze. + Android Keystore este folosit pentru a stoca în siguranță parola. Acest lucru permite funcționarea serviciului de notificări. și %d alte evenimente Răspunde la apel - Android Keystore va fi folosit pentru a stoca în siguranță fraza de acces după ce repornești aplicația sau schimbi fraza de acces - va permite primirea notificărilor. + Android Keystore va fi folosit pentru a stoca în siguranță parola după ce repornești aplicația sau schimbi parola — acest lucru va permite primirea de notificări. APLICAȚIE Creează grup Apeluri audio și video Arhivează și încarcă Bază de date de arhivare Se creează un link de arhivare - Creează un link de invitare unic. + Creează link de invitație de unică folosință Acceptă automat imagini Apel audio - Apel audio + apel audio Audio oprit PICTOGRAMĂ APLICAȚIE Cod de acces aplicație Creează grup secret Creează coadă - Serviciul în fundal rulează mereu - notificările vor fi afișate imediat ce sunt disponibile. + Serviciul de fundal rulează permanent – notificările vor fi afișate imediat ce mesajele sunt disponibile. Autentificare Autentificare eșuată - Codul de acces curent + Codul de acces actual Autentificare indisponibilă Atașează Înapoi Creează grup secret - Se creează link… + Se creează linkul… apel audio (necriptat 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ă Cod de acces aplicație este înlocuit cu cod de acces de autodistrugere. Apeluri audio/video - " -\nDisponibil în v5.1" + \nDisponibil în v5.1 Cod de acces aplicație Migrare date aplicație Eroare critică Aplică Creează profil Creează profil - Acceptă automat cererile de contactare + Acceptare automată a cererilor de contact personalizat În prezent dimensiunea maximă pentru fișiere este %1$s. Creează adresă SimpleX @@ -144,22 +143,22 @@ Creează-ți profilul Apeluri audio și video Audio pornit - Frază de acces curentă… - Creează link pentru grup + Parola actuală… + Creează link de grup Creează link 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. (actual) - Poză de profil eliminată + fotografia de profil a fost eliminată Temă întunecată eliminat Întunecată - Adresă de contact eliminată + adresa de contact eliminată Personalizează și distribuie teme colorate. Teme personalizate Repetă descărcarea @@ -170,174 +169,174 @@ Elimină Elimină Elimină membru - Elimini membrul? - Resetează la implicit - Resetează culoarea - Resetează culorile - Elimină imagine + Eliminați membrul? + Resetează la valorile implicite + Resetezi culoarea + Resetați culorile + Elimină imaginea Repetă încărcarea apel respins - Elimini fraza de acces din Keystore? - Elimini fraza de acces din setări? + Elimini parola din Keystore? + Elimini parola din setări? Repetă cererea de conectare? Necesar Reîncearcă - Primește fișiere în siguranță + Primiți fișiere în siguranță Grupuri mai sigure - Restabilește + Restaurați Reîmprospătează - Revoci fișierul? + Revocați fișierul? Revocă Renegociezi criptarea? - Resetează + Resetare Respinge - Salvează fraza de acces în setări + Salvează parola în setări Salvează și actualizează profilul grupului Repetă cererea de alăturare? - Repornește conversația + Reporniți conversația salvat - Salvat de la %s + salvat de la %s Salvează Salvat - Salvat de la + Salvat din Renegociază - Salvează servere - Servere WebRTC ICE salvate vor fi eliminate. + Salvează serverele + Serverele ICE WebRTC salvate vor fi eliminate. Salvează parola profilului - Salvează fraza de acces și deschide conversația + Salvează parola și deschide conversația %s și %s Renegociază criptarea Salvează și notifică contactul Salvează și notifică contactele Salvează și notifică membrii grupului - Rulează când aplicația este pornită + Rulează când aplicația este deschisă Răspunde Revocă fișierul Salvează Salvezi setările? - Salvezi preferințe? + Salvezi preferințele? 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 + Restaurează backupul bazei de date + Restaurezi backupul bazei de date? + Eroare de restaurare a bazei de date %1$s eliminat %s și %s conectați Rol Salvează Arată Respinge - Salvezi servere? + Salvezi serverele? Apel respins Mesaj salvat - Repornește aplicația pentru a crea un nou profil - Salvează fraza de acces în Keystore + Repornește aplicația pentru a crea un profil de conversație nou. + Salvează parola în Keystore Salvează profilul grupului Repetă - Trimite previzualizări ale link-ului - Setează frază de acces - Distribuie adresă + Trimite previzualizări de linkuri + Setează parola + Partajează adresa Trimis la Secundar Mesaj trimis - Setează preferințele grupului + Setați preferințele grupului trimis Adresa serverului este incompatibilă cu setările de rețea. Versiunea serverului este incompatibilă cu setările de rețea. - Trimițând prin - trimiterea de fișiere nu este acceptată încă - Scanează codul de securitate din aplicația contactului tău - Selectează contacte + Trimitere prin + trimiterea de fișiere nu este încă acceptată + Scanează codul de securitate din aplicația persoanei tale de contact. + Selectează contactele 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ă + Partajează media… + Partajează mesaj… + Afișează lista de conversații într-o fereastră nouă + Afișează consola în fereastră nouă + Configurează parola bazei de date + Setează parola bazei de date + setați adresă de contact nouă %s (actual) - Cod de sesiune + Codul sesiunii Expeditorul a anulat transferul de fișiere. - Serverul necesită autorizație pentru a crea cozi, verifică parola - Distribuie + Serverul necesită autorizare pentru a crea cozi. Verifică parola + Partajează trimitere eșuată - Caută sau lipește link SimpleX - Setează numele de contact + Caută sau lipește linkul SimpleX + Setează numele contactului Salvezi mesajul de bun venit? - Evaluare de securitate - Scanează cod QR de pe desktop + Evaluarea securității + Scanează codul 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. + Trimiteți un mesaj live - acesta se va actualiza pentru destinatar(i) pe măsură ce îl tastați + Partajează link + Trimiteți până la 100 de mesaje recente noilor membri. + Bara de căutare acceptă linkuri de invitație. secunde Scanează de pe mobil Mesaj trimis Scanează cod QR Trimite Trimite întrebări și idei - Arată opțiuni dezvoltator + Afișează opțiunile pentru dezvoltatori 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… + Mesajele trimise vor fi șterse după ora setată. + Serverul necesită autorizare pentru a încărca fișiere. Verifică parola + Afișează contactul și mesajul + Partajează fișier… + Setează numele contactului… Trimite mesaj - Trimite mesaj temporar + Trimiteți un mesaj care dispare (scanează sau lipește din clipboard) - Arată cod QR - Test server eșuat! - Arată: - Arată erori interne + Afișează codul QR + Testul serverului a eșuat! + Afișează: + Afișează erorile interne secret SETĂRI %s conectat - setează imagine de profil - Trimis către: %s + setați o nouă poză de profil + Trimis la: %s SERVERE - Trimite mesaj live + Trimite mesaj în direct %s descărcat - Distribui adresa cu contactele? - Arată previzualizare - trimite mesaj direct - Trimite mesaj direct pentru a te conecta + Partajați adresa cu contactele? + Afișează previzualizare + trimite pentru a te conecta + Trimiteți mesaj direct pentru a vă conecta Selectează - Trimiterea de fișiere va fi oprită. + Trimiterea fișierului va fi oprită. Trimite Setări - Scanează cod + Scanează codul Cod de securitate - Trimite-ne email - Scanează codul QR al serverului - Distribuie contactelor - Arată - cod de securitate schimbat - Arată ultimul mesaj - Trimite mesaj direct + Trimiteți-ne un e-mail + Scanați codul QR al serverului + Partajează cu contactele + Afișează + codul de securitate a fost schimbat + Afișează ultimele mesaje + Trimiteți un 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 + SimpleX nu poate rula în fundal. Vei primi notificări doar atunci când aplicația rulează. + Serviciul 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. + Oprire + Linkuri SimpleX + Linkurile SimpleX sunt interzise. + Securitatea SimpleX Chat a fost auditată de Trail of Bits. Mesaje SimpleX Chat Apeluri SimpleX Chat Adresă SimpleX simplexmq: v%s (%2s) - Link-uri SimpleX nepermise + Linkurile SimpleX nu sunt permise Echipa SimpleX - Închizi? + Opriți? Adresă de contact SimpleX - Link de grup SimpleX - Link-uri SimpleX - Invitație unică SimpleX - Siglă SimpleX + Link pentru grup SimpleX + Linkuri SimpleX + Invitație de unică folosință SimpleX + Logo SimpleX Grupuri mici (max 20) Mod incognito simplificat Pătrat, cerc, sau orice între. @@ -352,335 +351,332 @@ Niște servere au eșuat testul: Difuzor oprit Difuzor pornit - Copie de rezervă a datelor aplicației + Backup al datelor aplicației Tema aplicației Accent suplimentar 2 Începe conversația Toate modurile de culoare Folosește mereu releu - Aplică pentru + Aplică la Începe o nouă conversație - Stea pe GitHub - Criptare de la capăt la capăt standard + Acordă o stea pe GitHub + criptare standard end-to-end 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 + %s secundă(e) Începi conversația? Setări avansate - Adresă desktop rea - ID de mesaj incorect - Hash de mesaj incorect - ID de mesaj incorect - Hash de mesaj incorect + Adresă desktop incorectă + ID mesaj incorect + hash mesaj incorect + ID mesaj incorect + Hash 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 + v-ați schimbat rolul în %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.]]> + Migrează de pe alt dispozitiv pe noul dispozitiv și scanați codul QR.]]> Te conectezi prin adresa de contact? - Te conectezi printr-un link unic? + Vrei să te conectezi printr-un link unic? Conectare incognito Contactul deja există Schimbă codul de acces - Poți porni Blocare SimpleX din Setări. + Puteți activa SimpleX Lock din Setă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). + Persoana ta de contact trebuie să fie online pentru ca procesul de conectare să se finalizeze.\nPoți anula această conexiune și șterge contactul (apoi poți încerca mai târziu cu un link nou). eroare apel Apelurile tale Schimbă Schimbă rolul Contactul permite - Grupuri mai bune + Grupuri îmbunătățite 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. + Conversează cu dezvoltatorii + Trebuie să introduci parola de fiecare dată când pornești aplicația - aceasta nu este stocată pe dispozitiv. blocat Preferințe conversație Mod întunecat Interfață chineză și spaniolă - Crează un grup folosind un profil aleatoriu. + Creează un grup folosind un profil aleatoriu. Blochează membrii grupului Mobil conectat Conectat la desktop Anulează migrarea Celular - Nu poți trimite mesaje! + ești observator Schimbi adresa de primire? Contactul nu este conectat încă! Conectare prin link - Poți vedea linkul de invitație din nou în detaliile conexiunii. + Puteți vedea din nou linkul de invitație în detaliile conexiunii. Profilurile tale de conversație - Crează profil de conversație - apel terminat %1$s - Crează + Creează un profil de conversație + apel încheiat %1$s + Creează Bluetooth Camera - Apeluri pe ecranul de blocare: + Apeluri pe ecranul blocat: contactul are criptare e2e contactul nu are criptare e2e Contacte CONVERSAȚII BAZĂ DE DATE CONVERSAȚIE - Baza de date a conversației ștearsă + Baza de date a conversației a fost ștearsă Conversația rulează - Baza ta de date a conversațiilor - Baza ta de date a conversațiilor nu este criptată - setează frază de acces pentru a o proteja. - Fraza de acces de criptare a bazei de date va fi actualizată și stocată în setări. - ai schimbat rolul %s la %s + Baza de date a conversațiilor tale + Baza de date a conversațiilor tale nu este criptată - setează o parolă pentru a o proteja. + Parola pentru criptarea bazei de date va fi actualizată și stocată în setări. + ați schimbat rolul %s în %s Nu se pot invita contactele! - Te-ai alăturat grupului + Te-ai alăturat acestui grup 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 + Profilul tău de chat va fi trimis membrilor grupului Întunecat Culori mod întunecat - Și tu și contactul tău puteți face apeluri. + Atât tu, cât și contactul tău puteți efectua apeluri. %s anulat Conectat la mobil Eroare copiere Te conectezi prin link? - Nu ai putut fi verificat(ă); te rog încearcă din nou. + Nu ați putut fi verificat; vă rugăm să încercați din nou. Copiază Anulează mesajul live Conectare prin link / cod QR Contactele tale vor rămâne conectate. - Fraza de acces de criptare a bazei de date va fi actualizată. + Parola pentru criptarea 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 + Trebuie să îi permiți contactului tău să trimită mesaje vocale pentru a le putea trimite. + Versiunea de 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 + ați schimbat adresa + ați schimbat adresa pentru %s se schimbă adresa… se schimbă adresa pentru %s… - Și tu și contactul tău puteți trimite mesaje temporare. + Atât tu, cât și contactul tău puteți trimite mesaje autodistructive. Nu se pot primi fișiere - Poate fi dezactivat în setări – notificările vor fi încă afișate cât timp aplicația rulează.]]> + Poate fi dezactivat din setă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 + Nu aveți conversații Contactul și toate mesajele vor fi șterse - acest lucru nu poate fi anulat! - Copiat în clipboard + Copiat Camera - Ai invitat un contact - Profilul tău de conversație va fi trimis -\ncontactului tău + Ați invitat un contact + Profilul tău de conversație va fi trimis contactului tău. Contribuie Profil conversație aldin - Poți folosi markdown pentru a formata mesaje: - Apelând… + Puteți folosi marcarea pentru a formata mesajele: + 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. + Baza de date a conversației a fost importată + Trebuie să utilizați cea mai recentă versiune a bazei de date de chat DOAR pe un singur dispozitiv, altfel este posibil să nu mai primiți mesaje de la unele contacte. Nu se poate accesa Keystore pentru a salva parola bazei de date - Conversație migrată! + Conversația a fost migrată! Continuă apel în curs Apel în curs - Și tu și contactul tău puteți trimite mesaje vocale. + Atât tu, cât și contactul tău puteți trimite mesaje vocale. Contact ascuns: Nume contact - Crează adresă + Creează adresă Bază de date criptată! - Schimbi fraza de acces a bazei de date? + Schimbi parola bazei de date? Conversația este oprită - Poți porni conversația prin Setările aplicației / Bază de date sau repornind aplicația. - ai ieșit - Blochezi membrul? + Puteți porni discuția din Setările aplicației / Baza de date sau repornind aplicația. + ați ieșit + Blocați membrul? Blocat de admin - Și tu și contactul tău puteți adăuga reacții la mesaje. - Mesaje mai bune + Atât tu, cât și contactul tău puteți adăuga reacții la mesaje. + Mesaje îmbunătățite 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 + Contactul tău a trimis un fișier care este mai mare decât dimensiunea maximă acceptată în prezent (%1$s). + anulează previzualizarea linkului Verifică adresa serverului și încearcă din nou. - Tu îți controlezi conversația! - Apel deja terminat! - Apel terminat + Tu îți controlezi chatul! + Apelul s-a încheiat deja! + Apel încheiat Blochează pentru toți - Ai cerut deja conexiunea prin această adresă! + Ați solicitat deja conexiunea prin această adresă! Te conectezi la tine? Verifică conexiunea la internet și încearcă din nou Culori conversație - Temă conversație - Bun pentru baterie. -\nServiciul în fundal verifică mesaje la fiecare 10 minute. Ai putea rata apeluri sau mesaje urgente. + Tema conversaț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! + Blocați membrul pentru toți? + Atât tu, cât și contactul tău puteți șterge definitiv mesajele trimise. (24 de ore) + Folosește mai multă baterie! \nServiciul în fundal rulează mereu – notificările sunt afișate imediat ce mesajele sunt disponibile. Nu se poate trimite mesajul - Ștergeți + Șterge Confirmați fișiere de la servere necunoscute. - schimbat adresa pentru dumneavoastră - Confirmați parola nouă… + am schimbat adresa pentru tine + Confirmă noua parolă… Conectare conexiune %1$d conexiune stabilită Eroare de conexiune Conexiune expirată - se conectează + 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]]> + pentru fiecare profil de conversație pe care îl ai în aplicație.]]> Permiteți downgrade-ul - pentru fiecare contact și membru de grup \nVa rugăm considerați că: dacă aveți prea multe conexiuni, consumul dumneavoastră de baterie și trafic de internet pot fi considerabil mai mari, iar unele conexiuni pot eșua.]]> + pentru fiecare contact și membru al grupului.\nRețineți: dacă aveți multe conexiuni, consumul bateriei și al traficului de date poate crește considerabil, iar unele conexiuni pot eșua.]]> Conexiune terminată Se conectează la desktop - Rugat să primească imaginea + S-a solicitat primirea imaginii Cerere de conexiune trimisă! - Vă rugăm să rețineți: mesajele și releurile pentru fișiere sunt conectate printr-un proxy SOCKS. Apelurile ți trimiterea de previzualizări a link-urilor folosesc o conexiune directă.]]> + 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" + a schimbat rolul lui %s în %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ă. + După profilul de conversație (implicit) sau după conexiune (BETA). + Compară codurile de securitate cu contactele tale. Conexiune oprită Conexiune oprită Conectare la desktop Conexiunea la desktop este într-o stare proastă Conectare Conexiune - Ștergeți notițele private? + Ștergeți notele private? Conectare automată se conectează apelul… - se conectează (invetație la introducere) + se conectează (invitație de introducere) se conectează… %s este într-o stare proastă]]> - Confirmați setările de rețea + Confirmă setările de rețea se conectează… Eroare de conexiune (AUTENTIFICARE) - Optimizarea pentru baterie este activă, vom opri serviciile din fundal și cererile periodice pentru mesaje noi. Le puteți reactiva din setări. - conectat - Crează un grup: pentru a crea un nou grup.]]> + Optimizarea bateriei este activă, dezactivând serviciul de fundal și cererile periodice pentru mesaje noi. Le puteți reactiva din setări. + Conectat + Creează grup:: pentru a crea un nou grup.]]> Ștergeți conversația? - Ștergeți - Ștergeți conversația + Șterge + Șterge conversația Conectare Consolă conversație - Confirmați parola + Confirmă parola colorat - Toate contactele, conversațiile și fișierele dumneavoastră vor fi encriptate într-un mod sigur și încărcate pe bucăți pe releurile XFTP configurate. - Rugat să primească videoclipul + Toate contactele, conversațiile și fișierele tale vor fi criptate securizat și încărcate în fragmente către releele XFTP configurate. + S-a solicitat primirea videoclipului Comparați fișierul Vă rugăm să rețineți: nu veți putea recupera sau schimba parola dacă o veți pierde.]]> - schimbat rolul dumneavoastră la %s + rolul tău a fost schimbat în %s conectat se conectează - conectat + Conectat Ștergeți Buton de închidere Ștergeți verificarea Configurare servere ICE - conectat direct + conexiune solicitată se conectează (anunțat) Cel mai bun pentru baterie. Veți primi notificări doar când aplicația rulează (FĂRĂ servicii de fundal).]]> Se conectează apelul complet - Vă rugăm să rețineți: folosind aceeași bază de date pe două dispozitive, va intrerupe decripția mesajelor de la conexiunile dumneavoastră, ca protecție de securitate.]]> + 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. + Confirmă că-ți amintești parola bazei de date pentru a o migra. Conexiune Ștergi profilul de conversație? Șterge pentru mine %dd șters - Șterge contact + Șterge contactul Șters la Versiunea aplicației desktop %s nu este compatibilă cu această aplicație. - Ștergi mesajul membrului? - Șterge grup + Ștergeți mesajul membrului? + Șterge grupul Șterge și notifică contactele - Decentralizat + Descentralizat grup șters %d contact(e) selectat(e) Șters la: %s Ștergi profilul de conversație? - Șterge profil + Șterge profilul implicit (%s) Confirmări de livrare! Confirmările de livrare sunt dezactivate! Adresă desktop Dispozitive desktop Desktop - Eroare decriptare - Șterge imagine + Eroare de decriptare + Șterge imaginea Șterge după Șterge pentru toată lumea Descriere - Șterge fișier - Șterge coadă + Șterge fișierul + Șterge coada Ștergi contactul? Șterge Șterge - Ștergi fișiere și media? + Ștergi fișierele și conținutul media? %d zile %d zi Șterge adresa - Șterge mesaje - Ștergi %d mesaje? + Șterge mesajele + Ștergeți %d mesaje? Ștergi adresa? Șterge toate fișierele - Șterge link - Ștergi link? + Șterge linkul + Ștergi linkul? zile Șterge - Ștergi mesajul? + Ștergeți mesajul? Livrare Ștergi conexiunea în așteptare? - Șterge server + Ștergeți serverul Ș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 + Șterge profilul de conversație + Șterge fișierele 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ă + Baza de date a conversației a fost exportată Verifică pentru actualizări - Contactul %1$s a schimbat la %2$s + contactul %1$s s-a schimbat în %2$s Contactul este șters. Controlează-ți rețeaua - Fraza de acces pentru criptarea bazei de date va fi actualizată și stocată în Keystore. - Fraza de acces a bazei de date + Parola pentru criptarea bazei de date va fi actualizată și stocată în Keystore. + Parola bazei de date Servere XFTP configurate - Baza de date este criptată folosind o expresie de acces aleatorie. Schimbați-o înainte de a exporta. + Baza de date este criptată folosind o parolă aleatorie. Trebuie schimbată înainte de exportare. apel conectare Contact șters! @@ -689,23 +685,22 @@ Verifică pentru actualizări Creează Estompează media - BAZĂ DE DATE CHAT + BAZĂ DE DATE CONVERSAȚIE Conectează-te cu prietenii mai ușor. încercări - Completat + Finalizat 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 + Estompează pentru o mai bună confidențialitate. + Statusul conexiunii și al serverelor. Eroare bază de date - Contactele pot marca mesajele pentru ștergere; tu le vei putea vedea. + Contactele pot marca mesajele pentru ștergere; tu le vei putea vizualiza. 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 + Migrarea bazei de date este în curs.\nAceasta poate dura câteva minute. + Parola și exportul bazei de date Toate profilurile Profil actual Confirmi ștergerea contactului? @@ -714,10 +709,1793 @@ Conectat Corectează numele la %s? Continuă - Baza de date este criptată folosind o expresie de acces aleatorie, o poți schimba + Baza de date este criptată 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, te rog așteaptă sau verifică mai târziu! - \ No newline at end of file + Se conectează la contact, vă rugăm să așteptați sau să verificați mai târziu! + Setări avansate + Setări + Setări + apel audio criptat e2e + apel video criptat e2e + DISPOZITIV + EXPERIMENTAL + Criptează + erori de decriptare + TU + nicio criptare e2e + criptat e2e + Apel video primit + Dezactivează + Permiți apeluri? + încheiat + Sunete la apel + Apeluri îmbunătățite + Apel audio primit + Încheie apelul + marcat ca șters + %d mesaje marcate șterse + moderat de %s + %1$d alte erori de fișier. + %1$s mesaje nu au fost redirecționate + ai partajat linkul de unică folosință + ai partajat linkul de unică folosință incognito + prin link de grup + incognito prin link de grup + Prin browser + Spam + Conținut inadecvat + Încălcarea liniilor directoare ale comunității + Profil inadecvat + Alt motiv + Eroare la salvarea serverelor SMP + Asigurați-vă că adresele serverelor XFTP sunt în format corect, separate pe linii și nu sunt duplicate. + Eroare la încărcarea serverlor XFTP + Eroare la actualizarea configurației de rețea + Nu s-au putut încărca conversațiile + 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 + incognito prin linkul adresei de contact + prin link de unică folosință + 1 raport + 1 an + redirecționat + eroare la afișarea mesajului + eroare la afișarea conținutului + criptare end-to-end, cu confidențialitate perfectă în avans, nerepudiere și recuperare în caz de compromitere.]]> + criptare end-to-end rezistentă la computere cuantice, cu confidențialitate perfectă în avans, nerepudiere și recuperare în caz de compromitere.]]> + Această conversație este protejată prin criptare integrală. + Link complet + incognito printr-un link de unică folosință + a + b + prin %1$s + Eroare la încărcarea serverlor SMP + %1$d fișier(e) au fost șterse. + raport arhivat de %s + Invitație acceptată + Asigurați-vă că adresele serverelor SMP sunt în format corect, separate pe linii și nu sunt duplicate. + Eroare la salvarea serverelor XFTP + %1$d fișier(e) sunt încă în curs de descărcare. + %1$d fișier(e) nu au putut fi descărcate. + %1$d fișier(e) nu au fost descărcate. + prin linkul adresei de contact + Deschiderea linkului în browser poate reduce confidențialitatea și securitatea conexiunii. Linkurile SimpleX nesigure vor fi afișate cu roșu. + solicitat să se conecteze + Conversația nu a putut fi încărcată + 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 de mesaj nevalid + LIVE + moderat + conversație nevalidă + date nevalide + Această conversație este protejată prin criptare end-to-end rezistentă la atacuri cuantice. + Note private + Eroare la citirea parolei bazei de date + Versiunea bazei de date este incompatibilă + Invitația la grup nu mai este validă, a fost eliminată de expeditor. + Se alătură grupului + Părăsește grupul + Eroare la ștergerea linkului de grup + Eroare la trimiterea invitației + dezactivat + Membrii pot trimite mesaje vocale. + Filtrează conversațiile necitite și favorite. + Migrare în curs + Managementul rețelei + Finalizează migrarea pe un alt dispozitiv. + Incompatibil! + Cum să utilizezi serverele tale + Importă baza de date a conversației? + Ștergeți raportul + Redirecționați mesajele… + Șterge doar conversația + Păstrezi invitația nefolosită? + Introdu numele tău: + Acordați permisiunea (permisiunile) de a efectua apeluri + Notificări și baterie + Deschide + Activați confirmarea de primire pentru grupuri? + PENTRU CONSOLĂ + Moderat la + Remediați conexiunea + Profiluri de conversație multiple + Ascuns + Introducere parolă + Redirecționare server: %1$s\nEroare:%2$s + Fișierul nu a fost găsit - cel mai probabil fișierul a fost șters sau anulat. + Adresă de server invalidă! + Dezactivat + Fișier: %s + Eroare la crearea linkului de grup + Dispare la + dezactivat + direct + Fișiere și media + Vrei să te alături grupului? + k + Se deschide baza de date… + Cale de fișier nevalidă + Nume afișat duplicat! + Moderare + Marchează ca necitit + Eroare la redirecționarea mesajelor + Sau scanați codul QR + Gazdă + Cum se folosește marcarea + Imun la spam + Baza de date va fi criptată. + Grup inactiv + Bună dimineaţa! + Fișierele și conținutul media sunt interzise. + moderatori + Max 40 de secunde, primit instantaneu. + Configurație îmbunătățită a serverului + Mesaj de bun venit pentru grup + Reducerea suplimentară a consumului de baterie + Alătură-te conversațiilor de grup + Livrarea mesajelor îmbunătățită + Interfață lituaniană + Fă-ți conversațiile să arate diferit! + Menționați membrii 👋 + Ajutați administratorii să modereze grupurile lor. + Doar un singur dispozitiv poate funcționa în același timp + Grupul exită deja! + Se descarcă arhiva + Finalizați migrarea + Eroare la verificarea parolei: + Eroare la importarea bazei de date a conversației + Mesaje + Eroare la exportarea bazei de date a conversației + Nicio informație, încercați să reîncărcați + Activează jurnalele + Autentificarea dispozitivului este dezactivată. Se dezactivează SimpleX Lock. + Invitația a expirat! + Grup + Invitați în grup + Deconectează dispozitivele mobile + Nume afișat nevalid! + Eroare la schimbarea profilului! + Fără servere media și de fișiere. + Nu există servere de mesaje. + Nu există servere pentru rutarea mesajelor private. + Nu există servere pentru a primi mesaje. + Pentru profilul de conversație %s: + Nu există servere pentru a primi fișiere. + Eroare la conectarea la serverul de redirecționare %1$s. Vă rugăm să încercați mai târziu. + Versiunea serverului de destinație %1$s este incompatibilă cu serverul de redirecționare %2$s. + Notificările instantanee sunt dezactivate! + Dezactivare notificări + Migrați de pe un alt dispozitiv + Eroare la ștergerea bazei de date a conversației + Actualizarea bazei de date + este necesară renegocierea criptării + membru + Ștergi conversația? + eroare + Link nevalid + Fișierul va fi primit după ce persoana de contact va finaliza încărcarea. + Interfață italiană + Desktopul este ocupat + Eroare la reconectarea serverului + Eroare la adăugarea membrului/membrilor + Eroare la alăturarea grupului + Ascunde + Listă + Nicio conversație necitită + Nicio conversație în lista %s. + Nimic selectat + deschis + AJUTOR + Doar contactul tău poate trimite mesaje care dispar. + Se importă arhiva + Migrare dispozitiv + nu + oprit` + %ds + Repară criptarea după restaurarea backupurilor. + Ștergeți sau moderați până la 200 de mesaje. + Primirea mesajelor + Eroare la resetarea statisticilor + Eroare la crearea mesajului + Redirecționați mesajul… + Eroare + Importă + Opțiuni de conectate desktop + Eroare la crearea raportului + Adresa de destinație a serverului %1$s este incompatibilă cu setările de redirecționare ale serverului %2$s . + Eroare la trimiterea mesajului + Eroare la schimbarea adresei + Eroare la sincronizarea conexiunii + Eroare + Deconectează + Execuția funcției durează prea mult: %1$d secunde: %2$s + Ascunde + Introduceți parola + Serviciu de notificare + Redirecționare server: %1$s\nEroare server destinatar: %2$s + Fișierul este blocat de operatorul serverului:\n%1$s. + Editează + Info + Redirecționat de la + Ștergi cele %d mesaje ale membrilor? + Rapoarte de membri + Fișierele și conținutul media sunt interzise! + Fișierul nu a fost găsit + Fișier + Operatori de rețea + Draft de mesaje + Parola a fost schimbată! + Activare (păstrați suprascrierile grupului) + Dezactivează (păstrează suprascrierile pentru grupuri) + Activează pentru toate grupurile + Versiunea bazei de date este mai nouă decât app, dar nu există migrare ulterioară pentru: %s + Alătură-te anonim + Deschide + Pentru rutare privată + Activați funcția TCP keep-alive + Fără sunet când este inactiv! + Teme noi pentru conversații + Descentralizarea rețelei + altul + Doar 10 imagini pot fi trimise simultan + Mesajul este prea mare! + Imaginea va fi primită atunci când persoana ta de contact este online. Te rugăm să aștepți sau să verifici mai târziu! + Șterge mesajele conversației de pe dispozitivul tău. + Dacă alegeți să respingeți, expeditorul NU va fi notificat. + Cum se utilizează + Marcare în mesaje + Eroare la salvarea proxy-ului + Dacă confirmi, serverele de mesagerie vor putea vedea adresa ta IP, iar furnizorul tău - la ce servere te conectezi. + Cum ajută la confidențialitate + mesaj duplicat + Invitați prietenii + Să vorbim în SimpleX Chat + Salut!\nConectează-te cu mine prin SimpleX Chat: %s + Pentru social media + Ascunde profilul + Numele afișat nu poate conține spații albe. + Nume nevalid! + Eroare la deschiderea browserului + Cum funcționează + Doar dispozitivele client stochează profiluri de utilizator, contacte, grupuri și mesaje. + Instalat cu succes + Dezactivează + Opțiuni dezvoltator + Notificările vor înceta să funcționeze până când nu redeschideți aplicația + Eroare la salvarea setărilor + Șterge mesajele după + %d sec + Email + Eroare la salvarea parolei utilizatorului + Eroare la salvarea bazei de date + Conectați aplicații mobile și desktop! 🔗 + Dispozitive + Fișierul exportat nu există + Sau distribuiți în siguranță acest link pentru fișier + Statusul mesajului + Niciun telefon conectat + Doar tu poți șterge definitiv mesajele (contactul tău le poate marca pentru ștergere). (24 de ore) + Doar contactul tău poate trimite mesaje vocale. + Găsește conversații mai rapid + Luminos + Nu + Deschizi linkul web? + MESAJE ȘI FIȘIERE + moderator + Rol inițial + Doar proprietarii grupului pot modifica preferințele grupului. + Introdu mesajul de bun venit… + Doar contactul tău poate adăuga reacții la mesaje. + Erori la ștergere + Dezactivați ștergerea mesajelor + Păstrează conversația + Cod QR nevalid + Cod de securitate incorect! + Dispare la: %s + Conectează-te cu datele tale de autentificare + Deschide conversația + Eroare la keychain + Imediat + %d minute + Probleme de rețea - mesajul a expirat după multe încercări de a -l trimite. + Nicio informație despre livrare + Mesajul poate fi transmis mai târziu dacă membrul devine activ. + editat + Se încarcă conversațiile… + Nicio conversație filtrată + Server nou + Asigurați -vă că adresele Serverului WebRTC ICE sunt în format corect, separate pe linii și nu sunt duplicate. + Rutarea mesajelor + Editează imaginea + Deschide setările + Acordare în setări + Instant + Cum afectează bateria + Activare (păstrați suprascrierile) + Activează codul de autodistrugere + MEMBRU + Operator de rețea + Desktop-uri conectate + Eroare la acceptarea membrului + Mesaj text + mesaj nou + Ascunde contactul și mesajul + Cerere de contact nouă + Deschide consola conversației + Autentificarea dispozitivului nu este activată. Puteți activa SimpleX Lock din Setări, după ce activați autentificarea dispozitivului. + Deschideți freastra de migrare + Niciun mesaj + Pentru mine + Eroare server fișiere: %1$s + Pentru toți + Mesajele vor fi marcate pentru ștergere. Destinatarul (destinatarii) va (vor) putea dezvălui aceste mesaje. + Descărcare + Fișierul va fi șters de pe servere. + Alătură-te ca %s + Nicio conversație selectată + Încărcarea fișierului + Eroare la salvarea fișierului + Șterge fără notificare + Deconectat + Repari conexiunea? + Mesaj care dispare + Mai mult + Pentru fiecare server vor fi utilizate acreditări noi SOCKS. + Sau pentru a partaja în privat + Microfon + Acordare permisiuni + Cască + Închide + Criptează fișierele locale + Blochează după + Mod de blocare + Cod de acces nou + Caracteristici experimentale + Este necesară parola bazei de date pentru a deschide conversația. + Introdu parola… + Retrogradează și deschide conversația + Vor fi afișate mesaje de la acești membri! + nici unul + Eroare la eliminarea membrului + indirect (%1$s) + Remedierea nu este acceptată de membrul grupului + Operator + Fă profilul privat! + Deschideți contiții + Nu mai afișa + Permite să ai mai multe conexiuni anonime fără a partaja date între ele într-un singur profil de conversație. + Luminos + Mesajele care dispar sunt interzise în această conversație. + Numai tu poți trimite mesaje care dispar. + %d lună + Mai multe îmbunătățiri vor veni în curând! + Mai multe îmbunătățiri vor veni în curând! + Livrarea mesajelor îmbunătățită + Conexiune mai fiabilă. + luni + Forma mesaj personalizabilă. + Versiunea pentru desktop nu este compatibilă. Asigurați-vă că utilizați aceeași versiune pe ambele dispozitive. + Statistici detaliate + Descărcat + Eroare la reconectarea serverelor + Activat pentru + %d evenimente de grup + %d conversații cu membri + activat + Conexiunea a atins limita de mesaje nelivrate, este posibil ca persoana de contact să fie offline. + Dezactivează blocarea SimpleX + Eroare la salvarea setărilor + Deconectezi desktopul? + Nu activați + Eroare la primirea fișierului + Eroare la ștergerea solicitării de contact + Dezactivați ștergerea automată a mesajelor? + Nu trimiteți istoricul noilor membri. + renegocierea criptării este permisă pentru %s + Mesaje care dispar + Eroare la ștergerea grupului + %d rapoarte + Eroare + Eroare la anularea modificării adresei + Eroare la ștergerea conexiunii de contact în așteptare + criptare agreată pentru %s + Introduceți mesajul de bun venit… (opțional) + Criptează fișierele și conținutul media stocat + Eroare la acceptarea condițiilor + Eroare la activarea confirmărilor de livrare! + Eroare fișier + Eroare la pornirea conversației + Eroare la setarea adresei + Eroare la actualizarea linkului de grup + Eroare la actualizarea confidențialității utilizatorului + Fișier + Imagine salvată în Galerie + Exportați baza de date + apel ratat + Căști + Activați autodistrugerea + Dezactivare primire confirmare? + Activați confirmarea de primire? + Nume nou afișat: + Instrumente pentru dezvoltatori + FIȘIERE + Deschide linkurile din lista de conversații + Forma mesajului + Importați baza de date + Arhivă nouă a bazei de date + Arhivă veche a bazei de date + Deschideți folderul bazei de date + implicit (%s) + Activezi ștergerea automată a mesajelor? + Notificările vor fi livrate doar până când aplicația se oprește! + Baza de date va fi criptată, iar parola va fi stocată în Keystore. + Bază de date criptată + migrare diferită în aplicație/baza de date: %s / %s + Confirmare de migrare nevalidă + Membrul %1$s schimbat la %2$s + Niciun contact de adăugat + Extindeți selecția de roluri + Invitați la conversație + Nou rol de membru + Invitați membrii + Editează profilul grupului + Grupul va fi șters doar pentru tine – această acțiune nu poate fi anulată! + Membrul va fi eliminat din grup - acest lucru nu poate fi anulat! + Membrii vor fi eliminați din chat - acest lucru nu poate fi anulat! + Membrii vor fi eliminați din grup - acest lucru nu poate fi anulat! + Mesajele de la %s vor fi afișate! + inactiv + Eroare la blocarea membrului pentru toți + Profilul grupului este stocat pe dispozitivele membrilor, nu pe servere. + Eroare la salvarea profilului de grup + Textul condițiilor actuale nu a putut fi încărcat, puteți consulta condițiile prin intermediul acestui link: + Eroare la actualizarea serverului + Serverul operator + Șterge profilul de conversație pentru + Introdu parola în câmpul de căutare + Incognito + Modul incognito îți protejează confidențialitatea prin utilizarea unui profil aleatoriu nou pentru fiecare contact. + Numai date despre profilul local + Potrivire + Bună ziua! + Mesaje directe + Mesaje care dispar + Doar persoana ta de contact poate șterge definitiv mesajele (tu le poți marca pentru ștergere). (24 de ore) + Ștergerea definitivă a mesajelor este interzisă în această conversație. + Numai tu poți trimite mesaje vocale. + Reacțiile la mesaje sunt interzise în această conversație. + Numai tu poți efectua apeluri. + Doar contactul tău poate efectua apeluri. + %dh + %d ore + Mesajele directe între membri sunt interzise. + Mesajele directe între membri sunt interzise în această conversație. + Membrii pot trimite linkuri SimpleX. + %d saptamana + oferit %s + oferit %s: %2s + Șterge conversația + Ștergi conversația cu membrul? + Membru se va alătura grupului, acceptați membru? + Ascunde ecranul aplicației din aplicațiile recente. + Nume, avatare și izolare transport diferite. + Interfață franceză + Draft de mesaje + În sfârșit, le avem! 🚀 + Conectare mai rapidă și mesaje mai de încredere. + Interfață în japoneză și portugheză + Interfață de utilizator maghiară și turcă + Redirecționează și salvează mesajele + Activează Flux în setările Rețea și servere pentru o mai bună confidențialitate a metadatelor. + pentru o mai bună confidențialitate a metadatelor. + Redirecționați până la 20 de mesaje simultan. + Nu rata mesajele importante. + Organizează conversațiile în liste + Trimitere mai rapidă a mesajelor. + Ștergere mai rapidă a grupurilor. + ore + Activați + Dispozitive mobile conectate + Introduceți numele acestui dispozitiv… + Eroare + Versiune incompatibilă + Dispozitiv mobil nou + Detectabil prin rețeaua locală + Detectează prin rețeaua locală + Deschide portul în firewall + Desktopul este inactiv + Desktopul a fost deconectat + Desktopul are codul de invitație greșit + Descărcarea a eșuat + Importul a eșuat + Introdu parola + Eroare la descărcarea arhivei + Migrați către un alt dispozitiv + duplicate + Mesaje trimise + Șters + Fișiere descărcate + Erori de descărcare + Găsește această permisiune în setările Android și activeaz-o manual. + Dimensiunea fontului + Dacă nu vă puteți întâlni în persoană, arătați codul QR într-un apel video sau distribuiți linkul. + Cod QR nevalid + Cod de acces incorect + Redirecționați %1$s mesaj/mesaje? + Redirecționarea serverului %1$s nu s-a putut conecta la serverul de destinație %2$s. Vă rugăm să încercați mai târziu. + Ascunde: + Imaginea va fi primită după ce persoana de contact o va termina de încărcat. + Versiunea serverului de redirecționare este incompatibilă cu setările de rețea: %1$s. + Redirecționați mesajele fără fișiere? + Link de conexiune nevalid + Invitația la grup a expirat + Eroare la crearea listei de conversație + Eroare la încărcarea listelor de conversații + Eroare la actualizarea listei de conversații + Notificări instant + Fără parolă pentru aplicație + Favorite + Fișier salvat + Fișierul va fi primit când contactul tău este online. Te rugăm să aștepți sau să verifici mai târziu! + niciun detaliu + Deschide cu %s + Link de invitație unic + Ștergi lista? + Marchează ca verificat + Activează pentru toți + grup șters + criptare ok + invitat + Fără sunet + ajutor + niciun text + Membrii pot trimite mesaje directe. + - opțional, notificați contactele șterse.\n- nume de profil cu spații.\n- și multe altele! + Activează în conversațiile directe (BETA)! + Navigare îmbunătățită în chat + Noile acreditări SOCKS vor fi utilizate de fiecare dată când porniți aplicația. + Vă protejează adresa și conexiunile IP. + Mod luminos + Sursa mesajelor rămâne privată. + Mesaje în direct + Asigurați-vă că configurația proxy este corectă. + Cel mai probabil, această persoană a șters conexiunea cu tine. + Avertizare de livrare a mesajelor + Membrul va fi eliminat din chat - acest lucru nu poate fi anulat! + Mesaje primite + Noua aplicație pentru desktop! + Nu s-au găsit conversații + Fără istoric + Niciun contact selectat + Previzualizare notificări + Fără apeluri în fundal + Nu + Note + Fără servicii în fundal + Gazdele Onion vor fi folosite atunci când sunt disponibile. + Numai tu poți adăuga reacții la mesaje. + Încă nu există conexiune directă, mesajul a fost redirecționat de administrator. + - Deschide conversația la primul mesaj necitit.\n- Mergi la mesajele citate. + Oprit + Deschide setările serverului + criptare ok pentru %s + profilul grupului a fost actualizat + proprietar + Șterge conversația + părăsit + Remedierea nu este suportată de contact + Erori + Deschide grupul + Mesajele vor fi șterse - acest lucru nu poate fi anulat! + Eroare la criptarea bazei de date + Fișiere + Păstrează-ți conexiunile + Membru inactiv + Doar 10 videoclipuri pot fi trimise simultan + Imagine trimisă + mesaj + Favorit + Marchează ca citit + Fără sunet + Nume listă... + Numele listei și emoji ar trebui să fie diferite pentru toate listele. + Află mai multe + Invită + Eroare la inițializarea WebView. Asigurați-vă că aveți WebView instalat și că arhitectura sa suportată este arm64.\nEroare: %s + Stabilește o conexiune privată + Deschide SimpleX Chat pentru a accepta apelul + Activați blocarea + Acest lucru se poate întâmpla atunci când tu sau contactul tău ați folosit un backup vechi al bazei de date. + Cod de acces + Mod incognito + Conexiune de rețea + Statusul rețelei + Asigurați-vă că fișierul are sintaxa YAML corectă. Exportați tema pentru a avea un exemplu de structură de fișiere tematice. + Grupuri incognito + Experiență nouă de conversație 🎉 + Noi opțiuni media + Primiți notificări când sunteți menționați. + Migrare completă + Altul + Migrați aici + Numele complet: + Ajutor pentru marcare + Dacă introduceți această parolă la deschiderea aplicației, toate datele aplicației vor fi șterse ireversibil! + Eroare la modificarea setării + invitație la grup %1$s + Confidențialitate și securitate îmbunătățite + Moderarea grupului + Fișierul a fost șters sau linkul este invalid + Pentru a continua, conversația trebuie oprită. + Extinde + Parola bazei de date este diferită de cea salvată în Keystore. + observator + Ascunde + Eroare la încărcarea detaliilor + Eroare la ștergerea profilului de utilizator + Descărcare + NU trimite mesaje direct, chiar dacă serverul tău sau cel de destinație nu acceptă rutare privată. + Se redirecționează %1$s mesaje + grupul este șters + nesincronizat + Din Galerie + Imagine + (stocat doar de membrii grupului) + Mesaj nou + Link complet + Păstrează + Sau arată acest cod + Eroare la schimbarea profilului + Nicio persoană de contact filtrată + Servere media și fișiere + Alte servere XFTP + NU utilizați rutare privată. + Activează apelurile de pe ecranul de blocare prin Setări. + Dezactivează (păstrează suprascrierile) + Dacă introduceți parola de autodistrugere în timp ce deschideți aplicația: + Mediu + Părăsește conversația + Fără conversații cu membrii + Confirmare de primire a mesajelor! + minute + Conectează un dispozitiv mobil + Creează listă + Șterge + Instalați SimpleX Chat pentru terminal + Eroare la salvarea serverelor ICE + CULORILE INTERFEȚEI + %d fișier(e) cu dimensiunea totală de %s + Fișiere și media + invitat %1$s + Membrii pot șterge definitiv mesajele trimise. (24 de ore) + Membrii pot trimite mesaje ce dispar. + Mesajele directe între membri sunt interzise în acest grup. + Mesajele care dispar sunt interzise. + %dw + Desktop găsit + Se descarcă detaliile linkului + Eroare internă + Sau importă un fișier de arhivă + Sau lipește linkul arhivei + Eroare la inițializarea WebView. Actualizați sistemul la noua versiune. Vă rugăm să contactați dezvoltatorii.\nEroare: %s + Grupul va fi șters pentru toți membrii - această acțiune nu poate fi anulată! + Nume local + Depanați livrarea + Eroare la schimbarea rolului + Reacții la mesaje + expirat + Descărcați %s (%s) + Instalează actualizarea + Nu crea adresă + De exemplu, dacă un contact al tău primește mesaje printr-un server SimpleX Chat, aplicația ta le va trimite printr-un server Flux. + Deschide Setările Safari / Site-uri web / Microfon, apoi selectează Permite pentru localhost. + Apel ratat + Renegocierea criptării a eșuat. + Oprit + Dezactivați confirmări pentru grupuri? + Dezactivează pentru toate grupurile + Dezactivează pentru toți + Deschide linkul + Baza de date va fi criptată, iar parola va fi stocată în setări. + Eroare: %s + Părăsești grupul? + Grupul nu a fost găsit! + Eroare la crearea contactului membrului + Doar proprietarii conversației pot schimba preferințele. + Stare fișier + Starea fișierului: %s + Statusul mesajului: %s + Mesajul prea mare + Reparare + Deschideți modificările + Eroare la adăugarea serverului + Meniuri și alerte + pornit + oprit + activat pentru contact + activat pentru tine + Profiluri de conversație ascunse + Acum administratorii pot:\n- șterge mesajele membrilor.\n- dezactiva membrii (rol de observator) + Reacții la mesaje + Rapid și fără așteptare până când expeditorul este online! + Descoperă și alătură-te grupurilor + Chiar și atunci când este dezactivat în conversație. + - livrare mai stabilă a mesajelor.\n- grupuri mai bune.\n- și multe altele! + Fă un mesaj să dispară + Migrați către un alt dispozitiv prin codul QR. + Ștergeți până la 20 de mesaje odată. + Vrei să te alături grupului tău? + Fără conexiune la rețea + Notificări instant! + Deschide setările aplicației + %d secunde + Activează blocarea SimpleX + Eroare la afișarea notificării, contactați dezvoltatorii. + Eroare de livrare a mesajelor + Eroare server destinație: %1$s + Eroare: %1$s + Pentru toți moderatorii + Redirecționat + Istoric + Ca răspuns la + Mesaj redirecționat + Grupuri + Mesajele au fost șterse după ce le-ați selectat. + Fără conversații + Nimic de redirecționat! + %d conversație(i) + %d mesaje + Eroare de decodare + Fișierele și conținutul media nu sunt permise + Mesaj + membrul are o versiune veche + Renegocierea criptării în desfășurare. + Reparare + Fișier mare! + Mesajele din această conversație nu vor fi niciodată șterse. + Notificări + Doar proprietarii grupului pot activa mesajele vocale. + Editați + Mesaj în direct! + Fără sunet toate + OK + Previzualizare imagine din link + Conversație nouă + Link de invitație unic + Servere mesaje + Alte servere SMP + Introduceți serverul manual + Servere ICE (unul pe linie) + Rețea și servere + Nu utilizați acreditările cu proxy. + Se descarcă actualizarea aplicației, nu închideți aplicația + Oprit + Deschideți locația fișierului + Ieșire fără salvare + Parolă profil ascuns + italic + Cum funcționează SimpleX + Fără identificatori de utilizator. + Acest lucru se poate întâmpla atunci când:\n1. Mesajele au expirat în aplicația de trimitere după 2 zile sau pe server după 30 de zile.\n2. Decriptarea mesajului a eșuat, deoarece tu sau contactul tău ați folosit un backup vechi al bazei de date.\n3. Conexiunea a fost compromisă. + Eroare la exportarea bazei de date a conversației + Eroare la oprirea conversației + niciodată + Niciun fișier primit sau trimis + Parolă nouă… + Criptezi baza de date? + Introdu parola corectă. + Vrei să te alături grupului? + Migrații: %s + Alătură-te + Părăsește + Părăsești conversația? + a părăsit + invitat prin linkul tău de grup + Un membru nou vrea să se alăture grupului. + criptare agreată + renegocierea criptării este permisă + renegocierea criptării este necesară pentru %s + Invită + Moderat la: %s + Numele complet al grupului: + Preferințele grupului + Ștergerea definitivă a mesajelor este interzisă. + Membrii pot adăuga reacții la mesaje. + Membrii pot trimite fișiere și media. + Reacțiile la mesaje sunt interzise. + %d oră + %dm + %d min + %d luni + %dmth + Istoricul nu este trimis noilor membri. + Membrii pot raporta mesaje către moderatori. + proprietari + Admiterea membrilor + oprit + Linkuri de grup + Ștergere definitivă a mesajelor + Măriți dimensiunea fontului. + Deconectează + Deconectat din motivul: %s + Eroare la ștergerea bazei de date + Eroare la încărcarea arhivei + Detalii + alte erori + Eroare la ștergerea conversației cu membrul + Link grup + Remediați conexiunea? + %d săptămâni + Redirecţionează + Mesajul va fi șters - acesta nu poate fi anulat! + Mesajul va fi marcat pentru ștergere. Destinatarul (destinatarii) va (vor) putea dezvălui acest mesaj. + Doar proprietarii grupului pot activa fișiere și media. + Imagine + Permite accesul la cameră + Cum să + Ignoră + Informații despre coada de mesaje + Complet descentralizat – vizibil doar pentru membri. + Introduceți numele grupului: + Umple + Descărcați noi versiuni de pe GitHub. + Dacă ai primit un link de invitație SimpleX Chat, îl poți deschide în browser-ul tău: + Link nevalid! + Nu + Invitați membrii + Nou în %s + Gazdele Onion vor fi necesare pentru conectare.\nRețineți: nu vă veți putea conecta la servere fără adresa .onion. + Nu se vor folosi gazde Onion. + Exportă tema + Importați tema + Eroare la importul temei + Eroare la acceptarea cererii de contact + Descărcați fișierul + Eroare la ștergerea contactului + Eroare la crearea adresei + Eroare la ștergerea notelor private + Eroare la salvarea serverelor + Erori în configurarea serverelor. + Adresa serverului de redirecționare este incompatibilă cu setările rețelei: %1$s. + Nu există servere pentru a trimite fișiere. + Mod de rutare a mesajelor + Niciodată + Întoarce camera + Link nevalid + în așteptarea aprobării + Notificări periodice + Notificările periodice sunt dezactivate! + Lipește linkul arhivei + Se pregătește descărcarea + Lipește + Vă rugăm să rugați persoana de contact să activeze apelurile. + Rutare privată + Confidențialitatea redefinită + Vă rugăm să verificați conexiunea la rețea cu %1$s și să încercați din nou. + Rutare mesaje private 🚀 + Bara de instrumente pentru conversație, accesibilă + Redă din lista de conversații. + Vă rugăm să încercați mai târziu. + Citeşte mai mult + Te rog să verifici dacă linkul SimpleX este corect. + Vă rugăm să contactați administratorul grupului. + Se pregătește încărcarea + Acces refuzat! + Vă rugăm să reduceți dimensiunea mesajului și să îl trimiteți din nou. + În așteptare + peer-to-peer + Vă rugăm să reporniți aplicația. + Mesaje primite + Lipește linkul pentru conectare! + Interziceți reacțiile la mesaje. + Interzicerea trimiterii de linkuri SimpleX + Lipește linkul primit pentru a te conecta cu contactul tău… + Te rugăm să-i ceri persoanei tale de contact să activeze trimiterea de mesaje vocale. + Cod QR + portul %d + Periodic + Servere presetate + Reține parola sau păstreaz-o în siguranță – nu există nicio modalitate de a o recupera dacă o pierzi! + Confidențialitate și securitate + Protejează ecranul aplicației + Imagini de profil + Te rugăm să stochezi parola în siguranță, altfel NU vei putea accesa conversația dacă o pierzi. + Interziceți trimiterea de mesaje vocale. + Apeluri cu funcție picture-in-picture + Interfață persană + Prin proxy + Conexiuni de profil și server + răspuns primit… + Politica de confidențialitate și condițiile de utilizare. + RUTAREA MESAJELOR PRIVATE + Te rugăm să stochezi parola în siguranță, altfel NU o vei putea schimba dacă o pierzi. + Parola nu a fost găsită în Keystore. Te rugăm să o introduci manual. Acest lucru s-ar putea întâmpla dacă ai restaurat datele aplicației folosind un instrument de backup. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii. + Parola stocată în Keystore nu poate fi citită. Acest lucru se poate întâmpla după o actualizare a sistemului incompatibilă cu aplicația. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii. + Membru %1$s + în așteptare + recenzie în așteptare + Primit la: %s + Expirare protocol + Număr de PING-uri + Parola profilului + Interziceți reacțiile la mesaje. + Interzice ștergerea definitivă a mesajelor. + Interziceți trimiterea de mesaje directe către membri. + Interziceți trimiterea de mesaje care dispar. + Interziceți raportarea mesajelor către moderatori. + Interziceți trimiterea de mesaje vocale. + Păstrează ultima schiță a mesajului, cu atașamente. + Nume de fișiere private + Criptare rezistentă cuantic + Confidențialitate pentru clienții tăi. + Nume de fișiere media private. + Aleatoriu + Vă rugăm să raportați dezvoltatorilor:\n%s\n\nSe recomandă repornirea aplicației. + Vă rugăm să confirmați că setările de rețea sunt corecte pentru acest dispozitiv. + Este necesară o parolă + Parolă + Servere presetate + Lipiți adresa desktopului + Este posibil ca amprenta certificatului din adresa serverului să fie incorectă + Interval PING-uri + Expirare protocol per KB + Notificări private + Actualizarea profilului va fi trimisă contactelor tale. + Interziceți apelurile audio/video. + Nume profil: + Autentificare proxy + Parolă de afișat + Vă rugăm să reduceți dimensiunea mesajului sau să eliminați fișierul media și să îl trimiteți din nou. + Protejați adresa IP + Mesaj primit + Note private + Bara de instrumente a conversației + Temă de profil + Lipește linkul pe care l-ai primit + Parola nu a fost modificată! + Parolă setată! + Parola stocată în Keystore nu poate fi citită. Te rugăm să o introduci manual. Acest lucru se poate întâmpla după o actualizare a sistemului incompatibilă cu aplicația. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii. + Bări de instrumente ale aplicației, accesibile + Vă rugăm să așteptați ca moderatorii grupului să vă examineze cererea de alăturare. + Interziceți trimiterea de mesaje care dispar. + Interziceți trimiterea de fișiere și fișiere media. + Vă rugăm să raportați dezvoltatorilor:\n%s + Servere proxy + Apel în așteptare + Vă rugăm să raportați dezvoltatorilor. + Te rugăm să introduci parola anterioară după restaurarea backupului bazei de date. Această acțiune nu poate fi anulată. + criptare e2e rezistentă cuantic + Chitanțele sunt dezactivate + Primit la + Interfață poloneză + Protejează-ți adresa IP de releele de mesagerie alese de contactele tale.\nActivează din setările *Rețea și servere*. + Mesaj primit + Vă rugăm să așteptați cât timp fișierul se încarcă de pe dispozitivul mobil conectat + imagine de profil + substituent pentru imaginea de profil + Lipește linkul + Adresa prestabilită a serverului + Server presetat + Evaluează aplicația + Port + Conversațiile private, grupurile și contactele tale nu sunt accesibile operatorilor serverului. + O parolă aleatorie este stocată în setări, ca text simplu.\nO poți schimba ulterior. + Te rugăm să introduci parola actuală\ncorectă. + Previzualizare + Te rugăm să verifici dacă dispozitivul mobil și cel desktop sunt conectate la aceeași rețea locală și dacă firewall-ul de pe desktop permite conexiunea.\nTe rugăm să raportezi orice alte probleme dezvoltatorilor. + Servere conectate anterior + În așteptare + confirmare primită… + Protejează-ți profilurile de conversații cu o parolă! + Te rog să verifici dacă ai folosit linkul corect sau cere contactului tău să îți trimită unul nou. + Eroare de rutare privată + respins + Răspuns primit + Destinatarul/Destinatarii nu pot vedea de la cine provine acest mesaj. + Trimiterea confirmărilor este dezactivată pentru %d contacte + primit, interzis + Reamintește mai târziu + Server adăugat la operatorul %s. + Setați mesajul afișat noilor membri! + Setați-l în locul autentificării sistemului. + Setează o parolă pentru export + Raportați conținutul: doar moderatorii grupului îl vor vedea. + Adresa serverului + Primirea fișierului va fi oprită. + Resetează la tema aplicației + Înregistrare actualizată la: %s + Trimiteți un mesaj pentru a activa apelurile. + Resetare + Rapoarte + informații despre coada serverului: %1$s\n\nultimul mesaj primit: %2$s + Salvează și reconectează-te + Raportați altceva: doar moderatorii grupului îl vor vedea. + caută + Setează numele conversației… + Serverul de retransmisie este utilizat doar dacă este necesar. O altă parte poate observa adresa ta IP. + Resetezi toate statisticile? + Resetați toate sugestiile + Trimite mesaje direct atunci când adresa IP este protejată, iar serverul tău sau cel de destinație nu acceptă rutare privată. + Parola de autodistrugere activată! + Setați parola + Examinați condițiile + Raport + Adresa de primire va fi schimbată la un alt server. Schimbarea adresei se va finaliza după ce expeditorul se conectează online. + Salvezi setările de acces? + Revizuiți mai târziu + Trimiterea confirmărilor este dezactivată pentru %d grupuri + recenzie + Operatorul serverului s-a schimbat. + Scalare + Utilizare redusă a bateriei + Mesaje trimise + Primit total + Primiți erori + Reconectați serverul pentru a forța livrarea mesajului. Consumă trafic suplimentar. + Resetează toate statisticile + Trimis prin proxy + Trimis direct + revizuit de administratori + Operatori de server + Serverul de retransmisie protejează adresa IP, dar poate observa durata apelului. + PORNIȚI CHATUL + te-a eliminat + %s la %s + Protocolul serverului a fost modificat. + Recepție simultană + Resetează la tema utilizatorului + Setați admiterea membrilor + Respinge + Respingeți membrul? + Destinatarii văd actualizările pe măsură ce le tastați. + Istoric recent și bot de directoare îmbunătățit. + Setează expirarea mesajelor în conversații. + Trimiterea confirmărilor de livrare va fi activată pentru toate contactele. + Trimiterea confirmărilor de livrare va fi activată pentru toate contactele din toate profilurile de conversații vizibile. + Securizat + Selectat %d + Trimiterea confirmărilor este activată pentru %d contacte + Informații despre servere + Reconectați serverele? + Statisticile serverelor vor fi resetate - această acțiune nu poate fi anulată! + Reconectează toate serverele + Reconectați serverul? + Total trimise + Preferințele conversației selectate interzic acest mesaj. + Trimiterea confirmărilor este activată pentru %d grupuri + Parolă de autodistrugere + Trimiteți rapoarte private + Trimite mesaje direct atunci când serverul tău sau cel de destinație nu acceptă rutarea privată. + eliminat din grup + cererea de înscriere a fost respinsă + Selectează profilul de conversație + Salvează setările adresei SimpleX + Salvează lista + TRIMITE CONFIRMĂRI DE LIVRARE LA + Parola de autodistrugere a fost schimbată! + Înregistrare actualizată la + Selectează operatorii de rețea de utilizat. + Telefoane mobile la distanță + Trimite confirmări de citire + Setați 1 zi + Parolă de autodistrugere + Reconectează toate serverele conectate pentru a forța livrarea mesajelor. Aceasta folosește trafic suplimentar. + Se primesc mesaje… + Motivul raportării? + respins + Raport: %s + Se salvează %1$s mesaje + Raport trimis moderatorilor + Raportați spam: numai moderatorii grupului îl vor vedea. + Raportați profilul membrului: numai moderatorii grupului îl vor vedea. + Raportați încălcarea: numai moderatorii grupului o vor vedea. + Înregistrați mesajul vocal + Scanează / Lipește link + Server + Repornește aplicația pentru a folosi baza de date de conversații importată. + Eliminați membri? + Primirea prin intermediul + Raportarea mesajelor este interzisă în acest grup. + Examinați membrii + Verificați membrii înainte de a-i primi (a bate la ușă). + Eliminați arhiva? + Trimiteți erorile + Reconectați-vă + Selectează + Adresa serverului este incompatibilă cu setările de rețea: %1$s. + Coadă securizată + Este posibil ca expeditorul să fi șters cererea de conectare. + Versiunea serverului este incompatibilă cu aplicația ta: %1$s. + Deblocare + Încărcarea a eșuat + Aplicația se poate închide după 1 minut în fundal. + Cu sunet + Această setare se aplică mesajelor din profilul de conversație actual + Folosiți servere SimpleX Chat? + Proxy SOCKS + Nume de utilizator + SimpleX Chat și Flux au ajuns la un acord pentru a include în aplicație servere operate de Flux. + Aplicația preia mesajele noi periodic și consumă câteva procente din baterie pe zi. Aplicația nu folosește notificări push, așadar datele de pe dispozitivul tău nu sunt trimise către servere. + Blocare SimpleX + Cheie greșită sau adresă necunoscută a fragmentului de fișier - cel mai probabil fișierul este șters. + Prea multe videoclipuri! + Videoclip trimis + Oprește partajarea + Servere ICE WebRTC + Actualizați și deschideți chatul + Fișiere încărcate + Folosește profilul curent + Folosește noul profil incognito + Vizualizarea a eșuat + Funcție lentă + Blocare SimpleX + Deblocați pentru toți + Deblocați membrii pentru toate? + Actualizare + Până la 100 de mesaje recente sunt trimise noilor membri. + Ce este nou + Mulțumim utilizatorilor – contribuiți prin Weblate! + Mulțumim utilizatorilor – contribuiți prin Weblate! + Cu un consum redus de energie al bateriei. + Cu un consum redus de energie al bateriei. + Începând cu %s.\nToate datele sunt păstrate confidențiale pe dispozitivul tău. + Abonat + Erori de încărcare + Apel video + Pentru a vă conecta prin link + Mesajele vocale nu sunt permise + Neprotejat + Da + Actualizare + Aveți deja un profil de chat cu același nume afișat. Vă rugăm să alegeți un alt nume. + Acest nume afișat este invalid. Vă rugăm să alegeți un alt nume. + Mesajele vor fi șterse pentru toți membrii. + Folosiți serverul + Sistem + Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact. + Ești conectat la serverul folosit pentru a primi mesaje de la acest contact. + Mulțumim utilizatorilor – contribuiți prin Weblate! + Poți să încerci încă o dată. + Ești deja conectat la %1$s. + Cheie greșită sau conexiune necunoscută - cel mai probabil această conexiune este ștearsă. + Video + Mesaj vocal (%1$s) + Trimite + Comută lista de conversații: + Pentru a primi notificări, te rugăm să introduci parola bazei de date + Se așteaptă videoclipul + Mesaje nelivrate + Testul a eșuat la pasul %s. + Opriți fișierul + trimitere neautorizată + Bun venit, %1$s! + Atinge pentru a începe o conversație nouă + Folosește acreditări proxy diferite pentru fiecare conexiune. + Adresă SimpleX sau link de unică folosință? + Pentru a-ți proteja confidențialitatea, SimpleX folosește ID-uri separate pentru fiecare persoană de contact. + Ai fost invitat în grup + Pentru a primi + Utilizare pentru fișiere + Pentru a trimite + Temă + Utilizați aplicația cu o singură mână. + Videoclipul va fi primit după ce persoana de contact termină de încărcat. + Videoclipul va fi primit atunci când persoana ta de contact este online. Te rugăm să aștepți sau să verifici mai târziu! + video + Utilizarea serverelor SimpleX Chat. + Folosiți proxy SOCKS? + Browserul web implicit este necesar pentru apeluri. Vă rugăm să configurați browserul implicit în sistem și să partajați mai multe informații cu dezvoltatorii. + Afișează statusul mesajului + Port TCP pentru mesagerie + Descărcarea actualizării a fost anulată + Omiteți invitarea membrilor + Începând de la %s. + Se așteaptă conectarea mobilului: + Server XFTP + Această acțiune nu poate fi anulată - profilul, contactele, mesajele și fișierele tale vor fi pierdute definitiv. + Sistem + Deblocați membrul? + Cu sunet + Mesaje vocale + Statistici + Opriți chatul? + necunoscut + Poți ascunde sau dezactiva notificările unui profil de utilizator – ține apăsat pentru meniu. + Ați permis + Partajează link de unică folosință cu un prieten + Mesaje vocale + Mesaj vocal… + Setări proxy SOCKS + profil actualizat + Mod Blocare SimpleX + Mesajele vor fi marcate ca moderate pentru toți membrii. + necitit + Bun venit! + Atingeți pentru conectare + Mesajele vocale sunt interzise! + Ați acceptat conexiunea + Partajează adresa public + Partajează acest link de invitație de unică folosință + Pentru a verifica criptarea end-to-end, compară (sau scanează) codul de pe dispozitivele voastre cu persoana de contact. + Utilizare pentru conexiuni noi + Utilizați proxy SOCKS + Oprești partajarea adresei? + Când aplicația rulează + Aplicația vă protejează confidențialitatea utilizând operatori diferiți în fiecare conversație. + Vizualizează condițiile + Afișează doar contactul + Deblocare + Porniți + Oprește chatul + Acest mesaj a fost șters sau nu a fost încă primit. + Mesajul va fi marcat ca moderat pentru toți membrii. + Stop + Opriți primirea fișierului? + Se așteaptă fișierul + Această acțiune nu poate fi anulată - mesajele trimise și primite în această conversație, mai vechi decât data selectată, vor fi șterse. + Vizualizează codul de securitate + Atinge pentru a lipi linkul + Textul pe care l-ai lipit nu este un link SimpleX. + Stabil + Puteți configura operatorii în setările Rețea și servere. + Actualizare + apel video (necriptat e2e) + ID-ul următorului mesaj este incorect (mai mic sau egal cu precedentul).\nSe poate întâmpla din cauza unei erori sau când conexiunea este compromisă. + Fără Tor sau VPN, adresa ta IP va fi vizibilă pentru serverele de fișiere. + Aplicația va cere să confirmați descărcările de pe servere de fișiere necunoscute (cu excepția celor .onion sau când proxy-ul SOCKS este activat). + Sistem + Acestea pot fi ignorate în setările de contact și de grup. + SUPORT SIMPLEX CHAT + PROXY SOCKS + TEME + În timpul importului au apărut câteva erori non-fatale: + Atingeți pentru a vă alătura + Atenție: este posibil să pierdeți unele date! + Folosiți serverele + Reafișează profilul de chat + Titlu + Transparenţă + Videoclipuri și fișiere de până la 1 GB + Comută modul incognito la conectare. + Formă imagine de profil + Schimbă profilul de conversație pentru invitații de unică folosință. + Puteți activa mai târziu prin Setări + săptămâni + Acesta este linkul tău unic, valabil o singură dată! + Servere necunoscute! + Fără Tor sau VPN, adresa ta IP va fi vizibilă pentru aceste retransmiteri XFTP:\n%1$s. + Actualizează parola bazei de date + Opriți trimiterea fișierului? + tăiere + Viitorul mesageriei + Pentru a efectua apeluri, permiteți utilizarea microfonului. Încheiați apelul și încercați să sunați din nou. + Când sunt activați mai mulți operatori, niciunul dintre ei nu are metadate pentru a afla cine comunică cu cine. + Video pornit + Aceste setări sunt pentru profilul tău actual + Da + Coada + Utilizare de pe desktop + Oprește conversațiile pentru a exporta, importa sau șterge baza de date a conversațiilor. Nu vei putea primi și trimite mesaje cât timp conversațiile sunt oprite. + Unele fișiere nu au fost exportate + Parola este stocată în setări ca text simplu. + deblocat %s + status necunoscut + tu: %1$s + Mesaj de bun venit + Deblocați membrul + Deblocați membrul pentru toate? + Comută + Rolul va fi schimbat în %s. Toți participanții la chat vor fi notificați. + Rolul va fi schimbat în %s. Toți membrii grupului vor fi notificați. + Folosește %s + Site web + %s Servere + Actualizați setările de rețea? + Atingeți pentru a activa profilul. + Sistem + Mulțumim utilizatorilor – contribuiți prin Weblate! + Verifică securitatea conexiunii + Izolarea transportului + Pentru a proteja fusul orar, fișierele imagine/voce utilizează UTC. + - mesaje vocale de până la 5 minute.\n- timp personalizat de dispariție.\n- istoricul modificărilor. + Pentru a ascunde mesajele nedorite. + Prin protocol securizat de rezistență cuantică. + Cu fișiere și media criptate. + Va fi activat în conversațiile directe! + Al doilea operator presetat din aplicație! + Vizualizați condițiile actualizate + Verificați conexiunea + Le poți activa mai târziu din setările de Confidențialitate și securitate ale aplicației. + Verificați codul pe mobil + Numele acestui dispozitiv + Acest dispozitiv + Deconectare + Pentru a permite unei aplicații mobile să se conecteze la desktop, deschideți acest port în firewall, dacă îl aveți activat + Verificați codul cu desktopul + Această funcție nu este încă compatibilă. Încercați următoarea versiune. + Vă conectați deja prin intermediul acestei legături unice! + Atenție: inițierea chatului pe mai multe dispozitive nu este acceptată și va cauza erori de livrare a mesajelor. + Verifică parola bazei de date + Ethernet prin cablu + Abonamente ignorate + Mărime + Poți menționa până la %1$s membri per mesaj! + Blocarea SimpleX este activată + Verifică codul de securitate + Criptarea funcționează și noul acord de criptare nu este necesar. Poate duce la erori de conexiune! + Expirare conexiune TCP + Afișează + Se afișează informații pentru + Mesajul va fi șters pentru toți membrii. + Vă mulțumim pentru instalarea SimpleX Chat! + Partajează profil + Eroare necunoscută + Sesiuni de transport + Când IP-ul este ascuns + Sunet oprit + Reafișează profilul + Accent imagine de fundal + Arhiva bazei de date încărcată va fi eliminată definitiv de pe servere. + WiFi + Oprire + Mesaje ignorate + Parola va fi stocată în setări ca text simplu după ce o schimbi sau repornești aplicația. + Conexiune TCP + Erori de abonare + Atingeți pentru a vă conecta incognito + Imaginea nu poate fi decodificată. Vă rugăm să încercați o altă imagine sau să contactați dezvoltatorii. + Serverele pentru fișierele noi ale profilului tău de conversații actual + Acest grup nu mai există. + Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact (eroare: %1$s). + Actualizarea setărilor va reconecta clientul la toate serverele. + Acest șir de caractere nu este un link! + S-a atins timpul de expirare la conectarea la desktop + Pentru a te proteja împotriva înlocuirii linkului tău, poți compara codurile de securitate ale contactelor. + Pentru a-ți proteja informațiile, activează Blocare SimpleX.\nVa trebui să finalizezi autentificarea înainte de a activa această funcție. + Dacă persoana ta de contact nu a șters conexiunea sau dacă acest link a mai fost folosit, este posibil să fie o eroare. Te rugăm să o raportezi.\nPentru a te conecta, roagă persoana ta de contact să creeze un nou link și asigură-te că ai o conexiune stabilă la internet. + Când partajezi un profil incognito cu cineva, acest profil va fi folosit pentru grupurile la care te invită. + Mesaj vocal + La conectarea apelurilor audio și video. + Când oamenii solicită conectarea, poți accepta sau respinge solicitarea. + Acest grup are peste %1$d membri, confirmările de livrare nu sunt trimise. + Comutați între audio și video în timpul apelului. + Deconectați desktopul? + Se așteaptă desktopul… + Prea multe imagini! + Încarcă fișier + Atinge butonul + Atingeți pentru a scana + (pentru a partaja cu persoana de contact) + Pentru a începe o nouă conversație + Video + Partajează link de unică folosință + Acest link nu este un link de conectare valid! + Acest cod QR nu este un link! + Link scurt + Server XFTP + Actualizați modul de izolare a transportului? + Folosește rutare privată cu servere necunoscute. + Folosește rutare privată cu servere necunoscute atunci când adresa IP nu este protejată. + Folosiți portul TCP %1$s atunci când nu este specificat niciun port. + Nu stocăm niciunul dintre contactele sau mesajele tale (odată livrate) pe servere. + prin releu + Soft + Puternic + A doua ticăitură pe care am ratat-o! ✅ + Mulțumim utilizatorilor – contribuiți prin Weblate! + Afișează procentul + Serverele pentru noile conexiuni ale profilului tău de conversație actual + Afișează apeluri API lente + Pentru a fi notificat despre noile versiuni, activați verificarea periodică pentru versiunile Stabile sau Beta. + Folosește o parolă aleatorie + Această acțiune nu poate fi anulată - toate fișierele și fișierele media primite și trimise vor fi șterse. Imaginile cu rezoluție mică vor rămâne. + Îl poți modifica în setările Aspect. + ați acceptat acest membru + Istoric vizibil + Te alături deja grupului prin intermediul acestui link. + Server SMP + Servere necunoscute + Folosește aplicația în timpul apelului. + Numele dispozitivului va fi partajat cu clientul mobil conectat. + Încărcat + Folosiți portul TCP 443 doar pentru serverele presetate. + Omiteți această versiune + O poți crea mai târziu + O poți face vizibilă contactelor tale SimpleX din Setări. + Platforma de mesagerie și aplicații care vă protejează confidențialitatea și securitatea. + Profilul este partajat doar cu contactele tale. + Pentru a-ți dezvălui profilul ascuns, introdu parola completă în câmpul de căutare de pe pagina Profilurile tale de conversație. + Puteți configura serverele prin intermediul setărilor. + apel video + Video dezactivat + Parolă greșită pentru baza de date + Eroare necunoscută în baza de date: %s + l-ai blocat pe %s + Rolul va fi schimbat în %s. Membrul va primi o nouă invitație. + Mesaj de bun venit + Mesajul de bun venit este prea lung + Imagine de fundal + Mod sistem + da + Mesajele vocale sunt interzise în acest chat. + Cu mesaj opțional de bun venit. + Suportă Bluetooth și alte îmbunătățiri. + Aceasta este propria ta adresă SimpleX! + Se încarcă arhiva + %s a fost încărcat + Verifică parola + Blocarea SimpleX nu este activă! + Autentificare sistem + Baza de date nu funcționează corect. Atingeți pentru a afla mai multe. + Raportul va fi arhivat. + Atingeți Creează o adresă SimpleX în meniu pentru a o crea mai târziu. + Acest text este disponibil în setări + Ești invitat în grup + Se așteaptă imaginea + Se așteaptă imaginea + Se așteaptă videoclipul + Eroare temporară de fișier + Elimină din favorite + Mențiuni necitite + Conexiunea pe care ați acceptat-o va fi anulată! + Contactul cu care ați partajat acest link NU se va putea conecta! + vrea să se conecteze cu tine! + Partajează adresa SimpleX pe rețelele de socializare. + Pentru a te conecta, persoana ta de contact poate scana codul QR sau poate folosi linkul din aplicație. + Poți seta numele conexiunii, ca să-ți amintești cu cine ai partajat linkul. + Adresa SimpleX și linkurile de unică folosință pot fi partajate în siguranță prin orice aplicație de mesagerie. + Codul scanat nu este un cod QR de tip link SimpleX. + Server de testare + Servere de testare + Folosește credențiale aleatorii + Actualizare disponibilă: %s + Folosește chatul + Hash-ul mesajului anterior este diferit. + Această acțiune nu poate fi anulată - mesajele trimise și primite anterior celei selectate vor fi șterse. Poate dura câteva minute. + Parolă greșită! + Încercarea de a schimba parola bazei de date nu a fost finalizată. + Ai fost invitat(ă) în grup. Alătură-te pentru a te conecta cu membrii grupului. + profil de grup actualizat + Condiții actualizate + Utilizați pentru mesaje + Mesajele vocale sunt interzise. + Protocoalele SimpleX analizate de Trail of Bits. + Acest link a fost utilizat cu un alt dispozitiv mobil, vă rugăm să creați un link nou pe desktop. + Verifică conexiunile + Poți să încerci încă o dată. + Nu ești conectat la aceste servere. Pentru livrarea mesajelor către ele se folosește rutarea privată. + Se opresc conversațiile + Total + ești observator + Videoclipul nu poate fi decodificat. Vă rugăm să încercați un alt videoclip sau să contactați dezvoltatorii. + Puteți copia și micșora dimensiunea mesajului pentru a-l trimite. + aștept răspunsul… + aștept confirmarea… + Actualizați aplicația automat + Folosește acreditări proxy diferite pentru fiecare profil. + Folosești conexiune directă la internet? + Poți trimite mesaje către %1$s din contactele arhivate. + Pentru a vă proteja adresa IP, rutarea privată utilizează serverele SMP pentru a livra mesaje. + Izolarea transportului + Folosește gazde .onion + Când este disponibil + Link canal SimpleX + Spam + Acest link necesită o versiune mai nouă a aplicației. Vă rugăm să actualizați aplicația sau să solicitați persoanei de contact să vă trimită un link compatibil. + Legătură de conexiune neacceptată + Utilizați portul web + %s.]]> + %s]]> + Arhivare raport + Conversația va fi ștearsă doar pentru tine - această acțiune nu poate fi anulată! + Utilizarea bateriei aplicației / Nerestricționat în setările aplicației.]]> + Ghidul utilizatorului.]]> + V-ați alăturat acestui grup. Se conectează la membrul invitat al grupului. + Profilul tău va fi trimis persoanei de contact de la care ai primit acest link. + Te vei conecta cu toți membrii grupului. + Toate mesajele noi de la acești membri vor fi ascunse! + Profilul tău aleatoriu + (acest dispozitiv v%s)]]> + %s are o versiune neacceptată. Asigurați-vă că utilizați aceeași versiune pe ambele dispozitive]]> + %1$s.]]> + Conexiuni active + Conversații de afaceri + l-ai blocat pe %s + Bucăți șterse + Serverele tale ICE + ai eliminat %1$s + Preferințele tale + Adaugă listă + Toate + Puteți vizualiza în continuare conversația cu %1$s în lista de conversații. + Condiții acceptate + %s.]]> + %1$d erori de fișier:\n%2$s + Conexiune blocată + Conexiunea este blocată de operatorul serverului:\n%1$s. + criptate end-to-end, cu securitate post-cuantică în mesajele directe.]]> + Bările de instrumente ale aplicației + Securitate îmbunătățită ✅ + Toate conversațiile vor fi eliminate din lista %s, iar lista va fi ștearsă + Conexiunea ta a fost mutată la %s, dar a apărut o eroare la schimbarea profilului. + Puteți migra baza de date exportată. + Serverele tale XFTP + Utilizați gazdele .onion la Nu dacă proxy-ul SOCKS nu le acceptă.]]> + Profilul tău de chat va fi trimis membrilor chatului + Vei primi în continuare apeluri și notificări de la profilurile dezactivate atunci când acestea sunt active. + Întreabă + %s.]]> + Schimbați ștergerea automată a mesajelor? + Adresă sau link de unică folosință? + Sesiune de aplicație + Contactele tale + Datele tale de conectare pot fi trimise necriptat. + Adresă de afaceri + Adaugă membrii echipei tale în conversații. + Verifică mesajele la fiecare 10 minute + %s, acceptați condițiile de utilizare.]]> + Confirmă-ți datele de autentificare + Schimbă profilurile de conversație + Contacte + Conexiunea necesită renegocierea criptării. + Adresa ta SimpleX + Creează un link de unică folosință + Profilul tău actual + Aplicația rulează întotdeauna în fundal + acceptat %1$s + %s.]]> + Condițiile vor fi acceptate pe: %s. + Servere media și de fișiere adăugate + Estompare + - conectare la serviciul de directoare (BETA)!\n- confirmări de livrare (până la 20 de membri).\n- mai rapid și mai stabil. + Arhivează contactele pentru a conversa mai târziu. + %1$s.]]> + Ai partajat o cale de fișier nevalidă. Raportează problema dezvoltatorilor aplicației. + Deschide în aplicația mobilă, apoi atinge Conectare în aplicație.]]> + Actualizează adresa + Configurați operatorii serverului + Condițiile vor fi acceptate pentru operatorii activați după 30 de zile. + Apasă pe butonul de informații de lângă bara de adrese pentru a permite accesul la microfon. + Colţ + Puteți salva arhiva exportată. + Nu veți mai primi mesaje de la acest chat. Istoricul conversației va fi păstrat. + Nu veți mai primi mesaje de la acest grup. Istoricul conversației va fi păstrat. + Încercați să invitați persoana de contact cu care ați partajat un profil incognito în grupul în care utilizați profilul principal + Poți partaja un link sau un cod QR - oricine se va putea alătura grupului. Nu vei pierde membrii grupului dacă îl ștergi ulterior. + Blocați membrii pentru toți? + Conversație + %s.]]> + Condiții acceptate pe: %s. + Condițiile vor fi acceptate automat pentru operatorii activați pe: %s. + %s.]]> + %s.]]> + %s.]]> + Condiții de utilizare + Permite raportarea mesajelor către moderatori. + Conversează cu membrii + Convorbire cu administratori + Acceptă + Acceptă membru + Date de mesaj îmbunătățite. + Performanță îmbunătățită a grupurilor + Confidențialitate și securitate îmbunătățite + %s este inactiv]]> + %s lipsește]]> + %1$s!]]> + Confirmat + Erori de confirmare + Vi se va solicita să vă autentificați când porniți sau reluați aplicația după 30 de secunde în fundal. + să te conectezi la dezvoltatorii SimpleX Chat pentru a le adresa orice întrebări și pentru a primi actualizări.]]> + Adaugă prieteni + Adaugă membri echipei + Conversația există deja! + Deschide în aplicația mobilă.]]> + Continuă + %s a fost deconectat]]> + Despre operatori + %s este ocupat]]> + Nu puteți trimite mesaje! + ați ieșit + nu se pot trimite mesaje + contact șters + contact dezactivat + contactul nu este gata + Profilul, contactele și mesajele livrate sunt stocate pe dispozitiv. + Serverele tale ICE + Adaugă la listă + Modificați lista + invitație acceptată + Îți poți partaja adresa sub formă de link sau cod QR - oricine se poate conecta cu tine. + Acceptă + Confidențialitatea ta + Acceptă condițiile + Zoom + %1$s.]]> + %1$s.]]> + Bucăți încărcate + Arhivați toate rapoartele? + Arhivați %d rapoarte? + Arhivare raport? + Dispozitive Xiaomi: vă rugăm să activați Pornirea automată în setările de sistem pentru ca notificările să funcționeze.]]> + Arhivă + Pictogramă de context + Poți vizualiza rapoartele tale în conversația cu administratorii. + Schimbă ordinea + Te vei conecta atunci când dispozitivul contactului tău va fi online, te rugăm să aștepți sau să verifici mai târziu! + scana codul QR în timpul apelului video sau contactul tău poate partaja un link de invitație.]]> + Profilul tău %1$s va fi distribuit. + Nu îți vei pierde contactele dacă ulterior îți ștergi adresa. + Securitatea conexiunii + Serverele tale SMP + Toate serverele + Profilul tău este stocat pe dispozitiv și este partajat doar cu contactele tale. Serverele SimpleX nu pot vedea profilul tău. + Baza ta de date actuală de chat va fi ȘTERSĂ și ÎNLOCUITĂ cu cea importată.\nAceastă acțiune nu poate fi anulată - profilul, contactele, mesajele și fișierele tale vor fi pierdute iremediabil. + Convorbire cu administratori + Poți partaja această adresă cu contactele tale pentru a le permite să se conecteze cu %s. + Conversează cu membrul + Cererea de conectare va fi trimisă acestui membru al grupului. + Servere de mesaje adăugate + %s cu motivul: %s]]> + %s a fost deconectat]]> + (nou)]]> + Serverul tău + Trebuie să îi permiți contactului tău să te sune pentru a-l putea suna. + Adresa serverului tău + Serverele tale + Vei fi conectat la grup atunci când dispozitivul gazdei grupului va fi online, te rugăm să aștepți sau să verifici mai târziu! + Tu decizi cine se poate conecta. + Vei fi conectat când cererea ta de conectare va fi acceptată, te rugăm să aștepți sau să verifici mai târziu! + Acceptați ca observator + Experiență de utilizare îmbunătățită + 1 chat cu un membru + Toate rapoartele vor fi arhivate pentru tine. + depozitul nostru GitHub.]]> + Permiteți în următoarea fereastră de dialog să primească notificări instantaneu.]]> + Setări adresă + Arhivare rapoarte + Acceptă ca membru + toți + Afaceri + Prin utilizarea SimpleX Chat ești de acord să:\n- trimiți doar conținut legal în grupurile publice.\n- respecți ceilalți utilizatori – fără spam. + doar cu un singur contact - partajează-l personal sau prin orice aplicație de mesagerie.]]> + Nu trebuie să utilizați aceeași bază de date pe două dispozitive. + arătați codul QR în apelul video sau distribuiți linkul.]]> + Utilizare de pe desktop în aplicația mobilă și scanează codul QR.]]> + SimpleX rulează în fundal în loc să utilizeze notificări push.]]> + Utilizarea bateriei aplicației / Nerestricționat în setările aplicației.]]> + Bucăți descărcate + Conversația va fi ștearsă pentru toți membrii - această acțiune nu poate fi anulată! + Conexiunea nu este gata. + Conținutul încalcă condițiile de utilizare + Folosești un profil incognito pentru acest grup - pentru a preveni partajarea profilului principal, invitarea contactelor nu este permisă. + Ai trimis o invitație la grup + Ați respins invitația de grup + Scanare cod QR.]]> + cererea a fost trimisă + Alătură-te grupului + Acceptă cererea de contact + Adaugă mesaj + Biografie: + Biografie prea mare + Nu se poate modifica profilul + Conversează cu administratorii + Conversează cu membrii înainte ca aceștia să se alăture. + Conectează-te + Conectează-te mai rapid! 🚀 + contactul ar trebui să accepte… + Descriere prea lungă + Eroare la modificarea profilului + Folosește profil incognito + Deschide conversația + Eroare la respingerea cererii de contact + 4 noi limbi de interfață + Catalană, Indoneziană, Română și Vietnameză - mulțumită utilizatorilor noștri! + criptare end-to-end.]]> + Deschide o conversație nouă + Creează un grup nou + Deschide pentru a accepta + Deschide pentru conectare + Deschide pentru a te alătura + Timp de așteptare depășit pentru rutarea privată + Te poți conecta la conversație și poți trimite mesaje imediat ce apeși pe Conectare. + Rol nou în grup: Moderator + Descriere scurtă: + Trimis contactului tău după conectare. + Actualizezi la o adresă permanentă? + Mesaj de bun venit + SOLICITĂRI DE CONTACT DE LA GRUPURI + Această setare este pentru profilul tău actual + Partajează adresa veche + Partajează linkul vechi + Se încarcă profilul… + cerere de conectare din grupul %1$s + Eroare la deschiderea conversației + Eroare la deschiderea grupului + Mai puțin trafic pe rețelele mobile. + Adresa va fi scurtă, iar profilul tău va fi partajat prin intermediul acestei adrese. + Actualizează + Adresă SimpleX scurtă + Atinge Conectare pentru a începe conversația + Creează-ți adresa + Acceptă cererea de contact + doar după ce cererea ta este acceptată.]]> + Fișierele și conținutul media sunt interzise în această conversație. + Activează implicit mesajele care dispar. + Atât tu, cât și contactul tău puteți trimite fișiere și conținut media. + Permite contactelor tale să trimită fișiere și conținut media. + Conexiune de afaceri + Membrul a fost șters – nu poate accepta cererea + Permite fișierele și conținutul media doar dacă persoana de contact le permite. + Opțiuni depreciate + Păstrează-ți conversațiile curate + Nicio sesiune de rutare privată + Deschide linkul complet + Deschide pentru a folosi botul + Interzice trimiterea de fișiere și conținut media. + Timp de expirare protocol în fundal + Revizuiește membrii grupului + Trimiți cerere de contact? + Trimite cererea + Trimite cererea fără mesaj + Trimite feedbackul tău privat grupurilor. + Setează biografia și mesajul de bun venit al profilului. + Partajează-ți adresa + Atinge Conectează-te pentru a trimite cererea + Atinge Conectează-te pentru a folosi botul + Actualizezi linkul grupului? + Urează bun venit contactelor tale 👋 + Biografia ta: + Contactul tău de afaceri + Contactul tău + Grupul tău + Doar tu poți trimite fișiere și conținut media. + Doar contactele tale pot trimite fișiere și conținut media. + Deschide linkul curat + Respinge cererea de contact + Timp expirare conexiune TCP în fundal + Linkul va fi scurt, iar profilul grupului va fi partajat prin acesta. + Expeditorul NU va fi notificat. + Timpul de dispariție este setat doar pentru contactele noi. + Pentru a trimite comenzi, trebuie să fii conectat. + Pentru a folosi un alt profil după încercarea de conectare, șterge conversația și folosește din nou linkul. + Actualizează-ți adresa + Elimină urmărirea linkurilor + Șterge mesaje și blochează membri. + Atinge Alătură-te grupului + Actualizează linkul grupului + Profilul tău + Link de releu SimpleX + Grup + Eroare la marcarea conversației cu membrul ca fiind citită + 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 f234a2cf0a..95e5ed9409 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -14,7 +14,7 @@ ошибка соединяется Установлено соединение с сервером, через который Вы получаете сообщения от этого контакта. - Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %1$s). + Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %1$s). Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта. удалено @@ -42,19 +42,19 @@ SimpleX одноразовая ссылка SimpleX ссылка группы через %1$s - SimpleX ссылки + Ссылки SimpleX Описание Полная ссылка В браузере Использование ссылки в браузере может уменьшить конфиденциальность и безопасность соединения. Ссылки на неизвестные сайты будут красными. - Ошибка при сохранении SMP серверов - Пожалуйста, проверьте, что адреса SMP серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется. + Ошибка при сохранении SMP-серверов + Пожалуйста, проверьте, что адреса SMP-серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется. Ошибка при сохранении настроек сети Превышено время соединения Ошибка соединения - Пожалуйста, проверьте Ваше соединение с сервером %1$s и попробуйте еще раз. + Пожалуйста, проверьте Ваше соединение с сервером %1$s и попробуйте ещё раз. Ошибка при отправке сообщения Ошибка при добавлении членов группы Ошибка при вступлении в группу @@ -67,8 +67,7 @@ Ошибка в ссылке контакта Пожалуйста, проверьте, что Вы использовали правильную ссылку, или попросите Ваш контакт отправить Вам новую. Ошибка соединения (AUTH) - Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом. -\nЧтобы установить соединение, попросите Ваш контакт создать еще одну ссылку и проверьте Ваше соединение с сетью. + Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите Ваш контакт создать ещё одну ссылку и проверьте Ваше соединение с сетью. Ошибка при принятии запроса на соединение Отправитель мог удалить запрос на соединение. Ошибка при удалении контакта @@ -77,8 +76,8 @@ Ошибка удаления ожидаемого соединения Ошибка при изменении адреса Ошибка теста на шаге %s. - Сервер требует авторизации для создания очередей, проверьте пароль - Возможно, хэш сертификата в адресе сервера неверный + Сервер требует авторизации для создания очередей, проверьте пароль. + Хэш в адресе сервера не соответствует сертификату. Соединение Создание очереди Защита очереди @@ -90,7 +89,7 @@ Мгновенные уведомления выключены! SimpleX выполняется в фоне вместо уведомлений через сервер.]]> Он может быть выключен через Настройки – Вы продолжите получать уведомления о сообщениях пока приложение запущено.]]> - Разрешите это в следующем окне чтобы получать нотификации мгновенно.]]> + Разрешите это в следующем окне, чтобы получать уведомления мгновенно.]]> Оптимизация батареи включена, поэтому сервис уведомлений выключен. Вы можете снова включить его через Настройки. Периодические уведомления Периодические уведомления выключены! @@ -175,7 +174,7 @@ Чаты соединяется… Вы приглашены в группу - вступить как %s + Вступить как %s соединяется… Нажмите, чтобы начать чат Соединиться с разработчиками @@ -195,10 +194,10 @@ Не получается декодировать изображение. Пожалуйста, попробуйте другое изображение или свяжитесь с разработчиками. Изображение - Ожидается прием изображения + Ожидается приём изображения Предложено получить изображение Изображение отправлено - Ожидается прием изображения + Ожидается приём изображения Изображение будет принято, когда Ваш контакт будет в сети, подождите или проверьте позже! Изображение сохранено в Галерею @@ -206,9 +205,9 @@ Большой файл! Ваш контакт отправил файл, размер которого превышает поддерживаемый в настоящее время максимальный размер (%1$s). В настоящее время максимальный поддерживаемый размер файла составляет %1$s. - Ожидается прием файла + Ожидается приём файла Файл будет принят, когда Ваш контакт будет в сети, подождите или проверьте позже! - Файл сохранен + Файл сохранён Файл не найден Ошибка сохранения файла @@ -248,8 +247,8 @@ Начать новый разговор Создать ссылку-приглашение - Соединиться через ссылку / QR код - Сканировать\nQR код + Соединиться через ссылку или QR-код + Сканировать\nQR-код Создать секретную группу (чтобы отправить Вашему контакту) (сканировать или вставить из буфера) @@ -268,8 +267,8 @@ Нажмите кнопку сверху, затем: Чтобы соединиться через ссылку - Если Вы получили ссылку с приглашением из SimpleX Chat, Вы можете открыть ее в браузере: - Сканировать QR код.]]> + Если Вы получили ссылку с приглашением из SimpleX Chat, Вы можете открыть её в браузере: + Сканировать QR-код.]]> Open in mobile app на веб странице, затем нажмите Соединиться в приложении.]]> Принять запрос на соединение? @@ -298,7 +297,7 @@ Контакт, которому Вы отправили эту ссылку, не сможет соединиться! Подтвержденное соединение будет отменено! - Соединение еще не установлено! + Соединение ещё не установлено! Ваш контакт должен быть в сети, чтобы установить соединение. \nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой). @@ -311,7 +310,7 @@ изображение превью ссылки удалить превью ссылки Настройки - QR код + QR-код SimpleX адрес помощь SimpleX команда @@ -319,20 +318,20 @@ Email Больше - Показать QR код + Показать QR-код - Неверный QR код - Этот QR код не является ссылкой! + Неверный QR-код + Этот QR-код не является ссылкой! Неверная ссылка! Эта ссылка не является ссылкой-приглашением! Запрос на соединение послан! Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже! Соединение будет установлено, когда Ваш запрос будет принят. Пожалуйста, подождите или проверьте позже! - Соединение будет установлено, когда Ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже! - показать QR код во время видеозвонка или поделиться ссылкой.]]> + Соединение будет установлено, когда Ваш контакт будет в сети. Пожалуйста, подождите или проверьте позже. + показать QR-код во время видеозвонка или поделиться ссылкой.]]> Ваш профиль будет отправлен \nВашему контакту - сосканировать QR код во время видеозвонка, или Ваш контакт может отправить Вам ссылку.]]> + сосканировать QR-код во время видеозвонка, или Ваш контакт может отправить Вам ссылку.]]> Поделиться одноразовой ссылкой Соединиться @@ -351,7 +350,7 @@ Написать нам письмо Блокировка SimpleX Консоль - SMP серверы + SMP-серверы Адрес сервера по умолчанию Добавить серверы по умолчанию Добавить сервер @@ -360,7 +359,7 @@ Сохранить серверы Ошибка теста сервера! Серверы не прошли тест: - Сканировать QR код сервера + Сканировать QR-код сервера Ввести сервер вручную Сервер по умолчанию Ваш сервер @@ -372,28 +371,28 @@ Проверьте адрес сервера и попробуйте снова. Удалить сервер SimpleX Chat для терминала - Поставить звездочку в GitHub + Поставить звёздочку на GitHub Внести свой вклад Оценить приложение - Использовать серверы предосталенные SimpleX Chat? - Ваши SMP серверы + Использовать серверы, предосталенные SimpleX Chat? + Ваши SMP-серверы Используются серверы предоставленные SimpleX Chat. Инфо Как использовать серверы - Сохраненные WebRTC ICE серверы будут удалены. - Ваши ICE серверы - Настройка ICE серверов - ICE серверы (один на строке) - Ошибка при сохранении ICE серверов - Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется. + Сохранённые WebRTC ICE-серверы будут удалены. + Ваши ICE-серверы + Настройка ICE-серверов + ICE-серверы (один на строке) + Ошибка при сохранении ICE-серверов + Пожалуйста, проверьте, что адреса WebRTC ICE-серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется. Сохранить Сеть и серверы Настройки сети Настройки сети - Использовать SOCKS прокси? - Соединяться с серверами через SOCKS прокси через порт %d? Прокси должен быть запущен до включения этой опции. + Использовать SOCKS-прокси? + Соединяться с серверами через SOCKS-прокси через порт %d? Прокси должен быть запущен до включения этой опции. Использовать прямое соединение с Интернет? - Если Вы подтвердите, серверы смогут видеть Ваш IP адрес, а провайдер - с какими серверами Вы соединяетесь. + Если Вы подтвердите, серверы смогут видеть Ваш IP-адрес, а провайдер - с какими серверами Вы соединяетесь. Использовать .onion хосты Когда возможно Нет @@ -413,7 +412,7 @@ Имя профиля: Полное имя: Ваш активный профиль - Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю. + Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. Серверы SimpleX не могут получить доступ к Вашему профилю. Поменять аватар Удалить аватар Сохранить предпочтения? @@ -422,7 +421,7 @@ Сохранить и уведомить членов группы Выйти без сохранения - Вы котролируете Ваш чат! + Вы контролируете Ваш чат! Платформа для сообщений и приложений, которая защищает Вашу личную информацию и безопасность. Мы не храним Ваши контакты и сообщения (после доставки) на серверах. Создать профиль @@ -466,7 +465,7 @@ Будущее коммуникаций Более конфиденциальный Без идентификаторов пользователей. - Защищен от спама + Защищён от спама Вы определяете, кто может соединиться. Децентрализованный Кто угодно может запустить сервер. @@ -504,10 +503,10 @@ Принимать Показывать Выключить - Ваши ICE серверы - WebRTC ICE серверы - Relay сервер защищает Ваш IP адрес, но может отслеживать продолжительность звонка. - Relay сервер используется только при необходимости. Другая сторона может видеть Ваш IP адрес. + Ваши ICE-серверы + WebRTC ICE-серверы + Relay-сервер защищает Ваш IP-адрес, но может отслеживать продолжительность звонка. + Relay-сервер используется только при необходимости. Другая сторона может видеть Ваш IP-адрес. Откройте SimpleX Chat\nчтобы принять звонок Вы можете разрешить принимать звонки на экране блокировки через Настройки. @@ -537,7 +536,7 @@ Принять звонок %1$d пропущенных сообщений - ошибка хэш сообщения + ошибка хэша сообщения ошибка ID сообщения повторное сообщение Пропущенные сообщения @@ -549,7 +548,7 @@ Конфиденциальность Конфиденциальность Защитить экран приложения - Автоприем изображений + Автоприём изображений Отправлять картинки ссылок Резервная копия данных @@ -561,7 +560,7 @@ ЧАТЫ Инструменты разработчика Экспериментальные функции - SOCKS ПРОКСИ + SOCKS-ПРОКСИ ЗНАЧОК ТЕМЫ СООБЩЕНИЯ И ФАЙЛЫ @@ -634,22 +633,22 @@ Android Keystore используется для безопасного хранения пароля - это позволяет стабильно получать уведомления в фоновом режиме. База данных зашифрована случайным паролем, Вы можете его поменять. Внимание: Вы не сможете восстановить или поменять пароль, если потеряете его.]]> - Пароль базы данных будет безопасно сохранен в Android Keystore после запуска чата или изменения пароля - это позволит стабильно получать уведомления. - Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата. + Пароль базы данных будет безопасно сохранён в Android Keystore после запуска чата или изменения пароля - это позволит стабильно получать уведомления. + Пароль не сохранён на устройстве — Вы будете должны ввести его при каждом запуске чата. Зашифровать базу данных? Поменять пароль базы данных? База данных будет зашифрована. - База данных будет зашифрована и пароль сохранен в Keystore. - Пароль базы данных будет изменен и сохранен в Keystore. + База данных будет зашифрована и пароль сохранён в Keystore. + Пароль базы данных будет изменён и сохранён в Keystore. Пароль базы данных будет изменен. - Пожалуйста, надежно сохраните пароль, Вы НЕ сможете его поменять, если потеряете. - Пожалуйста, надежно сохраните пароль, Вы НЕ сможете открыть чат, если потеряете его. + Пожалуйста, надёжно сохраните пароль, Вы НЕ сможете его поменять, если потеряете. + Пожалуйста, надёжно сохраните пароль, Вы НЕ сможете открыть чат, если потеряете его. Неправильный пароль базы данных База данных зашифрована Ошибка базы данных Ошибка Keystore - Пароль базы данных отличается от сохраненного в Keystore. + Пароль базы данных отличается от сохранённого в Keystore. Файл: %s Введите пароль базы данных, чтобы открыть чат. Ошибка: %s @@ -679,18 +678,18 @@ Вступить Вступить инкогнито Вступление в группу - Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы. + Вы вступили в группу. Устанавливается соединение с пригласившим Вас членом группы. Выйти Выйти из группы Вы перестанете получать сообщения от этой группы. История чата будет сохранена. - Пригласить членов группы + Пригласить в группу Группа неактивна Приглашение истекло! Приглашение в группу больше не действительно, оно было удалено отправителем. Группа не найдена! Эта группа больше не существует. Нельзя пригласить контакты! - Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие Вашего основного профиля, приглашать контакты не разрешено + Вы используете профиль инкогнито в этой группе. Для защиты Вашего основного профиля приглашать контакты запрещено. Вы отправили приглашение в группу Вы приглашены в группу @@ -741,7 +740,7 @@ соединяется Нет контактов для добавления - Роль члена группы + Роль нового члена группы Развернуть выбор роли Пригласить в группу Не приглашать членов @@ -751,10 +750,10 @@ Выбрано контактов: %d Контакты не выбраны Нельзя пригласить контакт! - Вы пытаетесь пригласить инкогнито контакт в группу, где Вы используете свой основной профиль + Вы пытаетесь пригласить контакт, который знает Ваш профиль инкогнито, в группу, где Вы используете основной профиль. - Пригласить членов группы - УЧАСТНИКОВ ГРУППЫ: %1$s + Пригласить в группу + %1$s ЧЛЕНОВ ГРУППЫ Вы: %1$s Удалить группу Удалить группу? @@ -766,7 +765,7 @@ Создать ссылку Удалить ссылку? Удалить ссылку - Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились. + Вы можете поделиться ссылкой или QR-кодом — любой сможет присоединиться к группе. Члены группы останутся, даже если вы позже удалите ссылку. Все члены группы, которые соединились через эту ссылку, останутся в группе. Ошибка при создании ссылки группы Ошибка при удалении ссылки группы @@ -778,7 +777,7 @@ Удалить члена группы Отправить сообщение - Член группы будет удален - это действие нельзя отменить! + Член группы будет удалён - это действие нельзя отменить. Удалить ЧЛЕН ГРУППЫ Роль @@ -824,13 +823,13 @@ Инкогнито Случайный профиль - Режим Инкогнито защищает Вашу конфиденциальность — для каждого контакта создается новый случайный профиль. + Режим Инкогнито защищает Вашу конфиденциальность — для каждого контакта создаётся новый случайный профиль. Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя. - Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом. + Когда Вы соединены с контактом инкогнито, тот же самый профиль инкогнито будет использоваться для групп с этим контактом. Системная Светлая - Темная + Тёмная Тема Сбросить цвета @@ -876,7 +875,7 @@ Разрешить необратимо удалять отправленные сообщения. (24 часа) Запретить необратимое удаление сообщений. Разрешить отправлять голосовые сообщения. - Запретить отправлять голосовые сообщений. + Запретить отправлять голосовые сообщения. Члены могут посылать прямые сообщения. Прямые сообщения между членами группы запрещены. Члены могут необратимо удалять отправленные сообщения. (24 часа) @@ -948,7 +947,7 @@ Макс. 40 секунд, доставляются мгновенно. Окончательное удаление сообщений Ваши контакты могут разрешить окончательное удаление сообщений. - Добавить серверы через QR код. + Добавить серверы через QR-код. Улучшенная безопасность Скрыть экран приложения. Исчезающие сообщения @@ -1005,7 +1004,7 @@ Черновик сообщения Много профилей чата Сохранить последний черновик, вместе с вложениями. - Защищенные имена файлов + Защищённые имена файлов Благодаря пользователям – добавьте переводы через Weblate! Чтобы защитить Ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC. Французский интерфейс @@ -1014,12 +1013,12 @@ Отдельные транспортные сессии модерировано модерировано %s - Удалить сообщение участника\? + Удалить сообщение члена группы\? Модерировать Сообщение будет удалено для всех членов группы. - Сообщение будет помечено как удаленное для всех членов группы. + Сообщение будет помечено как удалённое для всех членов группы. Пожалуйста, свяжитесь с админом группы. - Вы не можете отправлять сообщения! + Вы \"читатель\" только чтение сообщений читатель Роль при вступлении @@ -1052,14 +1051,12 @@ Без звука Сделайте профиль конфиденциальным! Дополнительные улучшения скоро! - Теперь админы могут: -\n- удалять сообщения членов. -\n- приостанавливать членов (роль "наблюдатель") + Теперь админы могут: \n- удалять сообщения членов группы. \n- приостанавливать членов группы (роль наблюдатель) Защитите Ваши профили чата паролем! Раскрыть Поддержка bluetooth и другие улучшения. Сохранить приветственное сообщение\? - Установить сообщение для новых членов группы! + Установите приветственное сообщение для новых членов группы. Нажмите на профиль, чтобы переключиться на него. Благодаря пользователям - добавьте переводы через Weblate! Вы все равно получите звонки и уведомления в профилях без звука, когда они активные. @@ -1079,12 +1076,12 @@ Откатить версию и открыть чат Предупреждение: Вы можете потерять какие то данные! ID базы данных и опция Отдельные транспортные сессии. - Показать опции для девелоперов + Показать опции для разработчиков Удалить профиль чата Удалить профиль Пароль профиля Слишком много видео! - Запросил прием видео + Запросил приём видео Видео Видео отправлено Ожидание видео @@ -1096,31 +1093,31 @@ Раскрыть профиль Видео будет получено, когда Ваш контакт будет онлайн, пожалуйста, подождите или проверьте позже! Раскрыть профиль чата - Ошибка при загрузке SMP серверов - Ошибка при загрузке XFTP серверов - Ошибка при сохранении XFTP серверов - Проверьте, что адреса XFTP северов в правильном формате и не дублируются. + Ошибка при загрузке SMP-серверов + Ошибка при загрузке XFTP-серверов + Ошибка при сохранении XFTP-серверов + Проверьте, что адреса XFTP-серверов указаны в правильном формате и не дублируются. Сервер требует авторизации для загрузки, проверьте пароль. Сравнение файла Удалить файл Загрузка файла Загрузка файла - XFTP серверы - Ваши XFTP серверы - Использовать .onion хосты в Нет если SOCKS прокси их не поддерживает.]]> + XFTP-серверы + Ваши XFTP-серверы + Использовать .onion хосты в Нет если SOCKS-прокси их не поддерживает.]]> Ошибка аутентификации Нет кода доступа Ввод кода доступа Режим Блокировки SimpleX Системная аутентификация - Ошибка аутентификации; попробуйте еще раз. + Ошибка аутентификации; попробуйте ещё раз. Сразу Аутентифицировать Изменить код доступа Текущий Код Введите Код - Настройки SOCKS прокси - Использовать SOCKS прокси + Настройки SOCKS-прокси + Использовать SOCKS-прокси Хост Порт порт %d @@ -1131,7 +1128,7 @@ %1$d сообщений не удалось расшифровать. Ошибка расшифровки Блокировка SimpleX не включена! - Ошибка хэш сообщения + Ошибка хэша сообщения Хэш предыдущего сообщения отличается. Подтвердить код Неправильный код @@ -1153,7 +1150,7 @@ Остановить отправку файла\? Ошибка ID сообщения Это может произойти, когда Вы или Ваш контакт используете старую копию базы данных. - Пожалуйста, сообщите об этой ошибке девелоперам. + Пожалуйста, сообщите об этой ошибке разработчикам. Неправильный ID предыдущего сообщения (меньше или равен предыдущему). \nЭто может произойти из-за ошибки программы, или когда соединение компроментировано. %1$d сообщений пропущено. @@ -1201,7 +1198,7 @@ Пользовательское время Отправить Пригласить друзей - Сохранить настройки автоприема + Сохранить настройки адреса SimpleX Изменить режим самоуничтожения Изменить код самоуничтожения Самоуничтожение @@ -1251,7 +1248,7 @@ Создать адрес SimpleX Поделиться с контактами Прекратить делиться адресом\? - Автоприем + Автоприём Введите приветственное сообщение... (опционально) Сохранить настройки\? Прекратить делиться @@ -1264,7 +1261,7 @@ Просмотр Поделиться адресом Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с %s. - Темная тема + Тёмная тема Импорт темы Ошибка импорта темы SimpleX @@ -1335,6 +1332,7 @@ Нет истории Отправка отчётов о доставке включена для %d контактов. Отправка отчётов о доставке будет включена для всех контактов во всех видимых профилях чата. + Установка для Вашего активного профиля Установки для Вашего активного профиля Отправка отчётов о доставке выключена для %d контактов. Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения! @@ -1361,6 +1359,7 @@ Включить (кроме исключений) Выключить отчёты о доставке\? ОТПРАВКА ОТЧЁТОВ О ДОСТАВКЕ + ЗАПРОСЫ НА СОЕДИНЕНИЕ ИЗ ГРУПП шифрование согласовано шифрование согласовано для %s шифрование работает @@ -1370,10 +1369,10 @@ %s в %s Починить соединение Починка не поддерживается контактом. - Починка не поддерживается членом группы. + Восстановление шифрования не поддерживается членом группы Пересогласовать шифрование Быстрый поиск чатов - Отчеты о доставке сообщений! + Отчёты о доставке сообщений! Еще несколько изменений Отчёты о доставке! Включить @@ -1393,8 +1392,8 @@ Нет отфильтрованных разговоров Пересогласовать Пересогласовать шифрование\? - Запретить слать файлы и медиа. - Соединиться Инкогнито + Запретить посылать файлы и медиа. + Соединиться Инкогнито Разрешить Открыть настройки приложения Выключить уведомления @@ -1420,7 +1419,7 @@ выключено Эта функция ещё не поддерживается. Проверьте в следующем релизе. Соединиться напрямую\? - Этому члену группы будет отправлен запрос на соединение. + Запрос на соединение будет отправлен этому члену группы. Отчёты о доставке включены для %d групп Показывать последние сообщения Скоро! @@ -1432,11 +1431,11 @@ Черновик сообщения Расход батареи приложением / Без ограничений в настройках приложения.]]> Использовать активный профиль - Использовать новый Инкогнито профиль + Использовать новый профиль инкогнито Расход батареи приложением / Без ограничений в настройках приложения.]]> - База данных будет зашифрована, и пароль сохранен в настройках. - Шифруйте сохраненные файлы и медиа - Обратите внимание: соединение с серверами файлов и сообщений устанавливаются через SOCKS прокси. Звонки и картинки ссылок используют прямое соединение.]]> + База данных будет зашифрована, и пароль сохранён в настройках. + Шифруйте сохранённые файлы и медиа + Обратите внимание: соединение с серверами файлов и сообщений устанавливаются через SOCKS-прокси. Звонки и картинки ссылок используют прямое соединение.]]> Шифровать локальные файлы Приложение для компьютера! 6 новых языков интерфейса @@ -1448,7 +1447,7 @@ Удалить пароль из настроек\? Использовать случайный пароль Сохранить пароль в настройках - Упрощенный режим Инкогнито + Упрощённый режим Инкогнито Установить пароль базы данных Установить пароль базы данных Открыть директорию базы данных @@ -1456,21 +1455,20 @@ Создайте новый профиль в приложении для компьютера. 💻 Пароль будет сохранён в настройках как простой текст после того, как вы его измените или перезапустите приложение. Установите режим Инкогнито при соединении. - - соединиться с каталогом групп (BETA)! -\n- отчеты о доставке (до 20 членов). -\n- быстрее и стабильнее. + - соединиться с каталогом групп (BETA)! \n- отчёты о доставке (до 20 членов группы). \n- быстрее и стабильнее. Пароль хранится в настройках, как открытый текст. Открыть Ошибка при создании контакта Послать прямое сообщение контакту Ошибка отправки приглашения - Послать прямое сообщение - соединен напрямую + Отправьте сообщение чтобы соединиться + запрос на соединение Раскрыть Блокируйте членов группы Повторить запрос на соединение? Ошибка нового соглашения о шифровании удалил(а) контакт + запрос на соединение из группы %1$s Ошибка Создайте группу, используя случайный профиль. Создать группу @@ -1484,7 +1482,7 @@ Свяжите мобильное и настольное приложения! 🔗 %d сообщений помечено удалёнными Группа уже существует! - Использовать с компьютера в мобильном приложении и сосканируйте QR код.]]> + Использовать с компьютера в мобильном приложении и сосканируйте QR-код.]]> Уже соединяется! Несовместимая версия (новое)]]> @@ -1494,11 +1492,11 @@ Обнаружение по локальной сети и %d других событий Соединиться через ссылку? - Инкогнито группы + Группы инкогнито Вступление в группу уже начато! - %1$d сообщений модерировано членом %2$s + %1$d сообщений отмодерировано членом %2$s %s был отключен]]> - Быстрое вступление и надежная доставка сообщений. + Быстрое вступление и надёжная доставка сообщений. Соединиться с самим собой? Связанные мобильные Компьютер @@ -1570,7 +1568,7 @@ Указан неверный путь к файлу. Сообщите о проблеме разработчикам приложения. %1$s!]]> Сверьте код с компьютером - Сканировать QR код с компьютера + Сканировать QR-код с компьютера Разблокировать Вы уже запросили соединение через этот адрес! Показывать консоль в новом окне @@ -1604,13 +1602,13 @@ Включить доступ к камере Нажмите, чтобы сканировать Создать группу: создать новую группу.]]> - Не отправлять историю новым членам. - Отправить до 100 последних сообщений новым членам. + Не отправлять историю новым членам группы. + Отправить до 100 последних сообщений новым членам группы. Все сообщения будут удалены - это нельзя отменить! Камера недоступна Код доступа в приложение Добавить контакт: создать новую ссылку-приглашение или подключиться через полученную ссылку.]]> - Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата. + Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите её обратно до запуска чата. Соединение остановлено Создано Оставить @@ -1625,7 +1623,7 @@ Ошибка удаления заметки Венгерский и Турецкий интерфейс Искать или вставьте ссылку SimpleX - Этот QR код не является SimpleX-ccылкой. + Этот QR-код не является SimpleX-ccылкой. С зашифрованными файлами и медиа. С уменьшенным потреблением батареи. Оставить неиспользованное приглашение? @@ -1635,8 +1633,8 @@ Запустить чат? Личные заметки Доступ к истории - История не отправляется новым членам. - До 100 последних сообщений отправляются новым членам. + История не отправляется новым членам группы. + До 100 последних сообщений отправляются новым членам группы. Показывать внутренние ошибки Ошибка соединения с компьютером %s]]> @@ -1656,7 +1654,7 @@ Вставленный текст не является SimpleX-ссылкой. Создаётся ссылка… Нажмите, чтобы вставить ссылку - Ошибка QR кода + Ошибка QR-кода Поделиться одноразовой ссылкой-приглашением Повторить Ошибка приложения @@ -1677,11 +1675,11 @@ Очистить личные заметки? Новый чат Новое сообщение - Или отсканируйте QR код + Или отсканируйте QR-код Вы можете увидеть ссылку-приглашение снова открыв соединение. Показывать медленные вызовы API - Бывший член %1$s - Сохраненное сообщение + Член группы %1$s + Сохранённое сообщение неизвестно неизвестный статус %d сообщений заблокировано администратором @@ -1689,17 +1687,17 @@ %s разблокирован Вы разблокировали %s Разблокировать для всех - Заблокировать члена для всех? + Заблокировать члена группы для всех? заблокирован заблокировано администратором Заблокирован администратором Заблокировать для всех - Ошибка при блокировании члена для всех - Разблокировать члена для всех? + Ошибка при блокировании члена группы для всех + Разблокировать члена группы для всех? Вы заблокировали %s end-to-end шифрованием с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.]]> - Чат защищен end-to-end шифрованием. - Чат защищен квантово-устойчивым end-to-end шифрованием. + Чат защищён end-to-end шифрованием. + Чат защищён квантово-устойчивым end-to-end шифрованием. Открыть экран миграции Миграция с другого устройства Установить пароль @@ -1707,7 +1705,7 @@ Приветственное сообщение слишком длинное Сообщение слишком большое Повторить загрузку - Вы можете попробовать еще раз. + Вы можете попробовать ещё раз. Загрузка архива Подготовка архива %s загружено @@ -1741,12 +1739,12 @@ Остановка чата Архивировать и загрузить Подтвердить загрузку - Все ваши контакты, разговоры и файлы будут надежно зашифрованы и загружены на выбранные XFTP серверы. + Все ваши контакты, разговоры и файлы будут надёжно зашифрованы и загружены на выбранные XFTP-серверы. Ошибка загрузки Повторить загрузку - Вы можете попробовать еще раз. + Вы можете попробовать ещё раз. Отменить миграцию - Мигрировать с другого устройства на новом устройстве и сосканируйте QR код.]]> + Мигрировать с другого устройства на новом устройстве и сосканируйте QR-код.]]> Создание ссылки на архив Удалить базу данных с этого устройства Завершить миграцию @@ -1755,7 +1753,7 @@ Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений. не должны использовать одну и ту же базу данных на двух устройствах.]]> Проверьте подключение к Интернету и повторите попытку - Подтвердите, что Вы помните пароль базы данных для ее миграции. + Подтвердите, что Вы помните пароль базы данных для её миграции. Проверка пароля базы данных Проверить пароль Ошибка подтверждения пароля: @@ -1769,7 +1767,7 @@ квантово-устойчивым end-to-end шифрованием с идеальной прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.]]> Мигрировать сюда Мигрировать на другое устройство - Мигрируйте на другое устройство через QR код. + Мигрируйте на другое устройство через QR-код. Или вставьте ссылку архива Звонки с картинкой-в-картинке квантово-устойчивое e2e шифрование @@ -1803,7 +1801,7 @@ Наушники Громкоговоритель Звуки во время звонков - Более надежное соединение с сетью. + Более надёжное соединение с сетью. Статус сети сохранено сохранено из %s @@ -1817,7 +1815,7 @@ Ссылки SimpleX Разрешить отправлять ссылки SimpleX. Запретить отправку ссылок SimpleX - Члены могут отправлять ссылки SimpleX + Члены могут отправлять SimpleX ссылки. админы все члены владельцы @@ -1863,9 +1861,9 @@ Показать список чатов в новом окне Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов или когда SOCKS-прокси включен). Незащищённый - Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов. - Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. - Черная + Без Tor или VPN, Ваш IP-адрес будет доступен серверам файлов. + Отправлять сообщения напрямую, когда IP-адрес защищён, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + Чёрная Тёмный режим Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку. Режим цветов @@ -1881,23 +1879,22 @@ Сделайте ваши чаты разными! Информация об очереди сообщений Персидский интерфейс - Защитить IP адрес - Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. \nВключите в настройках Сети и серверов. + Защитить IP-адрес + Защитите ваш IP-адрес от серверов сообщений, выбранных Вашими контактами. \nВключите в настройках Сеть и серверы. Отправьте сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. Конфиденциальная доставка Использовать конфиденциальную доставку с неизвестными серверами. - Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен. - Когда IP защищен + Использовать конфиденциальную доставку с неизвестными серверами, когда IP-адрес не защищён. + Когда IP защищён Да - Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений. + Чтобы защитить Ваш IP-адрес, приложение использует Ваши SMP-серверы для конфиденциальной доставки сообщений. Изображения профилей Все режимы Тема приложения Сбросить на тему приложения Сбросить на тему пользователя Неизвестные серверы! - Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: -\n%1$s. + Без Tor или VPN, Ваш IP-адрес будет доступен этим серверам файлов: \n%1$s. Не использовать конфиденциальную маршрутизацию. Никогда Неизвестные серверы @@ -1925,7 +1922,7 @@ Рисунок обоев Фон обоев Применить к - Не удается отправить сообщение + Не удаётся отправить сообщение Бета Соединeно попытки @@ -1936,11 +1933,11 @@ Проверка на наличие обновлений Разрешить звонки? Звонки запрещены! - Не удается позвонить члену группы + Не удаётся позвонить члену группы Обновление скачано звонок - Не удается позвонить контакту - Не удается написать члену группы + Не удаётся позвонить контакту + Не удаётся написать члену группы Проверять обновления соединиться Адрес сервера назначения %1$s несовместим с настройками пересылающего сервера %2$s. @@ -1952,8 +1949,8 @@ Выбранные настройки чата запрещают это сообщение. Ошибка файла Сканировать / Вставить ссылку - Другие XFTP серверы - Настроенные XFTP серверы + Другие XFTP-серверы + Настроенные XFTP-серверы Загрузка %s (%s) Доступно обновление: %s Выключить @@ -1966,15 +1963,15 @@ Подробности Всего Активные соединения - Прием сообщений + Приём сообщений В ожидании Загружено Статистика серверов будет сброшена - это нельзя отменить! Всего отправлено Переподключить - SMP сервер + SMP-сервер Начиная с %s. - XFTP сервер + XFTP-сервер дубликаты истекло другое @@ -1987,7 +1984,7 @@ Блоков загружено Ошибки загрузки Размер - Ошибки приема + Ошибки приёма Принятые файлы Адрес сервера Ошибка сервера файлов: %1$s @@ -2003,7 +2000,7 @@ Блоков удалено Блоков принято Подписок игнорировано - Ошибка копирования + Скопировать ошибку видеозвонок Контакт будет удален — это нельзя отменить! Оставить разговор @@ -2014,7 +2011,7 @@ Нет отфильтрованных контактов Ваши контакты Архивированные контакты - Настроенные SMP серверы + Настроенные SMP-серверы Показать процент Слабое Среднее @@ -2027,8 +2024,7 @@ Подключенные серверы Ранее подключенные серверы Проксированные серверы - Начиная с %s. -\nВсе данные хранятся только на вашем устройстве. + Начиная с %s.\nВсе данные хранятся только на вашем устройстве. Переподключить сервер для устранения неполадок доставки сообщений. Это использует дополнительный трафик. Ошибка Ошибка переподключения к серверу @@ -2048,10 +2044,10 @@ Сообщения будут помечены на удаление. Получатель(и) смогут посмотреть эти сообщения. Выбрать Сообщения будут удалены для всех членов группы. - Сообщения будут помечены как удаленные для всех членов группы. + Сообщения будут помечены как удалённые для всех членов группы. Контакт удален! Разговор удален! - Член неактивен + Член группы неактивен Прямого соединения пока нет, сообщение переслано или будет переслано админом. Ничего не выбрано открыть @@ -2059,7 +2055,7 @@ Выбрано %d Настройки Вы по-прежнему можете просмотреть разговор с %1$s в списке чатов. - Другие SMP серверы + Другие SMP-серверы Выключено Установлено успешно Установить обновление @@ -2073,7 +2069,7 @@ Продолжить Серверы файлов и медиа Серверы сообщений - SOCKS прокси + SOCKS-прокси Некоторые файл(ы) не были экспортированы Вы можете мигрировать экспортированную базу данных. Вы можете сохранить экспортированный архив. @@ -2102,10 +2098,10 @@ Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка. Соединяйтесь с друзьями быстрее Управляйте своей сетью - Защищает ваш IP адрес и соединения. + Защищает ваш IP-адрес и соединения. Открыть настройки серверов Полученные сообщения - Ошибки приема + Ошибки приёма Архивируйте контакты чтобы продолжить переписку. Отправлено напрямую Отправлено через прокси @@ -2142,7 +2138,7 @@ Сохранение %1$s сообщений Убедитесь, что конфигурация прокси правильная. Аутентификация прокси - Использовать случайные учетные данные + Использовать случайные учётные данные Режим системы Ошибка пересылки сообщений %1$d ошибок файлов:\n%2$s @@ -2167,11 +2163,11 @@ Ошибка переключения профиля Выберите профиль чата Поделиться профилем - Соединение было перемещено на %s, но при смене профиля произошла неожиданная ошибка. + Соединение было перемещено в профиль %s, но при переключении профиля произошла ошибка. Угол Сессия приложения - Новые учетные данные SOCKS будут использоваться при каждом запуске приложения. - Новые учетные данные SOCKS будут использоваться для каждого сервера. + Новые учётные данные SOCKS будут использоваться при каждом запуске приложения. + Новые учётные данные SOCKS будут использоваться для каждого сервера. Сервер Форма сообщений Хвост @@ -2188,23 +2184,23 @@ Переключайте профиль чата для одноразовых приглашений. Аудит SimpleX протоколов от Trail of Bits. Чтобы совершать звонки, разрешите использовать микрофон. Завершите вызов и попробуйте позвонить снова. - Не использовать учетные данные с прокси. + Не использовать учётные данные с прокси. Ошибка сохранения прокси Пароль - Использовать разные учетные данные прокси для каждого соединения. - Использовать разные учетные данные прокси для каждого профиля. + Использовать разные учётные данные прокси для каждого соединения. + Использовать разные учётные данные прокси для каждого профиля. Имя пользователя - Ваши учетные данные могут быть отправлены в незашифрованном виде. + Ваши учётные данные могут быть отправлены в незашифрованном виде. Удалить архив? Загруженный архив базы данных будет навсегда удален с серверов. Принятые условия Принять условия Нет серверов сообщений. - Нет серверов для приема сообщений. + Нет серверов для приёма сообщений. Ошибки в настройках серверов. Для профиля %s: Нет серверов файлов и медиа. - Нет серверов для приема файлов. + Нет серверов для приёма файлов. Нет серверов для отправки файлов. Недоставленные сообщения Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. @@ -2232,13 +2228,13 @@ Прозрачность Децентрализация сети Второй оператор серверов в приложении! - Включить Flux + Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных. для лучшей конфиденциальности метаданных. Улучшенная навигация в разговоре Посмотреть измененные условия Устройства Xiaomi: пожалуйста, включите опцию Autostart в системных настройках для работы нотификаций.]]> Нет сообщения - Это сообщение было удалено или еще не получено. + Это сообщение было удалено или ещё не получено. Сообщение слишком большое! Пожалуйста, уменьшите размер сообщения и отправьте снова. Пожалуйста, уменьшите размер сообщения или уберите медиа и отправьте снова. @@ -2293,12 +2289,12 @@ Только владельцы разговора могут поменять предпочтения. Текст условий использования не может быть показан, вы можете посмотреть их через ссылку: Разговор - Член будет удален из разговора - это действие нельзя отменить! + Участник будет удалён из разговора - это действие нельзя отменить. Серверы по умолчанию Роль будет изменена на %s. Все участники разговора получат уведомление. Ваш профиль будет отправлен участникам разговора. - %s.]]> - %s.]]> + %s.]]> + %s.]]> Условия использования Дополнительные серверы файлов и медиа Ошибка сохранения сервера @@ -2310,7 +2306,7 @@ Для получения Использовать для сообщений Размыть - Прямые сообщения между членами запрещены. + Прямые сообщения между членами группы запрещены. Бизнес разговоры - Открывает разговор на первом непрочитанном сообщении.\n- Перейти к цитируемому сообщению. Конфиденциальность для ваших покупателей. @@ -2320,13 +2316,303 @@ Нет серверов для доставки сообщений. Вы можете настроить серверы позже. SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение. - Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре. + Приложение защищает вашу конфиденциальность, используя разные операторы в каждом разговоре. Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем. Ошибка сохранения серверов Условия будут приняты для включенных операторов через 30 дней. - Ошибка приема условий + Ошибка приёма условий Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети. Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности. - Например, если Ваш контакт получает сообщения через сервер SimpleX Chat, Ваше приложение доставит их через сервер Flux. - Прямые сообщения между членами запрещены в этом разговоре. - \ No newline at end of file + Например, если ваш контакт получает сообщения через сервер 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- уважать других пользователей – не отправлять спам. + Частные разговоры, группы и Ваши контакты недоступны для операторов серверов. + Настроить операторов серверов + Политика конфиденциальности и условия использования. + всех + Принять + Член группы хочет присоединиться. Принять? + группа удалена + удален из группы + %d чата(ов) + контакт не готов + контакт удален + не синхронизирован + запрос на вступление отклонён + Новый член группы хочет присоединиться. + Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление. + ожидает одобрения + Отклонить + Отклонить члена группы? + Ошибка при удалении чата + Полная ссылка + Ошибка при вступлении члена группы + Ссылка не поддерживается + Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку. + %d сообщений + Вы можете найти Ваши жалобы в Чате с админами. + Чат с админами + Чат с членом группы + выключено + Одобрять членов группы + Чаты с членами группы + Приём членов в группу + Вручную одобрять членов для вступления в группу. + Нет чатов с членами группы + Принять как читателя + Принять в группу + Принять члена группы + одобрен админами + Жалоба отправлена модераторам + Вы вышли + нельзя отправлять + %d чатов с членами группы + контакт выключен + член группы имеет старую версию + Вы не можете отправлять сообщения! + Короткая ссылка + Сохранить настройки вступления? + Вы приняли этого члена группы + рассмотрение + Установить вступление в группу + Удалить чат с членом группы? + Удалить разговор + принял %1$s + Чат с админами + Вы приняты + 1 чат с членом группы + SimpleX ссылка канала + Обновить адрес + Принять запрос на соединение + Добавить сообщение + О себе: + Нельзя поменять профиль + end-to-end шифрованием.]]> + только после того как Ваш запрос будет принят.]]> + Чат с админами + Общайтесь с членами группы до того как принять их. + Соединиться + Соединяйтесь быстрее! 🚀 + контакт должен принять… + Ошибка изменения профиля + Ошибка при открытии чата + Ошибка при открытии группы + Ошибка отклонения запроса + Вступить в группу + Меньше трафик в мобильных сетях. + Загрузка профиля… + Отправляйте сообщения сразу после соединения. + Новая роль в группах: Модератор + Нет сессии конфиденциальной доставки + Открыть чат + Открыть новый чат + Открыть новую группу + Откройте чтобы принять + Откройте чтобы соединиться + Откройте чтобы вступить + Таймаут конфиденциальной доставки + Адрес будет коротким, и Ваш профиль будет добавлен в адрес. + Фоновый таймаут протокола + Отклонить запрос на соединение + Может удалять сообщения и блокировать членов группы. + запрос отправлен + Одобрять членов группы + Отправить запрос на соединение? + Отправить запрос + Отправить запрос без сообщения + Отправляйте Ваши конфиденциальные предложения группе. + Отправляется Вашему контакту после соединения. + Обновить ссылку группы? + Обновить + Обновить адрес? + Цель: + Фоновый таймаут TCP-соединения + Отправитель не будет уведомлён. + Член группы удалён - невозможно принять запрос + Чтобы использовать другой профиль после попытки соединения, удалите чат и используйте ссылку снова. + Приветственное сообщение + О Вас: + Ваш профиль + Описание слишком длинное + Использовать профиль инкогнито + 4 новых языков интерфейса + Принять запрос на соединение + Бизнес контакт + Каталонский, Индонезийский, Румынский и Вьетнамский - благодаря нашим пользователям! + Создайте Ваш адрес + Описание слишком длинное + Включите исчезающие сообщения по умолчанию. + Группа + Очищайте Ваши чаты + Добавьте описание и приветственное сообщение. + Поделиться старым адресом + Поделиться старой ссылкой + Поделитесь Вашим адресом + Короткий адрес SimpleX + Нажмите Соединиться + Нажмите Соединиться, чтобы отправить запрос + Нажмите Вступить в группу + Ссылка будет короткой, и профиль группы будет добавлен в ссылку. + Время удаления устанавливается только для новых контактов. + Обновите Ваш адрес + Обновить ссылку группы + Приветствуйте Ваши контакты 👋 + Ваш бизнес контакт + Ваш контакт + Ваша группа + Разрешить файлы и медиа, только если их разрешает Ваш контакт. + Разрешить Вашим контактам отправлять файлы и медиа. + Бот + Вы и Ваш контакт можете отправлять файлы и медиа. + Файлы и медиа запрещены в этом чате. + Только Вы можете отправлять файлы и медиа. + Только Ваш контакт может отправлять файлы и медиа. + Откройте чтобы использовать бот + Запретить отправлять файлы и медиа. + Нажмите Соединиться, чтобы использовать бот. + Вы должны быть соединены, чтобы отправлять команды. + Удалённые настройки + Открыть очищенную ссылку + Открыть полную ссылку + Удалять параметры отслеживания + Ошибка прочтения чата + Хэш в адресе пересылающего сервера не соответствует сертификату: %1$s. + Хэш в адресе сервера не соответствует сертификату: %1$s. + Ссылка SimpleX relay + Хэш в адресе сервера назначения не соответствует сертификату: %1$s. + 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 ebf57836b5..411cbde4c4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -1043,7 +1043,7 @@ คุณได้รับเชิญให้เข้าร่วมกลุ่ม โปรไฟล์แบบสุ่มของคุณ ธีม - กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %1$s) + กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %1$s) SimpleX คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ โปรไฟล์ของคุณจะถูกส่งไปยังผู้ติดต่อที่ส่งลิงก์นี้มาให้คุณ @@ -1328,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 503d82158f..501f07ea50 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -14,7 +14,7 @@ Görüntülü ara Gizlilik ve güvenlik Gizli - Konuşmalar + Sohbetler Bağlantı ile bağlan Sohbet profillerin Konuşma tercihleri @@ -87,13 +87,13 @@ WebRTC ICE sunucu adreslerinin doğru formatta olduğundan emin olun: Satırlara ayrılmış ve yinelenmemiş şekilde. Kaydet ARAYÜZ RENKLERİ - Otomatik-kabul ayarlarını kaydet + SimpleX adres 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\ngelecek kuşağı + Mesajlaşmanın geleceği Ses kapalı Doğrulama iptal edildi Yeniden başlat @@ -264,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) @@ -288,7 +288,7 @@ Üyeyi çıkar Kaldır ARAMALAR - KONUŞMALAR + SOHBETLER SEN SOHBET VERİTABANI Kaldır @@ -301,7 +301,7 @@ sen: %1$s kaldırıldı ÜYE - Grup üyeleri kendiliğinden yok olan mesajlar gönderebilir. + Üyeler kendiliğinden yok olan mesajlar gönderebilir. Kendiliğinden yok olan mesaj gönderimini engelle. Yalnızca kişiniz sesli mesaj göndermeye izin veriyorsa sen de ver. Kişilerinin sesli mesaj göndermesine izin ver. @@ -605,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 @@ -647,11 +647,11 @@ Grup adını gir: Grup tam adı: Dosya ve medya - Grup üyeleri doğrudan mesaj gönderebilir. - Grup üyeleri, 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. - Grup üyeleri dosya ve medya paylaşabilir. + Üyeler doğrudan mesaj gönderebilir. + Üyeler, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) + Üyeler sesli mesaj gönderebilirler. + Dosya ve medya yasaklanmıştır. + Üyeler dosya ve medya paylaşabilir. Grup bağlantıları Konuşmada devre dışı bırakıldığında bile Çabuk ve göndericinin çevrim içi olmasını beklemeden! @@ -690,13 +690,13 @@ 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 Sesli mesajlar - 5 dakikaya kadar sesli mesajlar. -\n- özel mesaj kaybolma süreleri ayarlama +\n- özel mesaj kaybolma süreleri ayarlama \n- mesag düzenleme geçmişi. hafta Kişi zaten mevcut @@ -704,12 +704,12 @@ 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. Şuna cevap olarak - %s olarak katıl + %s olarak katılın Geçersiz link! Geçersiz QR kodu Geçersiz sunucu adresi! @@ -730,7 +730,7 @@ 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 @@ -758,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 @@ -835,15 +835,15 @@ 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. SimpleX Kilidini Ayarlar üzerinden açabilirsiniz. - gruba davet edildiniz + Gruba davetlisiniz Hiç sohbetiniz yok Gözlemcisiniz - Mesajlar gönderemezsiniz! + sen gözlemcisin Güvenlik kodunu görüntüle Sesli mesaj gönderebilmeniz için kişinizin de sesli mesaj göndermesine izin vermeniz gerekir. XFTP sunucuları @@ -931,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 @@ -940,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 @@ -949,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! @@ -1022,8 +1022,8 @@ 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. - Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s). + 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 Mesajlarda Markdown @@ -1201,7 +1201,7 @@ Mobilden tara Bağlantıları onayla Mesaj paylaş… - Önceki mesajın hash\'i farklı. + Önceki mesajın özeti veya karması farklı. SimpleX Kilit aktif değil! SimpleX Kilit Doğrudan bağlanılsın mı? @@ -1235,7 +1235,7 @@ Masaüstüne bağlan Doğrudan mesaj gönder Bu adres üzerinden zaten bağlantı talebinde bulundunuz! - doğrudan mesaj gönder + bağlanmak için gönder Gönderen kişi dosya aktarımını iptal etti. Mesajları yalnızca siz geri döndürülemez şekilde silebilirsiniz (kişiniz bunları silinmek üzere işaretleyebilir). (24 saat içinde) %1$s’ye bağlanıyorsunuz.]]> @@ -1713,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 @@ -1732,7 +1732,7 @@ SimpleX bağlantılarına izin verilmiyor Dosyalar ve medyaya izin verilmiyor Sesli mesajlara izin verilmiyor - Grup üyeleri SimpleX bağlantıları gönderebilir. + Üyeler SimpleX bağlantıları gönderebilir. iletildi Ağ bağlantısı SimpleX bağlantıları göndermesine izin ver. @@ -2032,7 +2032,7 @@ 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. + Bağlantınız %s\'e taşındı, ancak profil geçişinde bir hata gerçekleşti. Proxy kimlik doğrulaması Rastgele kimlik bilgileri kullan Her bağlantı için farklı proxy kimlik bilgileri kullan. @@ -2053,14 +2053,14 @@ Ses kapatıldı Yüklendi Sunucu istatistikleri sıfırlanacaktır - bu geri alınamaz! - Erişilebilir sohbet araç çubuğu + 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ıza özeldir. + %s\'den başlayarak.\nTüm veriler cihazınızda gizli tutulur. Bir proxy aracılığıyla gönderildi Sunucu adresi Yükleme hataları @@ -2124,4 +2124,389 @@ sadece bir kişiyle kullanılabilir - yüz yüze veya herhangi bir mesajlaşma programı aracılığıyla paylaşın]]> %s.]]> %s.]]> - \ No newline at end of file + 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 + Mesaj sunucuları yok. + Sunucu yapılandırmasında hatalar. + Yeni sunucu + yöneticiler + Bahsedildiğinizde haberdar olun. + Uygunsuz içerik + Uygunsuz profil + Liste adı... + Gizliliğe nasıl yardım ediyor + Liste adı ve emoji tüm listeler için farklı olmalıdır. + Herkesi sustur + Ağ merkezsizleştirme + Daha iyi meta veri gizliliği için Ağ ve sunucular ayarlarında Flux\'u etkinleştirin. + Mesaj silme özelliğini devre dışı bırak + Bu sohbetteki mesajlar asla silinmeyecektir. + Üyeyi kabul ederken hata oluştu + Liste + %d üyelerle sohbetler + 1 üye ile sohbet + %d sohbet(ler) + Tam bağlantı + Ağ operatörleri + Yeni üye gruba katılmak istiyor. + Yöneticilerle sohbetler + Üye ile sohbet + Gizli yönlendirme için + Üye kabulü + Kabul Et + Yöneticilerle sohbetler + Üyeyi kabul et + Mesaj yok + Gruplar + Sohbete davet et + Sohbetten çık + Üye sohbetten çıkarılacaktır - bu geri alınamaz! + Ağ operatörü + Mevcut koşullar metni yüklenemedi, koşulları bu bağlantı üzerinden inceleyebilirsiniz: + %s listesinde sohbet yok. + Hayır + %1$s kabul edildi + seni kabul etti + Koşullar şu tarihte kabul edilecektir: %s. + hepsi + Üyelerle sohbetler + Arkaplan servisi yok + Kabul Et + SimpleX Chat\'i kullanarak şunları kabul etmiş olursunuz:\n- genel gruplarda sadece yasal içerik göndermeyi.\n- diğer kullanıcılara saygı göstermeyi - spam yapmamayı. + Sunucu operatörlerini yapılandırma + Sohbetten çıkılsın mı? + yönetici + Bütün sunucular + Şunlar için sohbet profilini sil + Önemli mesajları kaçırmayın. + Bu üyelerden gelen mesajlar gösterilecektir! + Kabul edilen koşullar: %s. + %s , kullanım koşullarını kabul edin.]]> + Üyeler mesajları moderatörlere bildirebilir. + Üye ile birlikte sohbet silinsin mi? + Üyeli sohbetler yok + Üye olarak kabul et + Üye ve sohbet silinirken hata oluştu + Sohbetler yok + Sohbetler bulunamadı + %d mesajlar + Üye raporları + grup silindi + üyenin eski uygulama sürümü var + Şifreleme yeniden anlaşması devam ediyor. + Bağlantı şifreleme yeniden anlaşması gerektirir. + Geliştirilmiş sohbet navigasyonu + Üyelerden bahsedin 👋 + Gözlemci olarak kabul et + Üyeler sohbetten çıkarılacaktır - bu geri alınamaz! + kişi devre dışı bırakıldı + Sohbeti sil + Yöneticilerin gruplarını yönetmelerine yardım edin. + kişi hazır değil + kişi silindi + Üyeler gruptan çıkarılacaktır - bu geri alınamaz! + Üye gruba katılacak, üye kabul edilsin mi? + Özel mesaj yönlendirmesi için sunucu yok. + mesajlar gönderilemiyor + Medya ve dosya sunucuları yok. + Anahtar deposundaki parola okunamıyor. Bu, uygulamayla uyumsuz bir sistem güncellemesinden sonra gerçekleşmiş olabilir. Eğer durum bu değilse, lütfen geliştiricilerle iletişime geçin. + reddedildi + %s sunucuları + Güncellenen koşullar + Sadece siz ve moderatörler görebilir. + Sadece gönderen ve moderatörler görebilir. + Daha sonra oluşturmak için menüden SimpleX adresi oluştur seçeneğine dokunun. + Operatör + Dosyalar için kullanın + Sohbetlerde mesajların geçerlilik süresini ayarlayın. + İstenmeyen + Dosya göndermek için sunucu yok. + Dosyaları alacak sunucu yok. + Teslim edilmeyen mesajlar + Bağlantınızın değiştirilmesini önlemek için, iletişim güvenlik kodlarını karşılaştırabilirsiniz. + gruptan çıkarıldı + Notlar + senkronize değil + Özel medya dosya adları. + bağlanmak için talep edildi + Tek seferlik bağlantıyı bir arkadaşınızla paylaşın + Mesaj gönderemezsiniz! + uyarı + %s Kullanın + -İlk okunmamış mesaja sohbeti aç.\n- Alıntılanan mesajlara git. + Güncellenen koşulları görüntüleyin + Veya arşiv dosyasını içe aktarın + İstenmeyen + Uygulama, her konuşmada farklı operatörler kullanarak gizliliğinizi korur. + Rol %s olarak değiştirilecek. Sohbetteki herkes bilgilendirilecek. + Okunmamış bahsetmeler + Koşulları gözden geçirin + Sohbet profiliniz sohbet üyelerine gönderilecektir. + Sunucularınız + Açık koşullar + Bu işlem geri alınamaz - seçilen tarihten önce bu sohbette gönderilen ve alınan mesajlar silinecektir. + Bağlantının kimlerle paylaşıldığını hatırlamak için bağlantı adını ayarlayabilirsiniz. + SimpleX adresini sosyal medyada paylaşın. + Web sitesi + Desteklenmeyen bağlantı bağlantısı + Rapor + SimpleX adresi ve tek kullanımlık bağlantılar herhangi bir mesajlaşma uygulaması üzerinden güvenle paylaşılabilir. + Kısa bağlantı + Giriş ayarlarını kaydetmek ister misiniz? + Özel sohbetler, gruplar ve kişileriniz sunucu operatörleri tarafından erişilemez. + Birden fazla operatör etkinleştirildiğinde, hiçbirinin kimin kiminle iletişim kurduğunu öğrenmek için meta verisi yoktur. + Kullanılacak ağ operatörlerini seçin. + Güncelleme + Ağ ve sunucular ayarlarında operatörleri yapılandırabilirsiniz. + Bağlantıyı aç + Anahtar deposundaki parola okunamıyor, lütfen elle girin. Bu, uygulamayla uyumsuz bir sistem güncellemesinden sonra meydana gelmiş olabilir. Eğer durum bu değilse, lütfen geliştiricilerle iletişime geçin. + Bu üyeyi kabul ettiniz. + Lütfen grup moderatörlerinin gruba katılma isteğinizi incelemesini bekleyin. + Açık değişiklikler + Koşulları görüntüle + Üye kabulü + Uygulamadaki ikinci önceden ayarlanmış operatör! + Kısa bağlantı ekle + Gizlilik politikası ve kullanım koşulları. + Daha sonra incele + Sunucu operatörleri + SimpleX Chat ve Flux, Flux tarafından işletilen sunucuların uygulamaya dahil edilmesi konusunda bir anlaşma yaptı. + Sohbet listesinden bağlantıları aç + Evet + Web bağlantısını açmak ister misiniz? + beklemede + onay bekliyor + inceleme + Üyeleri kaldırmak mı? + Tüm üyeler için engellemeyi kaldırmak mı? + Sunucu protokolü değiştirildi. + Sunucuları kullanın + Operatör sunucusu + Sunucu operatöre %s eklendi. + Sunucu operatörü değişti. + Moderatörlere mesaj gönderilmesini yasaklayın. + Üyeyi reddetmek mi? + Özel raporlar gönder + %s ile aç + Şeffaflık + Ulaşılabilir sohbet araç çubuğu + Bu sohbetten mesaj almayı durduracaksınız. Sohbet geçmişi korunacaktır. + Alıcı + Müşterilerinizin gizliliği. + reddedildi + Sohbet adını ayarla… + Her mesajda en fazla %1$s üyeye atıfta bulunabilirsiniz! + İletileri alacak sunucu yok. + Bu mesaj silindi veya henüz alınmadı. + Lütfen mesaj boyutunu küçültün ve tekrar gönderin. + Lütfen mesaj boyutunu küçültün veya medyayı kaldırın ve tekrar gönderin. + Üye profilini bildir: yalnızca grup moderatörleri görebilir. + Spam bildir: Yalnızca grup moderatörleri bunu görebilir. + İhlali bildir: Yalnızca grup moderatörleri bunu görebilir. + Mesajı kopyalayıp boyutunu küçülterek gönderebilirsiniz. + Rapor içeriği: Yalnızca grup moderatörleri görebilir. + Diğerlerini bildir: Yalnızca grup moderatörleri bunu görebilir. + Moderatörlere gönderilen rapor + Raporlarınızı yöneticilerle sohbet bölümünde görüntüleyebilirsiniz. + Ayarlar aracılığıyla sunucuları yapılandırabilirsiniz. + Uzaktan kumandalı cep telefonları + kapalı + İnceleme üyeleri + Kabul etmeden önce üyeleri inceleyin (kapıyı çalın). + Bildirimler ve pil + inceleme bekliyor + Kapalı + Önceden ayarlanmış sunucular + Önceden ayarlanmış sunucular için yalnızca TCP bağlantı noktası 443\'ü kullanın. + Sohbet sahipleri tercihleri değiştirebilir. + Sohbetleri listeler halinde düzenleyin + Önceden ayarlanmış sunucular + Mesajlar için kullanın + Mevcut sohbet profilinizin yeni dosyaları için sunucular + Bu grupta mesajların bildirilmesi yasaktır. + Reddet + Bağlantı, teslim edilemeyen mesajların sınırına ulaştı, bağlantınız çevrimdışı olabilir. + Bu bağlantı için daha yeni bir uygulama sürümü gereklidir. Lütfen uygulamayı güncelleyin veya irtibat kişinizden uyumlu bir bağlantı göndermesini isteyin. + Rapor nedeni? + Rapor sizin için arşivlenecektir. + Okunmamış sohbet yok + Rapor: %s + Raporlar + katılma talebi reddedildi + yöneticiler tarafından incelenmiştir + sen ayrıldın + Adresi herkese açık olarak paylaş + Port belirtilmediğinde TCP portu %1$s kullanın. + Veya özel olarak paylaşmak için + SimpleX adresi mi yoksa tek kullanımlık bağlantı mı? + Listeyi kaydet + Göndermek için + SimpleX kanal bağlantısı + Mesajlaşma için TCP bağlantı noktası + Web bağlantı noktasını kullan + İletişim isteğini kabul et + Mesaj ekle + Profil değiştirilemiyor + uçtan uca şifreleme ile korunmaktadır.]]> + sonra mesaj gönderebileceksiniz.]]> + Yöneticilerle sohbet edin + Üyeler katılmadan önce onlarla sohbet edin. + Bağlan + Daha hızlı bağlanın! 🚀 + iletişim kabul etmelidir… + Kullanıcısını değiştirirken hata oluştu + Sohbeti açarken sorun oluştu + Grup açarken hata oluştu + İletişim isteği reddedildi + Gruba katıl + Mobil ağlarda daha az trafik. + Bağlan\'a dokunduğunuz anda anında mesaj gönderin. + Yeni grup rolü: Moderatör + Özel yönlendirme oturumu yok + Sohbeti aç + Yeni sohbet aç + Yeni grup aç + Kabul etmeye açık + Bağlanmak için açık + Katılmak için açık + Özel yönlendirme zaman aşımı + Profil, adres aracılığıyla paylaşılacaktır. + Protokol arka plan zaman aşımı + İletişim isteğini reddet + Mesajları siler ve üyeleri engeller. + istek gönderilir + İnceleme grubu üyeleri + İletişim isteği gönder? + Talep gönder + Mesaj olmadan istek gönder + Özel geri bildirimlerinizi gruplara gönderin. + Bağlantı kurulduktan sonra kişinize gönderilir. + Grup profilini bağlantı yoluyla paylaş + Profil paylaş + Profilinizi adres yoluyla paylaşın + TCP bağlantısı arka plan zaman aşımı + Gönderen bilgilendirilmeyecektir. + Bağlantı denemesinden sonra başka bir profil kullanmak için sohbeti silin ve bağlantıyı tekrar kullanın. + Hoş geldiniz mesajı + Profiliniz + Profil yükleniyor… + Bio: + Biyografiniz: + Kısa açıklama: + Sohbet etmek için Bağlan\'a dokunun + Bağlan\'a dokunarak isteği gönderin + İletişim isteğini kabul et + İrtibat kişiniz + Gruba katıl\'a dokunun + Grubunuz + Grup + İş bağlantısı + İş bağlantınız + Biyografi çok uzun + Açıklama çok büyük + Kaybolma süresi yalnızca yeni kişiler için ayarlanır. + Gizli profil kullan + Botu kullanmak için açın + Botu kullanmak için Bağlan tuşuna basın. + Bot + Komutlar gönderebilmek için bağlanmanış olmanız gereklidir. + Üye silinmiş - isteği kabul edemeyecek + Grup linkini güncelle + 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 eff112717e..6d498ef4ed 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 для безпечного збереження ключової фрази - це дозволить отримувати сповіщення. Дозвольте вашим контактам надсилати голосові повідомлення. Прийняти інкогніто Додати сервер адміністратор Додати привітання - Всі члени групи залишаться підключеними. + Усі учасники групи залишаться підключеними. Дозвольте вашим контактам надсилати повідомлення, які зникають. - Всі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас. + Усі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас. Версія додатку Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Оновлення профілю буде відправлено вашим контактам. - Всі ваші контакти залишаться підключеними. Оновлення профілю буде відправлено вашим контактам. + Усі ваші контакти залишаться підключеними. Оновлення профілю буде відправлено вашим контактам. Відповісти на виклик Адреса Додати профіль @@ -124,7 +124,7 @@ помилка підключення Ви підключені до сервера для отримання повідомлень від цього контакту. - Спроба підключитися до сервера для отримання повідомлень від цього контакту (помилка: %1$s). + Спроба підключитися до сервера для отримання повідомлень від цього контакту (помилка: %1$s). видалено Спроба підключитися до сервера для отримання повідомлень від цього контакту. відзначено як видалено @@ -200,7 +200,7 @@ редаговано помилка відправки непрочитане - приєднатися як %s + Приєднуйтесь як %s Скасувати попередній перегляд зображення Скасувати попередній перегляд файлу Очікування на зображення @@ -224,12 +224,11 @@ Скасувати приглушення Ви запросили контакт Контакт, якому ви поділилися посиланням, НЕ зможе підключитися! - заповнювач зображення профілю + аватар не встановлено QR-код довідка покажіть QR-код у відеовиклику, або поділіться посиланням.]]> - Ваш профіль чату буде відправлено -\nвашому контакту + Ваш профіль буде відправлено \nвашому контакту Одноразове запрошення Невірний код безпеки! Для перевірки end-to-end шифрування порівняйте (або скануйте) код на своїх пристроях. @@ -271,7 +270,7 @@ Як це впливає на батарею Миттєво Виклик вже завершено! - Ваші виклики + Виклики Ваші сервери ICE Відкрити через реле @@ -330,7 +329,7 @@ Торкніться, щоб розпочати новий чат Чат із розробниками У вас немає чатів - Ви не можете відправляти повідомлення! + ви спостерігач Будь ласка, зв\'яжіться з адміністратором групи. Файл буде отримано, коли ваш контакт буде в мережі, будь ласка, зачекайте або перевірте пізніше! Відео відправлене @@ -376,7 +375,7 @@ так Налаштування контакту Безпека SimpleX Chat була перевірена компанією Trail of Bits. - Ваші контакти можуть дозволити повне видалення повідомлень. + Ваші контакти можуть дозволити остаточне видалення повідомлень. База даних буде зашифрована. Помилка сховища ключів Невідома помилка @@ -390,7 +389,7 @@ Стан мережі Оновити налаштування мережі\? Локальні дані профілю тільки - Ваш випадковий профіль + Випадковий профіль ввімкнено ввімкнено для вас Забороняйте надсилання прямих повідомлень учасникам. @@ -417,7 +416,7 @@ Пропущений виклик Підключення виклику Конфіденційність і безпека - Ваша конфіденційність + Конфіденційність НАЛАШТУВАННЯ ДОПОМОГА ПІДТРИМАЙТЕ SIMPLEX CHAT @@ -443,7 +442,7 @@ Створити посилання на групу Змінити роль у групі\? Помилка при вилученні учасника - Ваш профіль чату буде відправлений учасникам групи + Ваш профіль буде відправлений учасникам групи Видалення для всіх Голосові повідомлення Голосові повідомлення заборонені в цьому чаті. @@ -451,20 +450,20 @@ Встановіть його замість системної аутентифікації. Вимкнути\? Поділитися з контактами - Ваш профіль зберігається на вашому пристрої і обмінюється лише з ваших контактів. Сервери SimpleX не можуть його бачити. - Зберегти і повідомити контакти - Зберегти і повідомити учасників групи + Ваш профіль зберігається на вашому пристрої та ділиться лише з вашими контактами. Серверам SimpleX профіль недоступний. + Зберегти та сповістити контакти + Зберегти та сповістити учасників Вийти без збереження Сховати профіль Пароль для відображення Створити без зашифрування e2e контакт має зашифрування e2e - Хеш попереднього повідомлення відрізняється. + Хеш попереднього повідомлення інший. Підтвердити пароль Новий пароль Перезапустити - Ваша база даних чату + База даних чату Чат зупинено БАЗА ДАНИХ ЧАТУ Новий архів бази даних @@ -497,7 +496,7 @@ Забороняйте надсилання повідомлень, які зникають. Забороняйте невідворотне видалення повідомлень. Учасники можуть надсилати голосові повідомлення. - %dm + %dм Нове в %s Самознищуючий пароль Італійський інтерфейс @@ -516,7 +515,7 @@ Зберігайте останню чернетку повідомлення із вкладеннями. Зникне повідомлення Надіслати зникне повідомлення - зображення профілю + аватар Більше Створити профіль GitHub.]]> @@ -529,7 +528,7 @@ Встановити на 1 день Забороняйте реакції на повідомлення. Реакції на повідомлення заборонені в цьому чаті. - %ds + %dс хвилини Китайський та іспанський інтерфейс підключення %1$d @@ -554,7 +553,7 @@ Блокування SimpleX Щоб захистити вашу інформацію, увімкніть блокування SimpleX. \nВам буде запропоновано завершити аутентифікацію перед увімкненням цієї функції. - Увійти за допомогою своїх облікових даних + Пройдіть аутентифікацію Увімкнути блокування SimpleX Блокування SimpleX не увімкнено! Поділитися @@ -569,10 +568,10 @@ Файл буде видалено з серверів. Відкликати несанкціонована відправка - Ласкаво просимо, %1$s! - Ласкаво просимо! + Вітаємо, %1$s! + Вітаємо! Цей текст доступний у налаштуваннях - вас запрошено в групу + Запрошуємо вас до групи Поділитися повідомленням… Поділитися медіа… Поділитися файлом… @@ -580,14 +579,14 @@ Одночасно можна відправити лише 10 відео Помилка декодування зображення Неможливо декодувати зображення. Спробуйте інше зображення або зв\'яжіться з розробниками. - Зображення + Фото Зображення буде отримано, коли ваш контакт завершить його вивантаження. Зображення буде отримано, коли ваш контакт буде в мережі, будь ласка, зачекайте або перевірте пізніше! Зображення збережено в галереї Відео Ваш контакт відправив файл, розмір якого більший, ніж поточно підтримуваний максимальний розмір (%1$s). Поточно максимально підтримуваний розмір файлу - %1$s. - Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з\'явиться в мережі. + Адреса отримання буде змінена на інший сервер. Зміна адреси буде завершена після того, як відправник з\'явиться в мережі. Перевірити код безпеки Надіслати повідомлення Записати голосове повідомлення @@ -609,8 +608,8 @@ Логотип SimpleX Електронна пошта Цей QR-код не є посиланням! - Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, зачекайте або перевірте пізніше! - Вас підключать, коли ваш запит на з\'єднання буде прийнятий, зачекайте або перевірте пізніше! + Ви будете підключені до групи, коли пристрій власник групи буде в мережі, зачекайте або перевірте пізніше! + Підключення відбудеться, коли ваш запит на підключення буде прийнято. Будь ласка, зачекайте або спробуйте пізніше! Поділитися 1-разовим посиланням Дізнатися більше Щоб підключитися, ваш контакт може сканувати QR-код або використовувати посилання у додатку. @@ -639,8 +638,8 @@ Ви можете створити його пізніше Ваш поточний профіль Видалити зображення - Зберегти уподобання? - Зберегти і повідомити контакт + Зберегти налаштування? + Зберегти та сповістити контакт Зберегти пароль профілю Пароль схованого профілю Профіль обмінюється лише з вашими контактами. @@ -718,10 +717,10 @@ Зникає о: %s (поточне) Вилучити учасника - Роль буде змінено на "%s". Всі учасники групи будуть сповіщені. + Роль буде змінено на %s. Усі учасники групи будуть сповіщені. Роль буде змінено на "%s". Учасник отримає нове запрошення. Група - Ласкаво просимо + Привітальне повідомлення Профіль групи зберігається на пристроях учасників, а не на серверах. Зберегти профіль групи Профіль і підключення до серверів @@ -733,7 +732,7 @@ Світлий Помилка імпорту теми Налаштування групи - Повідомлення зникнення + Зникаючі повідомлення ввімкнено для контакту вимкнено отримано, заборонено @@ -757,7 +756,7 @@ %d година %d тиждень %d тижні - %dw + %dтиж запропоновано %s: %2s З опційним вітанням. Приховуйте екран додатка в останніх програмах. @@ -785,7 +784,7 @@ Ви вже підключені до %1$s через це посилання. Режим інкогніто СЕРВЕРИ - Зберегти ласкаво просимо? + Зберегти вітальне повідомлення? Отримання через Приглушено, коли неактивно! Видалити профіль @@ -827,11 +826,11 @@ Інший час Створити одноразове запрошення Сканувати QR-код - Зображення + Фото Відео Прийняте вами з\'єднання буде скасоване! Контакт ще не підключений! - Тестування серверів + Тестувати сервери Зберегти сервери Ваш сервер Тест сервера не вдався! @@ -877,7 +876,7 @@ Надіслано о: %s Видалено о: %s %s (поточне) - %dh + %dч %d день %d днів скасовано %s @@ -938,7 +937,7 @@ Тільки власники груп можуть увімкнути голосові повідомлення. Відхилити Очистити чат\? - зображення попереднього перегляду посилання + зображення прев’ю посилання скасувати попередній перегляд посилання Налаштування Виклик у процесі @@ -986,7 +985,7 @@ Змінити роль Ви все ще отримуватимете дзвінки та сповіщення від приглушених профілів, коли вони активні. %d місяці - %dmth + %dміс Надіслані повідомлення будуть видалені після встановленого часу. Відкриття бази даних… Помилка встановлення адреси @@ -1004,12 +1003,12 @@ Підключитися за посиланням / QR-кодом Очистити Неправильний QR-код - Вас підключать, коли пристрій вашого контакту буде в мережі, зачекайте або перевірте пізніше! + Підключення відбудеться, коли пристрій вашого контакту буде онлайн. Будь ласка, зачекайте або спробуйте пізніше! Ви не втратите свої контакти, якщо ви пізніше видалите свою адресу. Коли люди просять про з\'єднання, ви можете його прийняти чи відхилити. Посібнику користувача.]]> SimpleX-адреса - Очистити перевірку + Скинути підтвердження %s перевірено %s не перевірено Написати нам ел. листа @@ -1025,14 +1024,14 @@ Обов\'язково КОЛЬОРИ ІНТЕРФЕЙСУ Створіть адресу, щоб дозволити людям підключатися до вас. - Ваші контакти залишаться підключеними. + Контакти залишатимуться підключеними. Створити SimpleX-адресу Оновлення профілю буде відправлено вашим контактам. Зупинити поділ адреси? Зупинити поділ Введіть текст привітання... (необов\'язково) Зберегти налаштування\? - Зберегти налаштування автоприйому + Зберегти налаштування адреси SimpleX Привіт! \nПриєднуйтесь до мене через SimpleX Chat: %s Запросити друзів @@ -1069,12 +1068,12 @@ Привітання Вибрати контакти Поділитися адресою - Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите членів групи, якщо потім видалите її. + Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо потім видалите її. Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли підключитися до %s. Локальна назва Ідентифікатор бази даних Попередній перегляд - Введіть ласкаво просимо… + Введіть привітальне повідомлення… Змінити адресу отримання Створити секретну групу Повністю децентралізовано - видимо тільки для учасників. @@ -1088,7 +1087,7 @@ Ви дозволяєте ні вимк - Встановити налаштування групи + Налаштування групи Налаштування Прямі повідомлення Помилка @@ -1109,7 +1108,7 @@ Видалити контакт Встановити ім\'я контакту… Файл - З галереї + Галерея Команда SimpleX хоче підключитися до вас! ЕКСПЕРИМЕНТАЛЬНІ ФУНКЦІЇ @@ -1134,7 +1133,7 @@ Підключитися Щоб показати ваш схований профіль, введіть повний пароль у поле пошуку на сторінці Ваші профілі. Підтвердити пароль - %dd + %dд Захистіть свої чат-профілі паролем! Помилка дешифрування Сервер вимагає авторизації для завантаження, перевірте пароль @@ -1146,7 +1145,7 @@ Дзвінки чату SimpleX Служба сповіщень Показати попередній перегляд - Попередній перегляд сповіщень + Перегляд сповіщень Запускається, коли додаток відкритий Запускається періодично Текст повідомлення @@ -1200,16 +1199,14 @@ Експортувати тему Переконайтеся, що файл має правильний синтаксис YAML. Експортуйте тему, щоб мати приклад структури файлу теми. Скинути кольори - за замовчуванням (%s) + типово (%s) Тільки ви можете здійснювати дзвінки. Тільки ваш контакт може здійснювати дзвінки. Що нового Голосові повідомлення За профілем чату (типово) або за підключенням (BETA). Власні теми - - голосові повідомлення до 5 хвилин. -\n- власний час на зникнення. -\n- історія редагування. + - голосові повідомлення до 5 хвилин.\n- налаштування часу для зникнення повідомлень.\n- історія змін. Японський та португальський інтерфейс Натисніть, щоб приєднатися Натисніть, щоб приєднатися анонімно @@ -1229,7 +1226,7 @@ Змінити ключову фразу бази даних? Не вдається отримати доступ до сховища ключів для збереження пароля бази даних Зберегти ключову фразу і відкрити чат - Ключова фраза не знайдена в сховищі ключів, будь ласка, введіть її вручну. Це може трапитися, якщо ви відновили дані додатка за допомогою інструменту резервного копіювання. Якщо це не так, зверніться до розробників. + Пароль не знайдено в сховищі ключів, введіть його вручну. Це могло статися, якщо ви відновили дані додатка за допомогою інструмента резервного копіювання. Якщо це не так, зверніться до розробників. Видалити профіль чату %d сек Пошук @@ -1429,9 +1426,9 @@ \n- швидше та надійніше. Ключова фраза зберігається в налаштуваннях як звичайний текст. Ви вже подали запит на підключення за цією адресою! - надіслати приватне повідомлення + відправити для підключення Показувати консоль в новому вікні - Всі нові повідомлення від %s будуть приховані! + Усі нові повідомлення від %s будуть приховані! підключив(лась) безпосередньо заблоковано Блокувати учасників групи @@ -1439,32 +1436,32 @@ Підключений робочий стіл Новий мобільний пристрій Підключати автоматично - Адреса робочого столу + Адреса комп\'ютера Одночасно може працювати лише один пристрій Підключіть мобільний і десктопний додатки! 🔗 Через безпечний квантовостійкий протокол. - Використовувати з робочого столу у мобільному додатку і скануйте QR-код.]]> + Використовувати з комп\'ютера в мобільному додатку та відскануйте QR-код.]]> Щоб приховати небажані повідомлення. Несумісна версія (новий)]]> - Відсунути відсилання до робочого столу? + Відключити комп\'ютер? Кращі групи - Параметри пов\'язаних робочих столів - Пов\'язані робочі столи + Параметри пов\'язаних комп\'ютерів + Підключені комп\'ютери Виявити через локальну мережу Інкогніто групи Цей пристрій %s був відключений]]> - Очікування робочого столу… + Очікування комп\'ютера… Швидше приєднуйтесь та надійшовні повідомлення. - Пов\'язані мобільні + Під’єднані мобільні Робочий стіл - Підключено до робочого столу + Підключено до комп\'ютера Назва цього пристрою Завантаження файлу - Підключення до робочого столу + Підключення до комп\'ютера Знайдено робочий стіл - Пристрої робочого столу + Комп\'ютери Не сумісно! Зв\'язати з мобільним Використовувати з комп\'ютера @@ -1477,13 +1474,13 @@ Перевірте код на мобільному Введіть назву цього пристрою… Помилка - Підключитися до робочого столу + Підключитися до комп\'ютера Відключити автор Підключено до мобільного - Некоректна адреса робочого столу - Вставити адресу робочого столу - Перевірити код з робочим столом + Некоректна адреса комп\'ютера + Вставити адресу комп\'ютера + Перевірити код з комп\'ютером Сканувати QR-код з комп\'ютера Пристрої Виявлено через локальну мережу @@ -1494,7 +1491,7 @@ Перевірити підключення Відключити робочий стіл? Будь ласка, зачекайте, поки файл завантажується з підключеного мобільного - Версія робочого столу %s не сумісна з цим додатком. + Версія комп\'ютерного додатка %s несумісна з цим додатком. Перевірити підключення Чат зупинено. Якщо ви вже використовували цю базу даних на іншому пристрої, перенесіть її назад перед запуском чату. З зашифрованими файлами та медіа. @@ -1556,7 +1553,7 @@ \n \nРекомендується перезапустити додаток. Надсилати до 100 останніх повідомлень новим користувачам. - До 100 останніх повідомлень надсилаються новим членам. + До 100 останніх повідомлень надсилаються новим учасникам. З\'єднання перервано Створено: %s Показати внутрішні помилки @@ -1611,7 +1608,7 @@ учасник %1$s змінений на %2$s вилучено зображення профілю Установлено нову адресу контакту - Установлено нове зображення профілю + встановити новий аватар оновлений профіль вилучено адресу контакту Колишній учасник %1$s @@ -1620,7 +1617,7 @@ Аудіодзвінок Помилка відкриття браузера Для використання дзвінків потрібен браузер за замовчуванням. Будь ласка, налаштуйте браузер за замовчуванням в системі та надайте більше інформації розробникам. - Перехід з іншого пристрою + Перенести з іншого пристрою квантово-стійке шифрування e2e стандартне наскрізне шифрування Цей чат захищений наскрізним шифруванням. @@ -1648,8 +1645,8 @@ Повторний імпорт Завершіть міграцію на іншому пристрої. Застосувати - Перенести пристрій - Перехід на інший пристрій + Міграція пристрою + Перенести на інший пристрій Помилка експорту бази даних чату Налаштування збереження помилок Помилка завантаження архіву @@ -1660,7 +1657,7 @@ Помилка видалення бази даних Для того, щоб продовжити, чат слід зупинити. Зупинка чату - %s завантажено + %s вивантажено Завантаження архіву Створення архівного посилання Видалити базу даних з цього пристрою @@ -1677,7 +1674,7 @@ Перевірте підключення до Інтернету та спробуйте ще раз Переконайтеся, що ви пам\'ятаєте пароль до бази даних для її перенесення. Помилка при перевірці парольної фрази: - Всі ваші контакти, розмови та файли будуть надійно зашифровані та завантажені частинами на налаштовані XFTP-реле. + Усі ваші контакти, розмови та файли будуть надійно зашифровані та завантажені частинами на налаштовані XFTP-реле. Please note: використання однієї і тієї ж бази даних на двох пристроях порушить розшифровку повідомлень з ваших з\'єднань, як захист безпеки.]]> Скасувати міграцію Чат перемістився! @@ -1751,10 +1748,10 @@ Надайте дозвіл(и) на здійснення дзвінків Відкрити налаштування ФАЙЛИ - Зображення профілю + Зображення профілів Підключення до мережі адміністратори - всі учасники + всіх учасників Литовський інтерфейс Надавати дозволи Кольори чату @@ -1829,7 +1826,7 @@ Так Отримання паралелізму Посилання SimpleX заборонені. - Сформуйте зображення профілю + Форма зображень профілю При підключенні аудіо та відеодзвінків. Скинути колір Система @@ -1846,7 +1843,7 @@ Налаштовані XFTP сервери Бета Статус файлу - Повідомлення надіслано + Надіслано повідомлень Статистика Попередньо підключені сервери Помилковий ключ або невідома адреса чанка файлу - найбільш імовірно, що файл було видалено. @@ -1948,13 +1945,12 @@ Поточний профіль Деталі Помилки - Отримання повідомлення - Повідомлення отримано + Отримання повідомлень + Отримані повідомлення В очікуванні Проксіровані сервери Показувати інформацію для - Починаючи з %s. -\nВсі дані зберігаються лише на вашому пристрою. + Починаючи з %s.\nУсі дані зберігаються приватно на вашому пристрої. Перепідключити сервер для примусової доставки повідомлень. Це використовує додатковий трафік. Скинути всю статистику Скинути всю статистику? @@ -1981,8 +1977,8 @@ Завантажені файли Помилки завантаження Частини видалені - Частини завантажено - Частини завантажено + Частин завантажено + Частин вивантажено Ця посилання було використано на іншому мобільному пристрої, створіть нове посилання на комп\'ютері. Помилка копіювання Будь ласка, перевірте, що мобільний пристрій і комп\'ютер підключені до однієї локальної мережі, і що брандмауер комп\'ютера дозволяє з\'єднання. @@ -2029,9 +2025,9 @@ Проксірований Надіслати помилки Завершено - Всі профілі + Усі профілі Скинути - Завантажено + Вивантажено Видалити %d повідомлень учасників? Повідомлення будуть позначені для видалення. Одержувач(і) зможуть розкрити ці повідомлення. Вибрати @@ -2062,7 +2058,7 @@ Видалити архів? Поділитися профілем Завантажений архів бази даних буде остаточно видалено з серверів. - Ваше з\'єднання було перенесено на %s, але виникла несподівана помилка під час перенаправлення на профіль. + Ваше з\'єднання було переміщено на %s, але при перемиканні профілю сталася помилка. Режим системи Не використовуйте облікові дані з проксі. Аутентифікація проксі @@ -2092,7 +2088,7 @@ Повідомлення були видалені після того, як ви їх вибрали. Помилка при пересиланні повідомлень Звук вимкнено - Помилка ініціалізації WebView. Переконайтеся, що WebView встановлено, і його підтримувана архітектура — arm64. \nПомилка: %s + Помилка під час ініціалізації WebView. Переконайтеся, що WebView встановлено і що його архітектура підтримується arm64.\nПомилка: %s Хвіст Кут Форма повідомлення @@ -2140,24 +2136,24 @@ Прийняті умови Умови будуть автоматично прийняті для увімкнених операторів: %s. Оператор мережі - %s серверів + %s сервери Вебсайт Ваші сервери - Використовуйте %s - Використовуйте сервери + Використовувати %s + Використовувати сервери %s.]]> %s.]]> Прийняти умови - Умови перегляду + Переглянути умови Додано сервери повідомлень Для приватної маршрутизації - Щоб отримати - Використовуйте для файлів + Для отримання повідомлень + Використовувати для файлів Додано медіа та файлові сервери - Відкриті зміни + Відкрити зміни Відкриті умови Сервери для нових файлів вашого поточного профілю чату - Щоб відправити + Для відправки Помилка додавання сервера Сервер оператора Сервер додано до оператора %s. @@ -2183,7 +2179,7 @@ Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням: Помилки в конфігурації серверів. Умови приймаються з: %s. - Увімкнути flux + Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих Умови приймаються до: %s. Продовжити Створити одноразове посилання @@ -2195,7 +2191,7 @@ SimpleX адреса або одноразове посилання? Новий сервер Немає серверів для отримання файлів. - Умови перегляду + Переглянути умови Немає серверів для надсилання файлів. Попередньо встановлені сервери Протокол сервера змінено. @@ -2204,14 +2200,14 @@ З\'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн. Натисніть Створити адресу SimpleX у меню, щоб створити її пізніше. Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові. - Використовуйте для повідомлень + Використовувати для повідомлень Ви можете налаштувати операторів у налаштуваннях Мережі та серверів. Або імпортуйте архівний файл Віддалені мобільні Пристрої Xiaomi: будь ласка, увімкніть Автозапуск у налаштуваннях системи, щоб сповіщення працювали.]]> Повідомлення занадто велике! Будь ласка, зменшіть розмір повідомлення або видаліть медіа та надішліть знову. - Додайте учасників команди до розмов. + Додайте учасників команди до розмови. Бізнес адреса Перевіряти повідомлення кожні 10 хвилин. Без фонової служби @@ -2242,9 +2238,297 @@ Будь ласка, зменшіть розмір повідомлення та надішліть знову. Скопіюйте та зменшіть розмір повідомлення для відправки. Ви припините отримувати повідомлення з цього чату. Історія чату буде збережена. - Ваш профіль чату буде надіслано учасникам чату. + Ваш профіль буде надіслано учасникам чату. Коли увімкнено більше ніж одного оператора, жоден з них не має метаданих, щоб дізнатися, хто спілкується з ким. прийнято запрошення запит на підключення Про операторів - \ No newline at end of file + 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- поважати інших користувачів – без спаму. + Налаштувати операторів сервера + Політика конфіденційності та умови використання + Це посилання вимагає новішої версії додатку. Будь ласка, оновіть додаток або попросіть вашого контакту надіслати сумісне посилання. + Повне посилання + Коротке посилання + Посилання на канал SimpleX + Несумісне посилання для підключення + Усі сервери + Ні + Типові сервери + Використовуйте TCP порт 443 лише для попередньо налаштованих серверів. + контакт не готовий + Помилка при прийомі учасника + Чат з учасником + Прийом учасника + усі + Чати з учасниками + Чат з адміністраторами + контакт вимкнено + Чат з адміністраторами + Прийняти + 1 чат з учасником + %d чат(ів) + %d чатів з учасниками + %d повідомлень + не можна надсилати + контакт видалено + групу видалено + прийнято %1$s + прийняв(ла) вас + Прийняти як учасника + Прийняти як спостерігача + Прийняти учасника + Видалити чат + Видалити чат з учасником? + Помилка видалення чату з учасником + Встановити прийом учасників + Зберегти налаштування прийому? + Будь ласка, зачекайте, поки модератори групи розглянуть ваш запит на приєднання до групи. + ви прийняли цього учасника + Схвалювати учасників + вимкнено + Схвалювати учасників для вступу до групи. + Адреса оновлення + не синхронізовано + схвалено адміністраторами + очікує на схвалення + перегляд + учасник використовує застарілу версію + видалено з групи + Повідомлення надіслано модераторам + запит на приєднання відхилено + Ви не можете надсилати повідомлення! + Ви можете переглянути свої звіти у чаті з адміністраторами. + Новий учасник хоче приєднатися до групи. + Немає чатів з учасниками + Учасник приєднається до групи, прийняти учасника? + Відхилити + Відхилити учасника? + ви вийшли + 4 нові мови інтерфейсу + Прийняти запит на контакт + Прийняти запит на контакт + Додати повідомлення + Біо: + Біографія занадто велика + Бізнес-зв\'язок + Не вдається змінити профіль + Каталонська, індонезійська, румунська та в\'єтнамська - завдяки нашим користувачам! + наскрізним шифруванням.]]> + лише після того, як ваш запит буде прийнятий.]]> + Чат з адміністраторами + Спілкуйтеся з учасниками до того, як вони приєднаються. + Підключіться + Підключайтеся швидше! 🚀 + контакт повинен прийняти… + Створіть свою адресу + Опис занадто великий + Увімкнути зникаючі повідомлення за замовчуванням. + Помилка зміни профілю + Помилка відкриття чату + Помилка відкриття групи + Помилка відхилення запиту на контакт + Група + Приєднуйтесь до групи + Підтримуйте чистоту в чатах + Менше трафіку в мобільних мережах. + Завантаження профілю… + Миттєве повідомлення, щойно ви натиснете \"Підключитися\". + Нова роль у групі: Модератор + Немає приватного сеансу маршрутизації + Відкритий чат + Відкрити новий чат + Відкрити нову групу + Відкрити для прийняття + Відкрито для підключення + Відкрито для приєднання + Тайм-аут приватної маршрутизації + Фоновий тайм-аут протоколу + Відхилити запит на контакт + Видаляє повідомлення та блокує користувачів. + запит відправлено + Учасники групи оглядів + Надіслати запит на контакт? + Надіслати запит + Надіслати запит без повідомлення + Надсилайте свої приватні відгуки до груп. + Відправлено вашому контакту після з\'єднання. + Налаштуйте біографію профілю та вітальне повідомлення. + Поділіться старою адресою + Поділіться старим посиланням + Поділіться своєю адресою + Короткий опис: + Коротка адреса SimpleX + Натисніть Підключитися до чату + Натисніть Підключитися, щоб відправити запит + Натисніть Приєднатися до групи + Таймаут TCP-з\'єднання bg + Адреса буде короткою, і ваш профіль буде доступний за цією адресою. + Посилання буде коротким, а профіль групи буде поширюватися за посиланням. + Відправник НЕ буде повідомлений. + Час зникнення встановлюється тільки для нових контактів. + Щоб використовувати інший профіль після спроби з\'єднання, видаліть чат і скористайтеся посиланням знову. + Оновіть свою адресу + Оновлення + Змінити адресу? + Оновити посилання на групу + Оновити посилання на групу? + Використовуйте профіль інкогніто + Вітальне повідомлення + Вітаємо ваші контакти 👋 + Твоя біографія: + Ваш діловий контакт + Ваш контакт + Ваша група + Ваш профіль + Дозволяйте файли та медіа лише за умови, що ваш контакт їх дозволяє. + Дозвольте своїм контактам надсилати файли та медіа. + Бот + Ви, і ваш контакт можете надсилати файли та медіа. + ЗАПИТИ НА ЗВ’ЯЗОК ВІД ГРУП + Застарілі опції + Помилка при відмітці як прочитане + Файли та медіа заборонені у цьому чаті. + Відбиток у адресі сервера переадресації не співпадає з сертифікатом: %1$s. + Відбиток у адресі сервера не співпадає з сертифікатом: %1$s. + Користувача видалено - не може прийняти запит. + Тільки ви можете надсилати файли та медіа. + Лише ваш контакт може надсилати файли та медіа. + Відкрити чисте посилання + Відкрити повне посилання + Відкрити для використання бота + Заборонити надсилання файлів і медіа. + Видалити відстеження посилань + запит на підключення до групи %1$s + 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 c9b30c652a..ae22c40277 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -7,7 +7,7 @@ Đị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ủ @@ -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. @@ -122,7 +122,7 @@ 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ỏ @@ -136,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" @@ -169,7 +169,7 @@ 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.]]> + 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… @@ -200,7 +200,7 @@ 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 liên kết + 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 @@ -208,34 +208,34 @@ đã hủy bỏ %s Không thể mời liên hệ! Hủy bỏ tin nhắn động - đã thay đổi quyền hạn của %s thành %s + đã 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 quyền hạn của bạn thành %s + đã 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 quyền hạn của nhóm? + 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 quyền hạn + Thay đổi chức vụ đang thay đổi địa chỉ… đang thay đổi địa chỉ… Bảng điều khiển trò chuyện - Ứng dụng SimpleX Chat đang hoạt động - Cơ sở dữ liệu SimpleX Chat đã bị xóa - Ứng dụng SimpleX Chat đã đượ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 bắt đầu ứng dụng. - Cơ sở dữ liệu SimpleX Chat đã được nhập - Ứng dụng SimpleX Chat đã được dừng lại + 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 SIMPLEX CHAT - Ứng dụng SimpleX Chat đã được dừng lại + 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 @@ -248,17 +248,17 @@ 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 xóa - Xóa + Xác minh dọn dẹp + Dọn dẹp Nút đóng - Xóa ghi chú riêng tư? + Dọn dẹp ghi chú riêng tư? có màu - Xóa cuộc trò chuyện - Xóa + 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.]]> - Xóa - Xóa cuộc trò chuyện? + 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 @@ -302,10 +302,10 @@ đ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 thông qua liên kết / mã QR + 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 thông qua liên kết + 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 @@ -315,21 +315,21 @@ 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 - Hết thời gian chờ kết nối + Thời gian chờ kết nối Lỗi kết nối Kết nối - Kết nối thông qua liên kết? + 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 thông qua địa chỉ liên lạc? - Kết nối thông qua liên kết dùng một lần? + 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ệ có cho phép + 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! @@ -354,8 +354,8 @@ 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 liên kết nhóm - Tạo liên kết + 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 @@ -370,8 +370,8 @@ Các màu chế độ tối Tối Lỗi nghiêm trọng - Tạo liên kết lời mời dùng một lần - Đang tạo liên kết… + 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 @@ -388,7 +388,7 @@ Tạo địa chỉ SimpleX Tạo hồ sơ Tạo hàng đợi - Tạo liên kết lưu trữ + Tạo đường dẫn lưu trữ tùy chỉnh Chủ đề tối Hạ cấp cơ sở dữ liệu @@ -412,7 +412,7 @@ \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ở ứng dụng SimpleX Chat. + 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ỉ? @@ -447,10 +447,10 @@ 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 liên kết? + Xóa đường dẫn? Xóa hồ sơ Xóa cho mọi người - Xóa liên kết + Xóa đường dẫn Xóa ảnh Xóa các tin nhắn Xóa nhóm? @@ -464,7 +464,7 @@ Xóa hàng đợi Công cụ nhà phát triển THIẾT BỊ - Tùy chọn nhà phát triển + 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! @@ -491,9 +491,9 @@ Các máy chủ đã kết nối Lỗi xác nhận Đã hoàn thành - Các khúc đã bị xóa - Các khúc đã được tải xuống - Các khúc đã được tải lên + 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 @@ -535,12 +535,12 @@ Đã bị tắt Tắt đã bị tắt - Tin nhắn tự xóa bị cấm trong nhóm này. + 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ờ phương tiện truyền thông + 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! @@ -550,7 +550,7 @@ 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ệ đã lưu trữ + 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 @@ -559,7 +559,7 @@ 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 SimpleX Chat đã được xuất + 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. @@ -592,18 +592,18 @@ 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ở SimpleX Chat + 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 tin đã tải về + 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 liên kết + Đ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 @@ -637,7 +637,7 @@ 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à phương tiện được lưu trữ + 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)! @@ -682,25 +682,25 @@ 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 SimpleX Chat + 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 liên kết nhóm - Lỗi thay đổi quyền hạn + 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 liên kết 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 SimpleX Chat + 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 SimpleX Chat - Lỗi xuất cơ sở dữ liệu SimpleX Chat + 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 @@ -718,15 +718,15 @@ 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 liên kết nhóm + 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 ứng dụng - Lỗi dừng ứng dụng + 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 @@ -737,7 +737,7 @@ 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 quyền hạn + Mở rộng chọn chức vụ THỬ NGHIỆM Mở rộng Thoát mà không lưu @@ -749,14 +749,14 @@ 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 tin nhắn - Không tìm thấy tập tin - có thể tập tin đã bị xóa và hủy bỏ. - Lỗ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 tin nhắn + 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 tin + 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 @@ -764,19 +764,19 @@ Tập tin Lỗi máy chủ tệp: %1$s Trạng thái tệp - Tệp và phương tiện truyền thông không được cho phép - Tệp và phương tiện truyền thông bị cấm trong nhóm này. + 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 & phương tiện truyền thông - Tệp và phương tiện truyền thông bị cấm! + 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à phương tiện truyền thông - Tệp đã bị xóa hoặc liên kết không hợp lệ + 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 SIMPLEX CHAT + 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! 🚀 @@ -828,26 +828,26 @@ Chào buổi chiều! Giao diện tiếng Pháp Đã tìm thấy máy tính - Liên kết đầy đủ + 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 nhóm có thể thả cảm xúc tin nhắn. - Các thành viên nhóm có thể xóa theo cách không thể hồi phục các tin nhắn đã gửi. (24 giờ) - Các thành viên trong nhóm có thể gửi tin nhắn thoại. + 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 - Liên kết nhóm + Đường dẫn nhóm Nhóm Tên đầy đủ nhóm: - Các thành viên trong nhóm có thể gửi tin nhắn tự xóa. - Các thành viên trong nhóm có thể gửi tệp và phương tiện truyền thông. - Các thành viên trong nhóm có thể gửi liên kết SimpleX. + 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 trong nhóm có thể gửi tin nhắn trực tiếp. - Liên kết nhóm + 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 @@ -907,15 +907,15 @@ 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ẻ liên kết. + 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 liên kết 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 liên kết mời.]]> + 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ẻ liên kết.]]> + 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 @@ -933,15 +933,15 @@ 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 liên kết dùng một lần + ẩ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 tới các máy chủ dùng để truyền dẫn tin nhắn. + Để 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 liên kết địa chỉ liên lạc + ẩn danh qua đường dẫn địa chỉ liên lạc Cuộc gọi thoại đến - Quyền hạn ban đầu - ẩn danh qua liên kết nhóm + 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 @@ -955,16 +955,16 @@ Phiên bản không tương thích MÀU SẮC GIAO DIỆN đã được mời - Liên kết không hợp lệ - tác vụ trò chuyện không hợp lệ + Đườ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! - Liên kết không hợp lệ! - Liên kết không hợp lệ + Đườ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ệ! - Liên kết kết nối 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 @@ -1023,7 +1023,7 @@ Chế độ khóa Giao diện tiếng Ý thiết bị này v%s)]]> - Biểu đạt cảm xúc tin nhắn bị cấm trong nhóm này. + 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ữ @@ -1047,17 +1047,17 @@ Trung bình Tin nhắn động Giữ lại cuộc trò chuyện - Hình ảnh xem trước của liên kết + 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 bị cấm trong nhóm này. + 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ó có thể được thay đổi sau trong phần cài đặt. + 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 @@ -1066,7 +1066,7 @@ Tin nhắn Lỗi keychain đã rời - đã được mời thông qua liên kết nhóm của bạn + đã đượ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 @@ -1081,7 +1081,7 @@ 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à phương tiệ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ị! @@ -1144,10 +1144,10 @@ Cuộc trò chuyện mới phút Kho lưu trữ cơ sở dữ liệu mới - Quyền hạn thành viên 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 phương tiện mới + 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. @@ -1178,13 +1178,13 @@ 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ó nội dung + 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 - Liên kết lời mời dùng một lần + Đườ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 @@ -1192,7 +1192,7 @@ 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à phương tiện. + 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! @@ -1200,10 +1200,10 @@ OK Chỉ xóa cuộc trò chuyện Chỉ bạn mới có thể gửi tin nhắn thoại. - bảo mật đầu cuối 2 lớp.]]> + 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. - Liên kết lời mời dùng một lần + Đườ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ờ) @@ -1212,12 +1212,12 @@ đượ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 (quyền hạn quan sát viên) + 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 - Mở hồ sơ 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 @@ -1228,7 +1228,7 @@ 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ở liên kết 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. Liên kết SimpleX không đáng tin cậy sẽ được đánh dấu màu đỏ. + 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 @@ -1244,7 +1244,7 @@ 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 hội thoại + 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ữ @@ -1256,14 +1256,14 @@ 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ệ hệ của bạn… + 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 cũ %1$s + 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 @@ -1285,7 +1285,7 @@ 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 + 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. @@ -1296,12 +1296,12 @@ 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 tớ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. + 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ể trò chuyện nếu bạn làm mất nó. + 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. @@ -1345,12 +1345,12 @@ 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à phương tiện truyền thô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 một lần? + Đị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.]]> @@ -1360,7 +1360,7 @@ 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 & phương tiệ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.]]> @@ -1373,15 +1373,15 @@ %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]]> - cho phép SimpleX chạy trong nền trong hộp thoại tiếp theo. Nếu không, thông báo sẽ bị vô hiệu hóa.]]> + 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.]]> - Dịch vụ nền SimpleX – nó tiêu tốn một vài phần trăm pin mỗi ngày.]]> + 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.]]> - Chọn nhà cung cấp + 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 @@ -1393,10 +1393,1021 @@ 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 - Cho phép flux + Sử dụng Flux trong cài đặt Mạng & máy chủ để bảo mật siêu dữ liệu tốt hơn. 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. - \ No newline at end of file + 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 là quan sát viê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ợ + 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ủ + Lỗi chấp nhận thành viên + %d cuộc trò chuyện với thành viên + %d tin nhắn + đã chấp nhận %1$s + đã chấp nhận bạn + Vui lòng đợi các kiểm duyệt viên nhóm xem xét yêu cầu tham gia nhóm của bạn. + Thành viên mới muốn tham gia nhóm. + tất cả + Thu nạp thành viên + tắt + Xem xét các thành viên + Thêm đường dẫn ngắn + Trò chuyện với các quản trị viên + Trò chuyện với thành viên + Chấp nhận thành viên + Chấp nhận là thành viên + Thành viên sẽ tham gia nhóm, chấp nhận thành viên? + Chấp nhận là quan sát viên + đã được xem xét bởi các quản trị viên + Chấp nhận + đang chờ xem xét + xem xét + Không có cuộc trò chuyện nào với thành viên + Các cuộc trò chuyện với thành viên + 1 cuộc trò chuyện với 1 thành viên + %d cuộc trò chuyện + liên hệ đã bị xóa + liên hệ chưa sẵn sàng + chưa được đồng bộ hóa + Báo cáo đã được gửi cho các kiểm duyệt viên + không thể gửi tin nhắn + liên hệ đã bị vô hiệu hóa + nhóm đã bị xóa + đã xóa khỏi nhóm + yêu cầu tham gia đã bị từ chối + thành viên có phiên bản cũ + Xóa cuộc trò chuyện + Xóa cuộc trò chuyện với thành viên? + Trò chuyện với các quản trị viên + Từ chối + Từ chối thành viên? + Lỗi xóa cuộc trò chuyện với thành viên + bạn đã chấp nhận thành viên này + Xem xét các thành viên trước khi chấp nhận (knocking). + Lưu cài đặt thu nạp? + Đặt thu nạp thành viên + Bạn không thể gửi tin nhắn! + Bạn có thể xem các báo cáo của mình trong Cuộc trò chuyện với các quản trị viên. + bạn đã rời đi + 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 0477307343..5f729d4ea3 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 @@ -183,7 +183,7 @@ 点击以加入隐身聊天 你的聊天资料将被发送给群成员 你正在尝试邀请与你共享隐身个人资料的联系人加入你使用主要个人资料的群 - 隐身模式通过为每个联系人使用新的随机配置文件来保护你的隐私。 + 隐身模式通过为每个联系人使用新的随机个人资料来保护你的隐私。 你正在为该群使用隐身个人资料——为防止共享你的主要个人资料,不允许邀请联系人 通过一次性链接隐身 只有群主可以启用语音信息。 @@ -229,7 +229,7 @@ 按聊天资料(默认)或按连接(BETA)。 检查服务器地址并重试。 清除验证 - 关闭按键 + 关闭 配置 ICE 服务器 确认 确认你的证书 @@ -460,7 +460,7 @@ 无效的服务器地址! 邀请成员 离开群 - 仅本地配置文件数据 + 仅本地个人资料数据 即时通知 即时通知! 使用你的凭据登录 @@ -575,7 +575,7 @@ 更新网络设置? 只有你可以不可逆地删除消息(你的联系人可以将它们标记为删除)。(24小时) 重新启动应用程序以创建新的聊天资料。 - 服务器需要授权才能创建队列,检查密码 + 服务器需要授权才能创建队列,检查密码。 测试在步骤 %s 失败。 你已经有一个显示名相同的聊天资料。请选择另一个名字。 已发送 @@ -589,8 +589,8 @@ 已删除 %1$s 你删除了 %1$s 你的个人资料将发送给你收到此链接的联系人。 - 正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。 - 你已连接到用于接收该联系人消息的服务器。 + 正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。 + 你已连接到用于接收该连接消息的服务器。 你分享了一次性链接 很可能此联系人已经删除了与你的联系。 资料图片占位符 @@ -609,7 +609,7 @@ 你当前的聊天数据库将被删除并替换为导入的数据库。 \n此操作无法撤消——你的个人资料、联系人、消息和文件将不可逆地丢失。 已邀请 %1$s 保存群资料 - 服务器地址中的证书指纹可能不正确 + 服务器地址指纹和证书不匹配。 请使用 %1$s 检查你的网络连接,然后重试。 多个聊天资料 数据库不能正常工作。点击了解更多 @@ -777,7 +777,7 @@ 给我们发电子邮件 SMP 服务器 尚不支持发送文件 - 正在尝试连接到用于从该联系人接收消息的服务器。 + 尝试连接到用于从该连接接收消息的服务器。 尚不支持接收文件 未知消息格式 测试服务器 @@ -853,7 +853,7 @@ SimpleX 团队 %1$s 名成员 - 你将在组主设备上线时连接到该群,请稍等或稍后再检查! + 你将在群主设备上线时连接到该群,请稍等或稍后再检查! 当你启动应用或在应用程序驻留后台超过30 秒后,你将需要进行身份验证。 连接到 SimpleX Chat 开发者提出任何问题并接收更新 。]]> 你已接受连接 @@ -861,7 +861,7 @@ %1$d 条已跳过消息 %ds 更新内容 - 你被邀请加入群 + 你受邀加入群 你没有聊天记录 等待图像中 语音消息 @@ -885,7 +885,7 @@ 你将 %s 的角色更改为 %s 你将自己的角色更改为 %s 你已更改地址 - 你可以共享链接或二维码——任何人都可以加入该群。如果你稍后将其删除,你不会失去该组的成员。 + 你可以共享链接或二维码——任何人都可以加入该群。如果你稍后将其删除,你不会失去该群的成员。 间接(%1$s) 在移动应用程序中打开按钮。]]> SimpleX @@ -905,7 +905,7 @@ %d 天 %dw 你被邀请加入群。 加入以与群成员联系。 - 你加入了这个群。连接到邀请组成员。 + 你加入了这个群。连接到邀请群成员。 你更改了 %s 的地址 你已离开 已选择 %d 名联系人 @@ -929,7 +929,7 @@ 观察员 你是观察者 更新群链接错误 - 你无法发送消息! + 你是观察员 初始角色 请联系群管理员。 系统 @@ -939,7 +939,7 @@ 隐藏 将个人资料设置为私密! 静音 - 保存并更新组配置文件 + 保存并更新群资料 不再显示 不活跃时静音! 语音和视频通话 @@ -969,8 +969,8 @@ 感谢用户——通过 Weblate 做出贡献! 解除静音 欢迎消息 - 当静音配置文件处于活动状态时,你仍会收到来自静音配置文件的电话和通知。 - 你可以隐藏或静音用户配置文件——长按以显示菜单。 + 当静音个人资料处于活动状态时,你仍会收到来自静音个人资料的电话和通知。 + 你可以隐藏或静音用户个人资料——长按以显示菜单。 欢迎消息 确认数据库升级 实验性 @@ -1004,7 +1004,7 @@ 视频已发送 要求接收视频 视频将在你的联系人完成上传后收到。 - 服务器需要授权来上传,检查密码 + 服务器需要授权来上传,检查密码。 上传文件 XFTP 服务器 你的 XFTP 服务器 @@ -1059,7 +1059,7 @@ 当你或你的连接使用旧数据库备份时,可能会发生这种情况。 解密错误 请向开发者报告。 - 上一条消息的散列不同。 + 上条消息的哈希值不同。 下一条消息的 ID 不正确(小于或等于上一条)。 \n它可能是由于某些错误或连接被破坏才发生。 停止文件 @@ -1068,10 +1068,10 @@ 停止接收文件? 即将停止接收文件。 停止 - 撤销文件 - 撤销文件? + 吊销文件 + 吊销文件? 文件将从服务器中删除。 - 撤销 + 吊销 音频/视频通话 " \n在 v5.1 版本中可用" @@ -1173,7 +1173,7 @@ 与你的联系人保持连接。 与联系人分享 邀请朋友 - 保存自动接受设置 + 保存 SimpleX 地址设置 保存设置? 停止分享 你好! @@ -1232,7 +1232,7 @@ 中止地址更改 允许发送文件和媒体。 文件和媒体 - 只有组主可以启用文件和媒体。 + 只有群主可以启用文件和媒体。 文件和媒体被禁止。 成员可以发送文件和媒体。 禁止发送文件和媒体。 @@ -1268,7 +1268,7 @@ 你的个人资料 %1$s 将被共享。 将为所有联系人启用送达回执功能。 打开应用程序设置 - 为所有组启用 + 为所有群启用 %s、%s 和 %d 其他成员已连接 已为 %d 联系人启用送达回执功能 已同意 %s 的加密 @@ -1277,7 +1277,7 @@ 使用新的隐身个人资料 过滤未读和收藏的聊天记录。 已更改安全密码 - 将为所有可见聊天配置文件中的所有联系人启用送达回执功能。 + 将为所有可见聊天个人资料中的所有联系人启用送达回执功能。 应用电池使用情况 / 无限制。]]> %s 在 %s 禁用回执? @@ -1285,12 +1285,12 @@ 可以在联系人和群设置中覆盖它们。 对所有联系人关闭 随机密码以明文形式存储在设置中。 \n你可以稍后更改。 - 已禁用 %d 组的送达回执功能 + 已禁用 %d 群的送达回执功能 需要为 %s 重新协商加密 SimpleX 无法在后台运行。只有在应用程序运行时,你才会收到通知。 启用(保留覆盖) 即将更新数据库加密密码并将其存储在设置中。 - 使用当前配置文件 + 使用当前个人资料 从设置中删除密码? 同意加密 启用回执? @@ -1309,12 +1309,12 @@ 为群禁用回执吗? %s、%s 和 %s 已连接 修复群成员不支持的问题 - 已为 %d 组启用送达回执功能 + 已为 %d 群启用送达回执功能 重新协商 禁用(保留覆盖) 设置数据库密码 已禁用 %d 联系人的送达回执功能 - 启用(保留组覆盖) + 启用(保留群覆盖) 应用电池使用情况 / 无限制。]]> 送达回执已禁用 打开数据库文件夹 @@ -1330,10 +1330,10 @@ 更改密码或重启应用后,密码将以明文形式保存在设置中。 粘贴你收到的链接以与你的联系人联系… 送达回执 - 没有选择聊天 + 没有选中的聊天 可以加密 重新协商加密 - 禁用(保留组覆盖) + 禁用(保留群覆盖) 为群启用回执吗? 修复联系人不支持的问题 对 %s 加密正常 @@ -1342,10 +1342,10 @@ 禁用通知 回复 不启用 - 连接请求将发送给该组成员。 + 连接请求将发送给该群成员。 密码以明文形式存储在设置中。 同步连接时出错 - 这些设置适用于你当前的配置文件 + 这些设置适用于你当前的个人资料 允许为 %s 重新协商加密 为所有人启用 需要重新协商加密 @@ -1365,8 +1365,8 @@ 打开 创建成员联系人时出错 发送私信来连接 - 发送私信 - 已直连 + 发送私信来连接 + 已请求连接 展开 重复连接请求吗? 已删除联系人 @@ -1557,7 +1557,7 @@ 显示名无效。请另选一个名称。 慢函数 显示缓慢的 API 调用 - 过往成员 %1$s + 成员 %1$s 未知 未知状态 开发者选项 @@ -1852,7 +1852,7 @@ 显示百分比 不活跃 缩放 - 所有配置文件 + 所有个人资料 文件 没有信息,试试重新加载 服务器信息 @@ -1917,8 +1917,7 @@ 重置 服务器地址不兼容网络设置:%1$s。 起始自 %s。 - 起始自 %s. -\n所有数据都是设备的私有数据。 + 自 %s 起 \n所有数据均私密地保存在你的设备上. 已订阅 已认可 服务器版本不兼容你的应用:%1$s. @@ -1931,7 +1930,7 @@ 订阅被忽略 已配置的 SMP 服务器 已配置的 XFTP 服务器 - 当前配置文件 + 当前个人资料 传输会话 已上传 已停用 @@ -1962,7 +1961,7 @@ 转发服务器 %1$s 连接目的地服务器 %2$s 失败。请稍后尝试。 转发服务器地址不兼容网络设置:%1$s。 转发服务器版本不兼容网络设置:%1$s。 - %1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置 + %1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置。 连接转发服务器 %1$s 出错。请稍后尝试。 模糊媒体文件 中度 @@ -2047,22 +2046,22 @@ 上传的数据库存档将永久性从服务器被删除。 确保代理配置正确 消息将被删除 - 此操作无法撤销! - 你的连接被移动到 %s,但在将你重定向到配置文件时发生了意料之外的错误。 + 你的连接被移动到 %s,但在切换个人资料时发生了错误。 代理不使用身份验证凭据 - 切换配置文件出错 + 切换个人资料出错 代理身份验证 删除存档? - 选择聊天配置文件 + 选择聊天个人资料 保存代理出错 密码 每个连接使用不同的代理身份验证凭据。 - 每个配置文件使用不同的代理身份验证。 + 每一个人资料使用不同的代理身份验证。 你的凭据可能以未经加密的方式被发送。 使用随机凭据 用户名 - 分享配置文件 + 分享资料 转发消息出错 - 在你选中消息后这些消息被删除。 + 在你选中消息后这些消息已被删除。 %1$d 个文件错误:\n%2$s 其他 %1$d 个文件错误。 %1$d 个文件未被下载。 @@ -2071,32 +2070,32 @@ 没什么可转发的! 仍有 %1$d 个文件在下载中。 %1$d 个文件下载失败。 - %1$d 个文件被删除了。 + 删除了 %1$d 个文件。 下载 %1$s 条消息未被转发 转发消息… 转发 %1$s 条消息 保存 %1$s 条消息 已静音 - 管理形状 + 消息形状 拐角 尾部 初始化 WebView 出错。确保你安装了 WebView 且其支持的架构为 arm64。\n错误:%s 应用会话 - 每次启动应用都会使用新的 SOCKS5 凭据。 + 每次启动应用都会使用新的 SOCKS 凭据。 服务器 打开 Safari 设置/网站/麦克风,接着在 localhost 选择“允许”。 要进行通话,请允许使用设备麦克风。结束通话并尝试再次呼叫。 - 单击地址附近的\"信息\"按钮允许使用麦克风。 - 每个服务器都会使用新的 SOCKS5 凭据。 + 单击地址栏附近的“信息”按钮允许使用麦克风。 + 每个服务器都会使用新的 SOCKS 凭据。 更好的消息日期。 更佳的安全性✅ 更佳的使用体验 可自定义消息形状。 - 一次性转发最多20条消息。 + 一次转发最多20条消息。 Trail of Bits 审核了 SimpleX 协议。 通话期间切换音频和视频。 - 对一次性邀请切换聊天配置文件。 + 对一次性邀请切换聊天个人资料。 更佳的通话 允许自行删除或管理员移除最多200条消息。 保存服务器出错 @@ -2117,21 +2116,21 @@ 创建一次性链接 用于社交媒体 或者私下分享 - 服务器运营者 - 网络运营者 - 30 天后将接受已启用的运营者的条款。 + 服务器运营方 + 网络运营方 + 30 天后将接受已启用的运营方的条款。 继续 稍后审阅 - 选择要使用的网络运营者。 + 选择要使用的网络运营方。 更新 你可以通过设置配置服务器。 - %s.]]> - 将于下列日期自动接受已启用的运营者的条款:%s。 + %s.]]> + 将于下列日期自动接受已启用的运营方的条款:%s。 预设服务器 你的服务器 接受条款的将来日期为:%s。 - 网络运营者 - 运营者 + 网络运营方 + 运营方 %s 台服务器 网站 无法加载当前条款文本,你可以通过此链接审阅条款: @@ -2148,33 +2147,33 @@ 用于消息 打开更改 打开条款 - 运营者服务器 - 已添加服务器到运营者 %s - 服务器运营者已更改。 + 运营方服务器 + 已添加服务器到运营方 %s + 服务器运营方已更改。 服务器协议已更改。 透明度 网络去中心化 - 应用中的第二个预设运营者! + 应用中的第二个预设运营方! 改进了聊天导航 查看更新后的条款 比如,如果你通过 SimpleX 服务器收到消息,应用会通过 Flux 服务器传送它们。 - 应用通过在每个对话中使用不同运营者保护你的隐私。 + 应用通过在每个对话中使用不同运营方保护你的隐私。 接受条款 模糊 - 地址或一次性链接? + 地址还是一次性链接? 已添加消息服务器 已添加媒体和文件服务器 地址设置 - 已接受条款 + 已接受的条款 应用工具栏 仅用于一名联系人 - 面对面或通过任何消息应用分享.]]> - %s.]]> - %s.]]> - %s.]]> - %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> %s的服务器,请接受使用条款。]]> - %s.]]> - 开启 flux + %s.]]> + 在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。 接受条款出错 为了更好的元数据隐私。 添加服务器出错 @@ -2190,10 +2189,10 @@ 此消息被删除或尚未收到。 连接达到了未送达消息上限,你的联系人可能处于离线状态。 为了防止链接被替换,你可以比较联系人安全代码。 - 你可以在“网络和服务器”设置中配置运营者。 - 接受运营者条款的日期:%s + 你可以在“网络和服务器”设置中配置运营方。 + 接受运营方条款的日期:%s 远程移动设备 - 或者导入压缩文件 + 或者导入存档文件 小米设备:请在系统设置中开启“自动启动”让通知正常工作。]]> 消息太大! 你可以复制并减小消息大小来发送它。 @@ -2229,8 +2228,301 @@ 聊天 将从聊天中删除成员 - 此操作无法撤销! 请减小消息尺寸并再次发送。 - 当启用了超过一个运营者时,没有一个运营者拥有了解谁和谁联络的元数据。 + 当启用了超过一个运营方时,没有一个运营方拥有了解谁和谁联络的元数据。 已接受邀请 被请求连接 - 关于运营者 - \ No newline at end of file + 关于运营方 + SimpleX Chat 和 Flux 达成协议将 Flux 运营的服务器包括在应用中。 + 修复 + 修复连接? + 正进行加密重协商。 + 连接需要加密重协商。 + 保存数据库出错 + 启用日志 + 连接未就绪。 + 创建聊天列表出错 + 更新聊天列表出错 + 加载聊天列表出错 + 列表 + 无聊天 + 找不到聊天 + %s列表中没有聊天。 + 收藏 + + 列表名… + 所有列表的名称和表情符号都应不同。 + 保存列表 + 删除 + 用%s打开 + 将从%s列表删除所有聊天,列表本身也将被删除 + 企业 + 编辑 + 添加到列表 + 联系人 + 创建列表 + 添加列表 + 全部 + 删除列表? + 没有未读聊天 + 附注 + 更改列表 + 更改顺序 + 创建报告出错 + 保存设置出错 + 存档 + 删除举报 + 举报 + 举报其他:仅协管会看到。 + 举报成员个人资料:仅协管会看到。 + 举报违规:仅协管会看到。 + 存档举报 + 举报内容:仅协管会看到。 + 举报垃圾信息:仅协管会看到。 + 协管 + 另一个理由 + 已存档的举报 + 违反社区指导方针 + 不当内容 + 不当个人资料 + 仅发送人和协管能看到 + 只有你和协管能看到 + 垃圾信息 + 存档举报? + 举报理由? + 将为你存档该举报。 + 存档了 %s 的举报 + 举报 + 1 个举报 + 成员举报 + %d 个举报 + 垃圾信息 + 连接被阻止 + 连接被服务器运营方阻止:\n%1$s. + 内容违反使用条款 + 文件被服务器运营方阻止:\n%1$s. + 询问 + + + 打开 web 链接? + 打开链接 + 打开来自聊天列表的链接 + 设置聊天名称… + 停用消息自动删除? + 停用消息删除 + 1 年 + 默认(%s) + 从你的设备删除聊天消息。 + 此操作无法撤销 —— 比此聊天中所选消息更早发出并收到的消息将被删除。 + 此聊天中的消息永远不会被删除。 + 更改消息自动删除设置? + 用于消息收发的 TCP 端口 + 未指定端口时,使用 TCP 端口 %1$s。 + 使用 web 端口 + 全部静音 + 取消提及的已读状态 + 每条消息最多提及 %1$s 名成员! + 将为你存档所有举报。 + 存档所有举报? + 存档 %d 份举报? + 存档举报 + 所有 协管 + 仅自己 + 举报:%s + 禁止向 协管 举报消息。 + 此群禁止消息举报。 + 成员可以向 协管 举报消息。 + 允许向 协管 举报消息。 + 提及成员👋 + 更好的群性能 + 更好的隐私和安全 + 不错过重要消息。 + 更快地删除群。 + 更快发送消息。 + 被提及时收到通知。 + 帮助管理员管理群。 + 将聊天组织到列表 + 私密媒体文件名。 + 发送私下举报 + 在聊天中设置消息过期时间。 + 被拒绝 + 被拒绝 + 数据库密码短语读取出错 + 无法读取 Keystore 中的密码短语,请手动输入它。这可能在不兼容本应用的系统更新后出现。如果不是这种情况,请联系开发者。 + 无法读取 Keystore 中的密码短语,请手动输入它。这可能在不兼容本应用的系统更新后出现。如果不是这种情况,请联系开发者。 + 待批准 + 待批准 + 条款已更新 + 所有来自这些成员的新消息都将被隐藏! + 为所有其他成员封禁这些成员? + 将从群中移除这些成员 — 此操作无法撤销! + 将显示来自这些成员的消息! + 删除成员吗? + 为所有其他成员解封这些成员吗? + 协管 + 将从聊天中移除这些成员 — 此操作无法撤销! + 隐私政策和使用条款。 + 接受 + 使用 SimpleX Chat 代表您同意:\n- 在公开群中只发送合法内容\n- 尊重其他用户 – 没有垃圾信息。 + 服务器运营方无法访问私密聊天、群和你的联系人。 + 配置服务器运营方 + 不支持的连接链接 + SimpleX 频道链接 + 短链接 + 此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。 + 完整链接 + 全部服务器 + 关闭 + 预设服务器 + 仅预设服务器使用 TCP 协议 443 端口。 + 接受成员出错 + 举报已发送至 协管 + %d 个聊天 + 和成员的 %d 个聊天 + %d 条消息 + 接受了 %1$s + 接受了你 + 你接受了该成员 + 新成员要加入本群。 + 审核 + 待审核 + 全部 + 成员准入 + 关闭 + 删除 + 接受 + 和成员聊天 + 和管理员聊天 + 没有和成员的聊天 + 接受为成员 + 接受成员 + 成员将加入本群,接受成员吗? + 由管理员审核 + 设置成员入群准许 + 和成员聊天 + 和管理员聊天 + 准许入群前审核成员(knocking)。 + 请等待群的 moderator 审核你加入该群的请求。 + 审核成员 + 保存入群设置? + 你可以在和管理员和聊天中查看你的举报。 + 接受为观察员 + 和一名成员的一个聊天 + 无法发送消息 + 你离开了 + 删除聊天出错 + 你无法发送消息! + 禁用了联系人 + 群被删除了 + 从群被删除了 + 加入请求被拒绝 + 删除聊天 + 删除和成员的聊天吗? + 未同步 + 成员有旧版本 + 删除了联系人 + 联系人未就绪 + 拒绝成员? + 升级地址 + 接受联络请求 + 添加消息 + 端到端加密.]]> + 仅在你的请求被接收后.]]> + 连接 + 联系人应当接受… + 更改户出错 + 打开聊天时出错 + 打开群时出错 + 拒绝联络请求出错 + 加群 + 打开聊天 + 打开新聊天 + 打开新群 + 打开以接受 + 打开以连接 + 打开以加入 + 地址不会长,将通过该简短地址分享个人资料。 + 拒绝联络请求 + 发送了请求 + 发送联络请求? + 发送请求 + 发送无消息请求 + 连接后发送给你的联系人。 + 升级群链接? + 升级 + 升级地址? + 不会通知发送者。 + 欢迎消息 + 你的个人资料 + 无法更改个人资料 + 要在连接尝试后使用不同的个人资料,请删除聊天并再次使用该链接。 + 和管理员聊天 + 在成员加入前和这些人聊天 + 更快地连接!🚀 + 消耗更少的移动网络数据。 + 轻按连接后即刻发消息。 + 新的群角色:协管 + 无私密路由会话 + 私密路由超时 + 协议后台超时 + 删除消息并封禁成员。 + 审核群成员 + 向群发送私密反馈。 + TCP 连接后台超时 + 正加载个人资料… + 自我介绍: + 简短描述: + 自我介绍: + 自我介绍过大 + 描述过大 + 接受联络请求 + 企业连接 + + 轻按连接进行聊天 + 轻按连接来发送请求 + 轻按加入群 + 你的企业联系人 + 你的联系人 + 你的群 + 只为新联系人设置了消失时间。 + 4 种新的界面语言 + 加泰罗尼亚语、印尼语、罗马尼亚语和越南语——感谢我们的用户! + 创建地址 + 默认启用定时消失消息。 + 保持聊天洁净 + 设置自我介绍和欢迎消息。 + 分享旧地址 + 分享旧链接 + 分享地址 + SimpleX 短地址 + 链接不会长,群资料会通过短链接分享。 + 更新你的地址 + 升级群链接 + 使用隐身个人资料 + 欢迎联系人👋 + 来自%1$s群的连接请求 + 此设置用于当前个人资料 + 来自群的联络请求 + 成员被删除——无法接受请求 + 只有你的联系人允许的情况下才允许文件和媒体。 + 允许你的联系人发送文件和媒体。 + 机器人 + 你和你的联系人都可发送文件和媒体。 + 此聊天禁止文件和媒体。 + 只有你可以发送文件和媒体。 + 只有你的联系人可以发送文件和媒体。 + 打开来使用机器人 + 禁止发送文件和媒体。 + 轻按“连接”使用机器人 + 你必须已连接才能发送命令。 + 已废弃的选项 + 打开干净链接 + 打开完整链接 + 删除链接跟踪 + SimpleX 中继链接 + 标记为已读时出错 + 目标服务器地址的指纹和证书不匹配:%1$s。 + 转发服务器地址的指纹和证书不匹配:%1$s。 + 服务器地址证书和证书不匹配:%1$s。 + 无订阅 + 未连接到用于从该连接接收消息的服务器(无订阅)。 + 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 fd58811439..f9a1a5b131 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 @@ -5,7 +5,7 @@ 關於 SimpleX 接受 接受 - 1天 + 1 天 1 個月 接受 關於 SimpleX Chat @@ -23,7 +23,7 @@ 全名: 使用更多電量!程式始終在背景中運行 – 通知會立即顯示。]]> 對電量友善。程式每10分鐘檢查一次訊息。你可能會錯過電話或警急訊息。]]> - 回應通話請求 + 回應通話 清除 允許向群組內的成員傳送私訊。 %d 秒 @@ -31,32 +31,32 @@ 自動接受聯絡人請求 允許傳送自動銷毀的訊息。 - 無法初始化數據庫 - 一直開啟 + 無法初始化資料庫 + 總是開啟 關閉 SimpleX 鎖定 啟用 SimpleX 鎖定 複製 回覆 分享 - 附件 + 附加 和開發人員對話 - 你的對話 + 聊天 分享檔案… - 分享訊息 + 分享訊息… 取消圖片預覽 允許使用語音訊息? 取消 取消實況訊息 - 選擇檔案 + 檔案 相機 - 從圖片庫選擇圖片 - 接受匿名聊天模式 + 從圖片庫 + 接受匿名 所有訊息記錄會刪除 - 這不能還原!這些訊息只會在你裝置中刪除。 清除 - 要清除對話記錄? + 清除聊天? 取消連結預覽 清除 - 清除對話記錄 + 清除聊天 分享一次性連結 傳送問題和想法給開發者 如何使用 @@ -81,30 +81,30 @@ 通話已經結束了! 關閉語音 開啟語音 - 通話已經取消了 + 通話已結束 通話中 開啟喇叭 通話 - 樣式 + 外觀 語音通話(沒有端對端加密) 語音通話 - 語音 & 視訊通話 + 語音和視訊通話 視訊通話 粗體 自動接收圖片 備份應用程式資料 應用程式圖示 - 已匯入對話數據庫 + 已匯入對話資料庫 Android 金鑰庫是用於安全地儲存密碼 - 確保通知推送服務的運作。 請注意:如果你忘記了密碼你將不能再次復原或修改密碼。]]> 當你重新啟動應用程式或修改密碼後, Android 金鑰庫將用來安全地儲存密碼 - 將允許接收訊息通知。 - 聊天室已停止運作 + 聊天已停止 只有這個群組的負責人才能修改群組內的設定。 - 修改 - 新增新的個人檔案 + 變更 + 新增個人檔案 所有對話和對話記錄會刪除 - 這不能還原! - 匿名聊天模式 - 經常 + 匿名 + 總是 開啟 你的設定 允許你的聯絡人傳送語音訊息。 @@ -115,13 +115,13 @@ 管理員可以建立加入群組的連結。 使用二維碼掃描以新增伺服器。 聊天室運行中 - 對話數據庫 - 聊天室已停止運作 + 聊天資料庫 + 聊天已停止 停止 - 已刪除數據庫的對話內容 - 修改數據庫密碼? + 已刪除資料庫的對話內容 + 修改資料庫密碼? 確定要退出群組? - 退出 + 離開 無法邀請聯絡人! 已連接 退出 @@ -133,11 +133,11 @@ 群組全名: 對話設定 關閉 - + 自動銷毀訊息 - 群組內的成員可以私訊群組內的成員。 + 成員可以傳送私訊。 %d 分鐘 - %d 月 + %d 個月 通話結束 %1$s 取消檔案預覽 無法接收檔案 @@ -146,8 +146,8 @@ 每個聊天室的設定。]]> 返回 省電模式運行中,關閉了背景通知服務和定期更新接收訊息。你可以在通知設定內重新啟用。 - 匿名聊天模式 - 預設值 (%s) + 匿名模式 + 預設(%s) 群組設定 聯絡人設定 分享媒體… @@ -175,9 +175,9 @@ 你的設定 新增到另一個裝置上 檢查輸入的伺服器地址,然後再試一次。 - 終端機對話 + 聊天終端機 於 Github 給個星星 - 隱身模式透過為每個聯絡人使用新的隨機設定檔來保護您的隱私。 + 隱身模式透過為每個聯絡人使用新的隨機設定檔來保護你的隱私。 這樣是允許每一個對話中擁有不同的顯示名稱,並且沒有任何的個人資料可用於分享或有機會外洩。 只有你的聯絡人允許的情況下,才允許自動銷毀訊息。 允許你的聯絡人傳送自動銷毀的訊息。 @@ -187,8 +187,8 @@ 允許傳送語音訊息。 多久後刪除 群組內所有成員會保持連接。 - 自訂顏色 - 已移除 + 輔色 + 已被審查 SimpleX 群組連結 私人檔案名稱 加入成員(s)時出錯 @@ -205,9 +205,9 @@ 未知的訊息格式 無效的訊息格式 - 實況 - 無效對話 - 無效數據 + 直播 + 無效的聊天 + 無效的數據 連接 %1$d 已建立連接 已邀請連接 @@ -222,7 +222,7 @@ 傳送訊息時出錯 聯絡人已存在 你已經連接到 %1$s。 - 使用個人檔案(預設值)或使用連接(測試版)。 + 按聊天個人檔案(預設)或按連接(測試版)。 減少電量使用 更多改進即將推出! 意大利語言界面 @@ -233,20 +233,20 @@ 透過一次性連結連接? 加入群組? 你的個人檔案將傳送給你接收此連結的聯絡人。 - 你將加入此連結內的群組並且連接到此群組成為群組內的成員。 + 你將連接至此群組內的所有成員。 連接 錯誤 連接中 你已連接到此聯絡人使用的伺服器以接收訊息。 - 嘗試連接至用於接收此聯絡人訊息的伺服器 (錯誤:%1$s)。 + 嘗試連接至用於接收此聯絡人訊息的伺服器 (錯誤:%1$s)。 正在嘗試連接到用於接收此聯絡人訊息伺服器。 已刪除 - 已標記為刪除 - 在瀏覽器中開啟連結可能會有私隱疑慮和不確定性。不受 SimpleX 信任的連結會顯示紅色。 + 已標記為已刪除 + 在瀏覽器中開啟連結可能會有隱私疑慮和不確定性。不受 SimpleX 信任的連結會顯示紅色。 儲存 SMP 伺服器時出錯 請確保 SMP 伺服器連結是正確的格式,每行也分隔且不重複。 更新網路配置時出錯 - 聯絡人載入失敗 + 聊天載入失敗 多個聯絡人載入失敗 請更新應用程式或聯絡開發人員。 連接超時 @@ -275,20 +275,20 @@ 即時通知 即時通知! 已禁用即時通知功能! - 數據庫目前沒有正常運作。點擊查看更多 + 資料庫目前沒有正常運作。點擊查看更多 SimpleX Chat 通話來電 通知服務 顯示預覽 通知預覽 如果你解鎖程式三十秒後在後台再次啟動或返回應用程式,你會需要進行多一次認證。 - 已解鎖 + 解鎖 使用你的憑據登入 使用終端機開啟對話 傳送訊息時出錯 大概是你的聯絡人已經刪除了和你的對話並且已經沒有和你有連接。 只為我刪除 - 為所有人刪除 - 已修改 + 為所有人 + 已編輯 已傳送 未經授權傳送 傳送失敗 @@ -320,8 +320,8 @@ 安全佇列 刪除佇列 斷開連接 - 禁用電量優化 為了 SimpleX 在下一個對話中。否則,將禁用通知功能。]]> - 在接收通知之前,請你輸入數據庫的密碼 + 允許它以接收實時通知。]]> + 在接收通知之前,請你輸入資料庫的密碼 應用程式會定期取得新的訊息 — 它每天會消耗百分之幾的電量。應用程式將不使用推送通知 — 你裝置中的數據不會傳送至伺服器。 SimpleX Chat 服務 正在接收訊息… @@ -332,7 +332,7 @@ 顯示聯絡人名稱和訊息內容 隱藏聯絡人名稱和訊息內容 隱藏聯絡人: - 有新的訊息 + 新訊息 已連接 SimpleX 鎖定 為了保護你的個人訊息,開啟 SimpleX 鎖定。 @@ -340,7 +340,7 @@ 開啟 已開啟SimpleX 鎖定 你的裝置沒有啟用螢幕鎖定。你可以通過設定內啟用螢幕鎖定,當你啟用後就可以使用 SimpleX 鎖定。 - 修改 + 編輯 刪除 展露 刪除訊息? @@ -355,11 +355,11 @@ 下載檔案需要傳送者上線的時候才能下載檔案,請等待對方上線! 語音訊息 傳送實況的訊息 - 實況訊息! + 直播訊息! 傳送實況訊息 - 這會即時顯示你在輸入中的文字 (掃描或使用剪貼薄貼上) 權限被拒絕! - SimpleX 有一個後台通知服務 – 它每天使用百分之幾的電量。]]> + SimpleX 在背景運作而不是使用推送通知。]]> 定期通知 每十分鐘會檢查一次訊息,最快可設為每分鐘檢查一次 訊息文字 @@ -386,10 +386,10 @@ 驗證安全碼 傳送訊息 刪除個人檔案時出錯 - 停止對話 + 停止聊天 圖片不能解碼,嘗試其他圖片或聯絡開發人員。 檔案 - 大型檔案 + 大型檔案! 等待檔案中 確定 重設 @@ -397,7 +397,7 @@ 沒有詳細資料 一次性邀請連結 已複製至你的剪貼薄 - 透過連結 / 二維碼去連接 + 透過連結/QR 圖碼連接 掃描二維碼 (僅由群組成員儲存) 想和你對話! @@ -411,17 +411,17 @@ 使用 .onion 主機 Onion 主機不會啟用。 連接 - 刪除聯絡地址 + 刪除地址 顏色 如何使用你的伺服器 使用連結連接 掃描二維碼。]]> 設定 這個二維碼不是一個連結! - 當可行的時候 + 當可用時 核心版本:v%s simplexmq: v%s (%2s) - 建立聯絡地址 + 建立地址 編輯圖片 斜體 已拒絕通話 @@ -446,17 +446,17 @@ 標記為未讀 你的聯絡人需要上線才能連接成功。 \n你可以取消此連接和刪除此聯絡人(或者你可以稍後使用新的連結再試一次)。 - 二維碼 + QR 圖碼 個人檔案頭像 預覧連結圖片 SimpleX 地址 幫助 SimpleX 團隊 電郵 - 查看更多 + 更多 顯示二維碼 - 無效的二維碼 - 無效連結! + 無效的 QR 圖碼 + 無效的連結! 這個連結不是一個有效的連接連結! 已傳送連接請求 當群組的建立人上線,你便會成功連接至群組,請耐心等待! @@ -476,10 +476,10 @@ 安全碼 Markdown 幫助 在訊息中使用 Markdown 語法 - 人手輸入伺服器地址 + 手動輸入伺服器 使用伺服器 用於新的連接 - 無效的伺服器地址 + 無效的伺服器地址! 此伺服器用於你目前的個人檔案 使用 SimpleX Chat 伺服器? 你的 SMP 伺服器 @@ -491,7 +491,7 @@ 需要 Onion 主機會在可用時啟用。 - 刪除聯絡地址? + 刪除地址? 你的個人檔案只會儲存於你的裝置和只會分享給你的聯絡人。 SimpleX 伺服器並不會看到你的個人檔案。 儲存並通知你的聯絡人 儲存並通知你的多個聯絡人 @@ -501,12 +501,12 @@ 如何使用 Markdown 語法 你可以使用 Markdown 語法以更清楚標明訊息: 刪除線 - 你錯過了電話 + 已錯過通話 接收到回應… 連接中… - 通話完結 + 已結束 連接 - 網路 & 伺服器 + 網路和伺服器 進階設定 使用 SOCKS 代理伺服器 儲存並通知群組內的聯絡人 @@ -533,7 +533,7 @@ %s 並未驗證 你的 SimpleX 聯絡地址 你的個人檔案 - 數據庫密碼及匯出 + 資料庫密碼和匯出 傳送電郵 SimpleX 鎖定 SMP 伺服器 @@ -545,30 +545,29 @@ 有一些伺服器測試失敗: 掃描伺服器的二維碼 你的伺服器 - 連接時需要使用 Onion 主機。 -\n請注意:如果沒有 .onion 地址,您將無法連接到伺服器。 - 對話檔案 + 連接時需要使用 Onion 主機。 \n請注意:如果沒有 .onion 地址,你將無法連接到伺服器。 + 聊天個人檔案 透過群組連結 透過群組連結使用匿名聊天模式 一個使用了匿名聊天模式的人透過連結加入了群組 透過使用一次性連結匿名聊天模式連接 即時 定期的 - 關閉 - 你錯過了通話 + 停用 + 已錯過通話 開啟 開啟 SimpleX Chat 以接受通話 已拒絕通話 顯示 連接通話中 - 檔案及媒體檔案 + 檔案和媒體 重新啟動應用程式以建立新的對話檔案。 刪除所有檔案及媒體檔案? 刪除所有你的個人對話資料的檔案 %d 檔案(s) 的總共大小為 %s 訊息 永不 - 沒有接收到或傳送到的檔案 + 沒有已接收或已傳送的檔案 目前的密碼… 加密 修改設定時出錯 @@ -584,11 +583,11 @@ 已修改你的身份為 %s 連接中(已接受) 退出 - 負責人 + 擁有者 已移除 完成 連接中 - 創建人 + 建立者 新的成員身份 %1$s 位成員 修改群組內的設定 @@ -599,9 +598,9 @@ 成員的身份會修改為 "%s"。所有在群組內的成員都接收到通知。 成員的身份會修改為 "%s"。該成員將接收到新的邀請。 網路狀態 - 重置為預設值 + 重設為預設值 當你與某人分享已啟用匿名聊天模式的個人檔案時,此個人檔案將用於他們邀請你參加的群組。 - 黑暗 + 深色 為所有人刪除 語音訊息 已提供 %s: %2s @@ -613,7 +612,7 @@ 訊息草稿 保留最後一則帶附件的訊息草稿。 傳輸隔離 - 匯入對話數據庫? + 匯入對話資料庫? 請放置你的密碼於安全的地方,如果你遺失了密碼將不可能再次存取它。 金鑰庫錯誤 沒有聯絡人可以選擇 @@ -621,15 +620,15 @@ 刪除連結 完全去中心化 - 只有成員能看到。 禁止傳送自動銷毀的訊息。 - %d 個小時 + %d 小時 更新內容 帶有可選擇的歡迎訊息。 - 刪除數據庫時出錯 + 刪除資料庫時出錯 匯入 - %s 秒(s) - 加密數據庫時出錯 + %s 秒 + 加密資料庫時出錯 在金鑰庫儲存密碼 - 還原數據庫的備份 + 還原資料庫的備份 群組為不活躍狀態 邀請連結過時! 刪除群組? @@ -641,25 +640,25 @@ 語音訊息於這個聊天室是禁用的。 允許你的聯絡人可以完全刪除訊息。 沒有用戶識別符。 - 新一代的私密訊息平台 + 未來的訊息平台 去中心化的 - 人們只能在你分享了連結後,才能和你連接。 - 重新定義私隱 + 你決定誰可以連接。 + 重新定義隱私 建立你的個人檔案 這是如何運作 - 你可以之後透過設定修改。 + 它如何影響電池 私下連接 任何人都可以託管伺服器。 - 無視 + 忽略 語音通話來電 貼上你收到的連結 端對端加密 沒有端對端加密 關閉喇叭 訊息和檔案 - 私隱 & 安全性 + 隱私和安全性 主題 - 法語界面 + 法語介面 端對端加密的語音通話 拒絕 錯誤的訊息 ID @@ -670,32 +669,32 @@ 幫助 設定 幫助 SIMPLEX CHAT - 對話 + 聊天 開發者工具 SOCKS 代理伺服器 - 重新啟動應用程式以匯入對話數據庫。 + 重新啟動應用程式以匯入對話資料庫。 刪除所有檔案 啟用自動銷毀訊息? 刪除訊息 在多久後刪除訊息 請輸入正確的目前密碼。 - 已受加密的數據庫密碼是使用隨機性的文字,你可以修改它。 - 數據庫將加密。 - 數據庫將加密並且密碼會儲存於金鑰庫。 - 數據庫錯誤 - 不能讀取金鑰庫以儲存數據庫密碼 + 已受加密的資料庫密碼是使用隨機性的文字,你可以修改它。 + 資料庫將加密。 + 資料庫將加密並且密碼會儲存於金鑰庫。 + 資料庫錯誤 + 不能讀取金鑰庫以儲存資料庫密碼 錯誤:%s 檔案:%s - 需要數據庫的密碼以開啟對話。 + 需要資料庫的密碼以開啟對話。 輸入密碼… 輸入正確的密碼。 開啟對話 儲存密碼和開啟對話 - 你未完成修改數據庫密碼的程序。 - 密碼不存在於金鑰庫,請手動輸入它,有這種情況你可能是使用了備份用的工具。如果不是請聯絡開發人員。 + 你未完成修改資料庫密碼的程序。 + Keystore 中找不到密碼,請手動輸入它。如果你使用了備份工具還原應用程式的資料,則可能會發生這種情況。如果不是這種情況,請聯絡開發人員。 還原 - 還原數據庫的備份? - 還原數據庫時出錯 + 還原資料庫的備份? + 還原資料庫時出錯 加入 確定要加入群組? 加入匿名聊天模式 @@ -713,7 +712,7 @@ 刪除群組邀請連結時出錯 為終端機 本機名稱 - 數據庫 ID + 資料庫 ID 身份 傳送私人訊息 成員將被移除於此群組 - 這不能還原! @@ -734,17 +733,17 @@ 更新網路設定? 更新 更新設定會將客戶端重新連接到所有的伺服器。 - 刪除對話資料? - 刪除對話資料給 + 刪除聊天個人檔案? + 刪除聊天個人檔案給 檔案和伺服器連接 只有本機檔案 你的隨機個人檔案 系統 - 明亮 + 淺色 重設顏色 關閉 已接收,已禁用 - 設定為1天 + 設定為 1 天 聯絡人可以標記訊息為已刪除;你仍可以看到那些訊息。 禁止傳送語音訊息。 只有你的聯絡人可以傳送自動銷毀的訊息。 @@ -752,26 +751,26 @@ 不可逆地刪除訊息於這個聊天室內是禁用的。 只有你可以傳送語音訊息。 私訊群組內的成員於這個群組內是禁用的。 - 群組內的成員可以不可逆地刪除訊息。(24小時) + 成員可以不可逆地刪除訊息。(24 小時) 語音訊息 改善伺服器配置 當你切換至最近應用程式版面時,無法預覽程式畫面。 運行對話 - 數據庫密碼 - 匯出數據庫 - 匯入數據庫 - 新的數據庫存檔 - 舊的數據庫存檔 - 刪除數據庫 + 資料庫密碼 + 匯出資料庫 + 匯入資料庫 + 新的資料庫封存 + 舊的資料庫封存 + 刪除資料庫 開啟對話時出錯 停止對話? 設定密碼以匯出 停止對話時出錯 - 已受加密的數據庫是使用一個隨機性的文字。請在修改前將它匯出。 - 匯出數據庫時出錯 - 匯入數據庫時出錯 - 受加密的數據庫密碼會再次更新。 - 加密數據庫? + 已受加密的資料庫是使用一個隨機性的文字。請在修改前將它匯出。 + 匯出資料庫時出錯 + 匯入資料庫時出錯 + 受加密的資料庫密碼會再次更新。 + 加密資料庫? 邀請至群組 %1$s 邀請成員 群組找不到! @@ -779,18 +778,18 @@ 已刪除群組 群組已經刪除 已邀請 - 已透過連結邀請了你進群組 + 已透過你的群組連結邀請 聯絡人允許 %ds 私人通知 GitHub內查看更多。]]> 視訊通話來電 - 掛斷電話來電 + 掛斷 點對點 對話已經過端對端加密 對話沒有經過端對端加密 - 數據庫已加密! - 已加密數據庫 + 資料庫已加密! + 已加密資料庫 群組資料已經更新 成員 你:%1$s @@ -808,8 +807,8 @@ 禁止傳送自動銷毀的訊息。 禁止不可逆的訊息刪除。 禁止傳送語音訊息。 - 群組內的成員可以傳送自動銷毀的訊息。 - 自動銷毀訊息於這個群組內是禁用的。 + 成員可以傳送自動銷毀的訊息。 + 自動銷毀訊息已停用。 已提供 %s 儲存群組檔案時出錯 主題 @@ -823,9 +822,9 @@ 只有你的聊絡人可以不可逆的刪除訊息(你可以將它標記為刪除)。(24小時) 只有你的聯絡人可以傳送語音訊息。 禁止私訊群組內的成員。 - 不可逆地刪除訊息於這個群組內是禁用的。 - 群組內的成員可以傳送語音訊息。 - 語音訊息於這個群組內是禁用的。 + 不可逆地刪除訊息已停用。 + 成員可以傳送語音訊息。 + 語音訊息已停用。 %d 個月 %dm %dmth @@ -847,36 +846,32 @@ 驗證你與聯絡人的安全碼。 感謝用戶 - 使用 Weblate 的翻譯貢獻! 正在修改聯絡地址為 %s … - 受加密的數據庫密碼會再次更新和儲存於金鑰庫。 + 受加密的資料庫密碼會再次更新和儲存於金鑰庫。 SimpleX 是怎樣運作 - 當發生: -\n1. 訊息將在傳送至客戶端後兩天或在伺服器內三十天時過時。 -\n2. 訊息解密失敗,因為你或你的聯絡人用了舊的數據庫備份 -\n3. 連接被破壞。 - 兩層的端對端加密。]]> + 當發生: \n1. 訊息將在傳送至客戶端後兩天或在伺服器內三十天時過時。 \n2. 訊息解密失敗,因為你或你的聯絡人用了舊的資料庫備份 \n3. 連接被破壞。 + 只有客戶端裝置儲存個人檔案、聯絡人、群組,和訊息。 請放置你的密碼於安全的地方,如果你遺失了密碼,將不可能修改你的密碼。 - 停止聊天室以匯出對話,匯入或刪除對話數據庫。當聊天室停止後你將不能接收或傳送訊息。 + 停止聊天室以匯出對話,匯入或刪除對話資料庫。當聊天室停止後你將不能接收或傳送訊息。 你正在使用匿名聊天模式進入此群組 - 為了避免分享你的真實個人檔案,邀請聯絡人是不允許的。 你傳送了一個群組連結 你移除了 %1$s - 你退出了群組 + 你已退出 你修改了聯絡地址為 %s 連接中(邀請介紹階段) - 你目前的對話數據庫會刪除並且以你匯入的對話數據庫頂替上。 -\n這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。 + 你目前的對話資料庫會刪除並且以你匯入的對話資料庫頂替上。 \n這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。 透過聯絡人的邀請連結連接 透過一次性連結連接 傳輸隔離 更新傳輸隔離模式? - 為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 IDs 資料, SimpleX 平台有自家佇列的標識符,這對於你的每個聯絡人也是獨一無二的。 + 為了保護你的隱私,SimpleX 對你的每個聯絡人使用不同的 ID。 當應用程式是運行中 透過設定啟用於上鎖畫面顯示來電通知。 這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。 - 你必須在裝置上使用最新版本的對話數據庫,否則你可能會停止接收某些聯絡人的訊息。 + 你必須在裝置上使用最新版本的對話資料庫,否則你可能會停止接收某些聯絡人的訊息。 這操作不能還原 - 所有已經接收和傳送的檔案和媒體檔案將刪除。低解析度圖片將保留。 這設置適用於你目前的個人檔案 這操作無法撤銷 - 早於所選擇的時間發送和接收的訊息將被刪除。這可能需要幾分鐘的時間。 - 更新數據庫密碼 + 更新資料庫密碼 當每次啟動應用程式後你會需要輸入密碼 - 這不是儲存於你的個人裝置上。 你被邀請加入至群組 你將停止接收來自此群組的訊息。群組內的記錄會保留。 @@ -894,7 +889,7 @@ 開啟視訊 翻轉相機 待確認通話 - 你的私隱 + 你的隱私 你的通話 經由分程傳遞連接 在上鎖畫面顯示來電通知: @@ -903,25 +898,25 @@ 你錯過了多個訊息 實驗性功能 - 你的對話數據庫並未加密 - 設置密碼保護它。 - 數據庫密碼與儲存在金鑰庫中的密碼不同。 + 你的對話資料庫並未加密 - 設置密碼保護它。 + 資料庫密碼與儲存在金鑰庫中的密碼不同。 未知的錯誤 - 還原數據庫備份後請輸入舊密碼。這個操作是不能撤銷的! - 你可以透過 應用程式的設定或透過數據庫 去重新啟動應用程式來開啟對話。 + 還原資料庫備份後請輸入舊密碼。這個操作是不能撤銷的! + 你可以透過 應用程式的設定或透過資料庫 去重新啟動應用程式來開啟對話。 你已經被邀请加入至群組。加入後可與群組內的成員對話。 你已加入至群組 已確認聯絡人 - 你的對話數據庫 - 刪除對話資料? - 錯誤的數據庫密碼 - 未知的數據庫錯誤:%s + 你的對話資料庫 + 刪除聊天個人檔案? + 錯誤的資料庫密碼 + 未知的資料庫錯誤:%s 密碼錯誤! 你已經加入至群組。正在連接至群組內的成員。 這群組已經不存在。 更新群組檔案 你修改了 %s 的身份為 %s 連接中(介紹階段) - 使用對話 + 使用聊天 透過轉送 關閉視訊 你修改了自己的身份為 %s @@ -929,11 +924,11 @@ 由 %s 移除 將為所有成員刪除該訊息。 刪除成員訊息? - 主持 + 審查 該訊息將對所有成員標記為已移除。 - 你不能傳送訊息! - 你是觀察者 - 觀察者 + 你是觀察員 + 你是觀察員 + 觀察員 更新群組連接時出錯 請聯絡群組管理員。 初始角色 @@ -965,7 +960,7 @@ 儲存用戶密碼時出錯 更新用戶隱私時出錯 中繼伺服器僅在必要時使用。 另一方可以觀察到你的 IP 地址。 - 輸入密碼以搜尋! + 在搜尋中輸入密碼 群組歡迎訊息 隱藏的對話資料 不再顯示 @@ -980,35 +975,35 @@ 當靜音配置檔案處於活動狀態時,你仍會接收來自靜音配置檔案的通話和通知。 取消隱藏 影片將在你的聯絡人在線時接收,請你等等或者稍後再檢查! - 確認數據庫更新 - 數據庫版本不相容 - 數據庫降級 - 數據庫升級 + 確認資料庫更新 + 資料庫版本不相容 + 資料庫降級 + 資料庫升級 無效的遷移確認 - 數據庫現行版本比應用程式新,但是無法降級遷出:%s - 在應用程式/數據庫的不同遷移:%s/%s + 資料庫現行版本比應用程式新,但是無法降級遷出:%s + 在應用程式/資料庫的不同遷移:%s/%s 遷移:%s 警告:你可能會遺失一些數據! - 圖片將在你的聯絡人完成上傳後接收。 - 檔案將在你的聯絡人完成上傳後接收。 + 圖片將在你的聯絡人完成上載後接收。 + 檔案將在你的聯絡人完成上載後接收。 實驗性 升級和開啟對話 顯示開發者選項 - 刪除資料 + 刪除個人檔案 取消隱藏聊天資料 取消隱藏個人檔案 - 刪除對話資料 + 刪除聊天個人檔案 詢問以接收影片 同一時間只能傳送十段影片 太多影片! 影片 已傳送影片 等待影片中 - 影片將在你的聯絡人完成上傳後接收。 + 影片將在你的聯絡人完成上載後接收。 等待影片中 隱藏: 顯示: - 數據庫 IDs 和傳輸隔離選項。 + 資料庫 IDs 和傳輸隔離選項。 降級和開啟對話 個人檔案密碼 儲存 XFTP 伺服器時出錯 @@ -1016,7 +1011,7 @@ 加載 SMP 伺服器時出錯 加載 XFTP 伺服器時出錯 建立檔案 - 伺服器需要認證後才能上傳,檢查密碼 + 伺服器需要認證後才能上載,檢查密碼 對比檔案 刪除檔案 下載檔案 @@ -1060,7 +1055,7 @@ 停止 停止接收檔案? 錯誤的訊息 ID - 當你或你的連結在用舊的數據庫備份時會發生。 + 當你或你的連結在用舊的資料庫備份時會發生。 上一則訊息的雜奏則是不同的。 應用程式密碼 迅速以及不用等待發送者在線! @@ -1082,7 +1077,7 @@ " \n在 v5.1中可用" 允許你的聯絡人與你進行通話。 - 上傳檔案 + 上載檔案 XFTP 伺服器 系統認證 你未能通過認證;請再試一次。 @@ -1090,14 +1085,14 @@ 系統 此ID的下一則訊息是錯誤(小於或等於上一則的)。 \n當一些錯誤出現或你的連結被破壞時會發生。 - %1$d 訊息解密失敗。 + %1$d 條訊息解密失敗。 使用SOCKS 代理伺服器 你的 XFTP 伺服器 %1$d 條訊息已跳過。 影片和檔案和最大上限為1gb 影片 - 呈交 - 開啟數據庫中… + 提交 + 開啟資料庫中… 查看更多 SimpleX 聯絡地址 一次性連結 @@ -1130,15 +1125,15 @@ 已傳送訊息 標題 關於 SimpleX 的聯絡地址 - 額外的強調色 + 額外的輔色 外加的輔助 - 聯絡地址 - 後台 + 地址 + 背景 建立一個聯絡地址讓其他用戶與你連接。 自定義主題 黑暗主題 設定聯絡地址時出錯 - 開啟多個個人檔案 + 變更聊天個人檔案 內查看更多關於用戶指南的資料。]]> 自毁 啟用自毀密碼 @@ -1152,7 +1147,7 @@ 如果你在開啟應用程式時輸入此密碼,所有應用程式資料將被不可逆轉地刪除! 啟用自毀 應用程式密碼已替換為自毀密碼。 - 已建立一個具有所提供名稱的空白對話檔案,而應用程式將照常開啟。 + 已建立一個具有所提供名稱的空白聊天個人檔案,而應用程式將照常開啟。 已刪除所有的應用程式數據。 設定密碼 你可以與聯絡人分享此地址,讓他們使用 %s 進行連接。 @@ -1164,10 +1159,10 @@ 當有人向你發出連接請求,你可以接受或拒絕請求。 你的聯絡人會保持連接。 你的所有聯絡人會保持連接。更新了的個人檔案將傳送給你的聯絡人。 - 將聯絡地址新增到你的個人檔案中,以便你的聯絡人與其他人分享你的聯絡地址。更新了的個人檔案將傳送給你的聯絡人。 + 新增地址至你的個人檔案,以便你的聯絡人可以與其他人分享。更新了的個人檔案將傳送給你的聯絡人。 讓我們在 SimpleX Chat中聊天 確保檔案具有正確的 YAML 語法。匯出主題以獲得主題檔案結構的範例。 - 訊息互動 + 訊息反應 允許你的聯絡人新增訊息互動。 星期 @@ -1178,10 +1173,10 @@ 自定義時間 傳送 自動銷毀訊息 - 已移除在:%s + 已移除於:%s (目前的) - 允許訊息互動。 - 更佳的訊息 + 允許訊息反應。 + 更好的訊息 自定義和分享顏色主題 自定義主題 小時 @@ -1189,19 +1184,19 @@ 分鐘 個月 - 自定義 + 自訂 已接收在 已刪除在 - 已傳送在 - 數據庫 ID:%d + 已傳送於 + 資料庫 ID:%d 已移除在 - 1分鐘 - 30秒 - 5分鐘 - 已刪除在:%s + 1 分鐘 + 30 秒 + 5 分鐘 + 已刪除於:%s 禁止訊息互動。 - 群組內的成員可以新增訊息互動。 - 訊息互動於這個群組內是禁用的。 + 成員可以新增訊息反應。 + 訊息反應已停用。 加載詳細資料時出錯 傳送自動銷毀訊息 %s (目前的) @@ -1216,35 +1211,35 @@ 所有數據會在輸入後刪除。 沒有文字 紀錄已更新在 - 已傳送在:%s - 已接收在:%s + 已傳送於:%s + 已接收於:%s 紀錄已更新在:%s 只有你可以新增訊息互動。 銷毀於 - 訊息互動 + 訊息反應 感謝用戶-透過 Weblate 做出貢獻! 你和你的聯絡人可以新增訊息互動。 - 導入過程中出現了一些非致命錯誤 - 你可以到綜端機對話以獲取更多詳細資訊。 + 匯入過程中出現了一些非致命錯誤: 只有你的聯絡人允許的情況下,才允許訊息互動。 - 長達5分鐘的語音訊息。 \n- 自定義銷毀時間。 \n- 編輯紀錄。 搜尋 - 已關閉 + 關閉 確認來自未知伺服器的檔案。 超出額度 - 收件人未收到先前傳送的訊息 應用程式資料轉移 - 應用 - 請在轉移之前確認你還記得數據庫密碼 + 套用 + 請在轉移之前確認你還記得資料庫密碼 被管理員封鎖 進階設定 封鎖群組成員 活躍連接 中止 - 和其他 %d 事件 + 和其他 %d 個事件 封鎖成員? - 6種全新的介面語言 - 藍芽 + 6 種全新的介面語言 + 藍牙 %2$s 審核了 %1$d 條訊息 已封鎖 將停止地址更改。將使用舊聯絡地址。 @@ -1254,12 +1249,12 @@ 封鎖 應用程式主題 管理員 - 模糊以增強隱私 + 模糊以增強隱私。 所有成員 管理員可以為所有人封鎖一名成員 無法傳送訊息 為所有成員封鎖此成員? - + 黑色 中止更改地址? 無法傳送訊息給群組成員 所有顏色模式 @@ -1267,14 +1262,14 @@ 應用程式密碼 應用程式 聊天顏色 - 聊天已停止。如果你已經在另一台設備使用過此資料庫,你應該在啟動聊天前將數據庫傳輸回來。 + 聊天已停止。如果你已經在另一台設備使用過此資料庫,你應該在啟動聊天前將資料庫傳輸回來。 即將推出! 軟體更新以下載 儲存聯絡人以便稍後聊天 相機 選擇一個檔案 - 存檔並上傳 - 你的所有聯絡人、對話和檔案將被安全加密並切塊上傳到你設定的 XFTP 中繼 + 封存並上載 + 你的所有聯絡人、對話和檔案將被安全加密並切塊上載到你設定的 XFTP 中繼 正在儲存資料庫 取消遷移 與 %s 協調加密中… @@ -1284,12 +1279,12 @@ 所有訊息都將被刪除 - 這無法復原 請注意:訊息和檔案中繼通過 SOCKS 代理連接。通話和傳送連預覽使用直接連接。]]> 封鎖全部 - 改進群組功能 + 更好的群組 行動網路 封鎖成員 - 警告:此存檔將被刪除。]]> + 警告:此封存將被刪除。]]> 清除私密筆記? - 添加聯絡人 + 新增聯絡人 總是 協調加密中… 允許傳送檔案和媒體 @@ -1303,23 +1298,23 @@ 作者 已封鎖 被管理員封鎖 - 額外的強調色2 + 額外的輔色 2 阿拉伯語、保加利亞語、芬蘭語、希伯來語、泰國語和烏克蘭語——感謝使用者們與Weblate 已加入群組! 確認網路設定 嘗試 已確認 確認錯誤 - 完成 + 已完成 區塊已刪除 - 區塊已上傳 + 區塊已上載 區塊已下載 添加聯絡人: 來創建新的邀請連結,或通過你收到的連結進行連接。]]> 建立群組: 建立新的群組。]]> 錯誤的桌面地址 已轉移聊天 從另一部設備轉移 並掃描QR code。]]> - 請注意: 作為安全保護措施,在兩部設備上使用同一數據庫會破壞解密來自你聯絡人的訊息。]]> + 請注意: 作為安全保護措施,在兩部設備上使用同一資料庫會破壞解密來自你聯絡人的訊息。]]> 確定刪除聯絡人? 檢查更新 無法與聯絡人通話 @@ -1327,7 +1322,7 @@ 無法與群組成員通話 應用程式將為新的本機檔案(影片除外)加密。 檢查你的網路連接並重試 - 所有配置文件 + 所有個人檔案 已設定的 SMP 伺服器 聊天主題 通話 @@ -1343,23 +1338,23 @@ Webview 初始化失敗。更新你的系統到新版本。請聯繫開發者。 \n錯誤:%s 已刪除聯絡人 - %d 個群事件 + %d 個群組事件 訊息太大 訊息傳送警告 錯誤:%1$s 開發者選項 與 %s 的加密需要重協商 %s 不活躍]]> - 最喜歡 + 最愛 訊息成功送達! 檔案和媒體 連線停止 %s的連接不穩定]]> 聯絡人 適合 - 群組成員可以傳送檔案和媒體。 - 連結行動裝置 - 此群組禁止檔案和媒體 + 成員可以傳送檔案和媒體。 + 已連結行動裝置 + 檔案和媒體已停用。 結束通話 刪除 %d 條訊息嗎? 訊息草稿 @@ -1373,11 +1368,11 @@ 啟用(保留組覆蓋) 淺色 淺色模式 - 群組成員可傳送 SimpleX 連結。 + 成員可以傳送 SimpleX 連結。 深色 詳情 訊息接收 - 無效連結 + 無效的連結 %d 條訊息被標記為刪除 %d 條訊息已攔截 轉發伺服器地址不相容網路設定:%1$s。 @@ -1391,7 +1386,7 @@ 如果成員變得活躍,可能會在之後傳送訊息。 刪除了聯絡人! 聯絡人將被刪除 - 無法復原此操作 - + 已停用 安裝成功 建立 @@ -1409,7 +1404,7 @@ 新的聊天主題 連線和伺服器狀態 控制你的網路 - 從GitHub下載最新版本。 + 從 GitHub 下載最新版本。 啟用 新的聊天體驗 🎉 新的媒體選項 @@ -1418,11 +1413,11 @@ 連接到桌面 連線終止 連接到桌面 - 上傳存檔出錯 - 文件被刪除或鏈接無效 - 導入失敗 - 頭戴式耳機 - 耳機 + 上載封存出錯 + 檔案已被刪除或連結無效 + 匯入失敗 + 耳機 + 聽筒 對所有聯絡人關閉 深色模式 啟用已讀回條時出錯! @@ -1439,7 +1434,7 @@ 檔案錯誤 備用訊息路由 如果你或你的目標伺服器不支持私密路由,將不直接傳送訊息。 - 建立個人資料 + 建立個人檔案 從另一台裝置轉移 成員姓名從 %1$s 改為了 %2$s 同意加密 @@ -1450,7 +1445,7 @@ \n- 還有更多! 匈牙利語和土耳其語用戶界面 轉移完成 - 從此裝置刪除數據庫 + 從此裝置刪除資料庫 傳送 下載 轉發 @@ -1460,7 +1455,7 @@ 轉發訊息… 建立連結中… 保留 - 聯絡人姓名從 %1$s 改為了 %2$s + 聯絡人名稱從 %1$s 變更為 %2$s 新訊息 邀請 停用(保留覆蓋) @@ -1487,7 +1482,7 @@ 轉發伺服器:%1$s \n錯誤:%2$s 轉發伺服器 %1$s 連結目標伺服器 %2$s 失敗。請稍後嘗試。 - 導入存檔中 + 正在匯入封存 改進訊息傳送 安裝更新 它保護你的 IP 位址和連線。 @@ -1500,8 +1495,8 @@ 保存設定出錯 導出的檔案不存在 導出資料庫時出錯 - 確認上傳 - 正在建立存檔連結 + 確認上載 + 正在建立封存連結 加密OK 將顯示來自 %s 的訊息! 送達回執! @@ -1512,12 +1507,12 @@ 加密重協商失敗 將更新資料庫密碼並儲存在設定中。 使用隨機身分建立群組 - 加入速度更快、訊息更可靠。 + 更快的加入速度,訊息更可靠。 匿名群組 - 連接行動裝置 + 連結行動裝置 回復 和 %1$s 連接? - 在桌面應用裡建立新的帳號。💻 + 在桌面應用程式建立新的個人檔案。💻 輸入此裝置名稱… 已連結到行動裝置 可通過局域網發現 @@ -1525,10 +1520,10 @@ 嚴重錯誤 內部錯誤 驗證密碼短語出錯: - 顯示名稱無效! + 無效的顯示名稱! 無效的檔案路徑 過濾未讀和收藏的聊天記錄。 - 斷開連結 + 斷開連接 斷開桌面連結? 中止地址更改時出錯 顯示通知出錯,請聯繫開發者。 @@ -1550,12 +1545,12 @@ 已關閉送達回執! %s 斷開連接]]> %s 未找到]]> - 存檔下載中 + 封存下載中 轉移中 下載失敗 為所有組啟用 需要重協商加密 - 關閉 + 已停用 修復連結 修復聯絡人不支援的問題 修復 @@ -1571,14 +1566,14 @@ 顯示內容出錯 顯示訊息出錯 錯誤 - 無後台通話 - 建立聊天資料 + 無背景通話 + 建立聊天個人檔案 已同意 %s 的加密 允許重新協商與 %s 的加密 與 %s 的加密OK 桌面設備 - 連接桌面選項 - 連接桌面 + 已連結桌面選項 + 已連結桌面 直接連線中 邀請 建立群組 @@ -1621,27 +1616,27 @@ 已下載的檔案 下載出錯 功能執行所花費的時間過長:%1$d 秒:%2$s - 無效名稱 + 無效的名稱! 正確名字為 %s? 複製錯誤 正在連接到桌面 找到桌面 自動連接 與PC版的連接不穩定 - 桌面 + 桌上電腦 已安裝的PC版本不支援。請確認兩台裝置所安裝的版本相同 PC版邀請碼錯誤 通過連結連接? 加入你的群組嗎? 轉移到此處 - 無效連結 + 無效的連結 在另一部設備上完成轉移 - 下載存檔錯誤 + 下載封存錯誤 轉移到另一部裝置 必須停止聊天才能繼續。 保留對話 不通知刪除 - 無效的QR code + 無效的 QR 圖碼 保留未使用的邀請嗎? 停用回執? 啟用回執? @@ -1651,7 +1646,7 @@ 為儲存的檔案和媒體加密 連接到你自己? 完成轉移 - 目前配置文件 + 目前個人檔案 檔案 重連伺服器出錯 重連伺服器出錯 @@ -1662,14 +1657,14 @@ 資料庫將被加密,密碼將儲存在設定中 刪除成員的 %d 條訊息嗎? 成員非活躍 - 輸入訊息 + 訊息 訊息將被標記為刪除。收信人可以揭示這些訊息。 連接 訊息 停用 下載更新中,請不要關閉應用 下載 %s(%s) - 從不 + 永不 中等 媒體和檔案伺服器 訊息伺服器 @@ -1684,7 +1679,7 @@ 為群組停用回執? 過往的成員 %1$s 私密訊息路由 🚀 - 貼上存檔連結 + 貼上封存連結 從桌面使用並掃描QR code。]]> 無傳送資訊 或者顯示此碼 @@ -1694,11 +1689,11 @@ 請稍後再試。 沒有過濾的聯絡人 打開檔案位置 - 打開 + 開啟 私密筆記 波斯語用戶界面 從聊天列表播放。 - 正在準備上傳 + 正在準備上載 擁有者 不相容! 請將它報告給開發者: @@ -1708,21 +1703,21 @@ 從已連接行動裝置加載檔案時請稍候片刻 或者掃描QR code 禁止傳送 SimpleX 連結 - 沒有選擇聊天 + 沒有已選擇的聊天 打開設定 這人資料主題 個人資料圖片 同一時刻只有一台裝置可工作 保護 IP 地址 其他 - 無網路連接 + 沒有網路連接 無歷史記錄 無過濾聊天 請將它報告給開發者: \n%s 打開應用程式設定 開啟轉移畫面 - 通知將停止,直到您重啟應用程式 + 通知將停止,直到你重啟應用程式 禁止傳送檔案和媒體。 打開群組 只有群組所有者才能啟用檔案和媒體。 @@ -1740,7 +1735,7 @@ 請檢查行動裝置和桌面設備連接到的是同一個本地網絡,且桌面防火牆允許連接。 \n請和開發者分享任何其他問題。 在防火牆中打開端口 - 或貼上存檔連結 + 或貼上封存連結 正在準備下載 只刪除對話 貼上連結 @@ -1754,7 +1749,7 @@ 私密路由出錯 尚無直接連接,訊息由管理員轉發。 什麼也沒選中 - 打開 + 開啟 私密路由 打開資料庫文件夾 私密訊息路由 @@ -1766,28 +1761,371 @@ 接收到的訊息 接收錯誤 重新連接所有已連接的伺服器來強制傳送訊息。這會使用額外流量。 - 重連伺服器強制傳送訊息。這會使用額外流量。 + 重新連接伺服器以強制傳送訊息。這會使用額外流量。 並行接收 收件人看不到這條訊息來自誰。 刪除了資料圖片 - 可使用的聊天工具箱 - 可存取的聊天工具欄 + 單手模式的應用程式工具列 + 單手模式的聊天工具列 最近歷史和改進的目錄機器人。 每 KB 協議超時 - 保護您的真實 IP 地址。不讓你聯絡人選擇的訊息中繼看到它。 -\n在*網絡&伺服器*設定中開啓。 - 隨機密碼以明文形式儲存在設定中。 -\n您可以稍後更改。 + 保護你的真實 IP 地址,以不讓你的聯絡人所選擇的訊息中繼看到它。 \n在*網路和伺服器*設定中啟用。 + 隨機密碼以明文形式儲存在設定中。 \n你可以稍後更改。 傳送回條已禁用 代理伺服器 - 重連伺服器? + 重新連接伺服器? 抗量子端到端加密 收到的回覆 - 重連所有伺服器 - 重連 + 重新連接所有伺服器 + 重新連接 代理 隨機 - 更新 + 重新整理 接收總計 稍後提醒 - \ No newline at end of file + 新增朋友 + 營運者伺服器 + 關閉? + 此裝置 + 審核員 + %1$d 個檔案下載失敗。 + 聊天已存在! + 沒有訊息伺服器。 + %1$d 個檔案錯誤:\n%2$s + 設定聊天名稱… + 靜音全部 + 刪除 + + + 關閉 + 此裝置名稱 + 關於營運者 + 預設(%s) + 聯絡人 + 你的聯絡人 + 聊天資料庫 + 詢問 + + 重設顏色 + 已傳送回覆 + 審核員 + 拒絕 + 重設 + 已傳送總數 + 清單 + 設定 + 清單名稱… + 單手模式的聊天工具列 + %1$d 個檔案仍然正在下載。 + %1$s 條訊息未被轉發 + %1$d 個檔案已刪除。 + 下載 + 筆記 + 尾部 + 重新啟動 + 應用程式工具列 + 伺服器地址 + 營運者 + 你已退出 + 顯示百分比 + 伺服器 + 接受 + + %s:%s + 聊天 + 你的伺服器 + 縮放 + 模糊 + %1$d 個檔案未被下載。 + 1 年 + 接受 + 已訂閱 + SimpleX 頻道連結 + %s、%s,和 %d 名成員 + 待處理 + 解除連結 + 最愛 + 給所有審核員 + 舉報 + 濫發 + 搜尋 + 影片 + 取消最愛 + + 喇叭 + 解除封鎖 + 網站 + 透明度 + 重複 + 編輯 + 重新同步 + 選擇 + 使用者名稱 + 密碼 + 更新 + 繼續 + 已拒絕 + 系統 + 關閉 + 全部 + 已受保護 + 全部 + 商業 + 角落 + 群組 + 報告 + 統計 + 比例 + 重試 + 穩定 + 審核 + 未知 + 關閉 + 已儲存 + 封存 + 已拒絕 + 大小 + WiFi + 修復 + 已儲存 + 濫發 + 已上載 + 未受保護 + 沒有私密訊息路由伺服器。 + 沒有訊息接收伺服器。 + 沒有媒體和檔案伺服器。 + 其他 %1$d 個檔案錯誤。 + 已新增媒體和檔案伺服器 + %s 已封存報告 + 沒有檔案傳送伺服器。 + 沒有檔案接收伺服器。 + 封存報告 + 此聊天受到抗量子端對端加密保護。 + 使用目前個人檔案 + 使用新的匿名個人檔案 + 更好的通話 + 地址設定 + 已新增訊息伺服器 + 你分享了一個無效的檔案路徑。請將此問題報告給應用程式開發者。 + 如果沒有 Tor 或 VPN,你的 IP 位址將對以下 XFTP 中繼可見:\n%1$s。 + 檢視已崩潰 + 新增短連結 + 接受了 %1$s + 接受了你 + 新增團隊成員 + 接受成員 + 新增清單 + SimpleX 無法在背景執行。你只有在應用程式執行時才會收到通知。 + 應用程式工作階段 + 已接受邀請 + 此顯示名稱無效。請選擇另一個名稱。 + 新增至清單 + 地址還是一次性連結? + 應用程式總是在背景執行 + 已接受的條款 + 接受為成員 + 接受為觀察員 + 全部伺服器 + 允許向審核員舉報訊息。 + 封存全部報告? + 封存 %d 份報告? + 封存報告? + 封存報告 + 1 個舉報 + 無背景服務 + 短連結 + 已封存的報告 + 只有你和審核員能夠檢視 + 只有傳送者和審核員能夠檢視 + 已儲存自 %s + 此聊天受到端對端加密保護。 + 另一個原因 + 不當的個人檔案 + 不當的內容 + 違反社群準則 + 接受條款 + 重複連接請求? + 移除封存? + 已直接傳送 + 傳送錯誤 + 連接安全性 + 變更排序 + 訊息形狀 + 驗證連接 + 視訊通話 + 刪除舉報 + 使用 %s + 重新同步加密? + 已儲存自 + 連接被封鎖 + 建立清單 + 轉發訊息… + 建立一次性連結 + 網路營運者 + 解除連結桌上電腦? + 重複上載 + 設定資料庫密碼 + %s 已上載 + 已更新個人檔案 + 開啟連結 + 設定資料庫密碼 + 工作階段代碼 + 已傳送訊息 + 正在停止聊天 + 伺服器資訊 + %d 條訊息 + %d 個聊天 + 設定密碼短語 + 待審核 + 未知伺服器! + 伺服器營運者 + 已解除封鎖 %s + 安全碼已變更 + 未知狀態 + 傳送回條 + 移除成員? + 重新同步加密 + 使用伺服器 + TCP 連接 + 用於傳送 + SimpleX 連結 + 審核成員 + 刪除聊天 + 拒絕成員? + 重新啟動聊天 + %s 已下載 + 重複下載 + 重複匯入 + 上載失敗 + 驗證密碼短語 + 已選取 %d + SOCKS 代理 + 解除封鎖成員 + 使用隨機密碼 + %s 已連接 + 已儲存訊息 + 更安全的群組 + XFTP 伺服器 + 變更清單 + 新伺服器 + 儲存清單 + 刪除清單? + 解除封鎖成員? + 網路營運者 + 系統模式 + 驗證連接 + 開始聊天? + %s 在 %s + 移除成員? + 移除成員 + 移除圖片 + 傳送私人訊息 + 聯絡人已刪除 + 未同步 + 聯絡人已停用 + 稍後審核 + 分享個人檔案 + 完整連結 + 已上載檔案 + 上載錯誤 + 正在上載封存 + 開始聊天 + SMP 伺服器 + 傳輸工作階段 + 修復連接? + 總計 + 未知伺服器 + 舉報理由? + %s 和 %s + 減少電量使用。 + 你的個人檔案 %1$s 將會被分享。 + 你無法傳送訊息! + 減少電量使用。 + 你已解除封鎖 %s + 你已接受此成員 + 停用刪除訊息 + 商業地址 + 變更自動燒毀訊息? + 與成員聊天 + 與管理員們聊天 + 刪除聊天 + 刪除聊天? + 與成員們聊天 + 商業聊天 + 更好的安全性 ✅ + 與管理員們聊天 + 無法傳送訊息 + 可自訂訊息形狀。 + 更好的使用體驗 + 內容違反使用條款 + 停用自動刪除訊息? + 刪除或審查最多 200 條訊息。 + 於網路和伺服器設定中啟用 Flux 以獲得更好的元資料隱私。 + 配置伺服器營運者 + 使用條款 + 更好的隱私和安全性 + 你可以再試一次。 + 每 10 分鐘檢查訊息 + 聯絡人未就緒 + 你可以在外觀設定中變更它。 + 從你的裝置刪除聊天訊息。 + 更好的群組性能 + 你已封鎖 %s + 連接未就緒。 + 更好的訊息日期。 + 為了更好的元資料隱私。 + 你可以再試一次。 + 使用匿名個人檔案 + 開啟聊天 + 連接 + 升級 + 群組 + 可見的紀錄 + 慢速函數 + 有線以太網 + 桌布背景 + 桌布輔色 + 訂閱已忽略 + 訂閱錯誤 + 代理身份驗證 + 已靜音 + 未送達的訊息 + 沒有訊息 + 預設伺服器 + 審閱使用條款 + %s 個伺服器 + 檢視使用條款 + 接收 + 開啟使用條款 + 開啟變更 + 網路去中心化 + 離開聊天? + 離開聊天 + 啟用日誌 + 沒有聊天 + 成員舉報 + 僅自己 + 舉報:%s + 待批准 + 已更新使用條款 + 預設伺服器 + 加入群組 + 增加訊息 + 傳送請求 + 你的個人資料 + 歡迎訊息 + 升級地址? + 正在載入個人資料… + 你的簡介: + 簡短描述: + 你的聯絡人 + 你的群組 + 商業連接 + 顯示最近的訊息 + 簡化的匿名模式 + 點擊以連接 + 從桌面使用 + 開啓新聊天 + 接受聯絡請求 + 接受聯絡請求 + diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 9d747206ab..136a883035 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -14,8 +14,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import chat.simplex.common.model.* -import chat.simplex.common.model.ChatModel.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.SimpleXTheme @@ -58,12 +56,12 @@ fun showApp() { } else { // The last possible cause that can be closed withApi { - withChats { + withContext(Dispatchers.Main) { chatModel.chatId.value = null - chatItems.clearAndNotify() + chatModel.chatsContext.chatItems.clearAndNotify() } - withReportsChatsIfOpen { - chatItems.clearAndNotify() + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value = null } } } @@ -173,7 +171,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { // Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires LaunchedEffect(toast, toasts.size) { delay(toast.second) - simplexWindowState.toasts.removeFirst() + simplexWindowState.toasts.removeFirstOrNull() } } var windowFocused by remember { simplexWindowState.windowFocused } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index 3127a31d38..cbcb32f353 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -135,9 +135,9 @@ actual fun ImageBitmap.hasAlpha(): Boolean { return false } -actual fun ImageBitmap.addLogo(): ImageBitmap { - val radius = (width * 0.16f).toInt() - val logoSize = (width * 0.24).toInt() +actual fun ImageBitmap.addLogo(size: Float): ImageBitmap { + val radius = (width * size).toInt() + val logoSize = (width * size * 1.5).toInt() val logo: BufferedImage = MR.images.icon_foreground_common.image val original = toAwtImage() val withLogo = BufferedImage(width, height, original.type) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index e7bcf4802a..41964b7d18 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -10,8 +10,7 @@ import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.key.* @@ -45,25 +44,27 @@ import kotlin.text.substring actual fun PlatformTextField( composeState: MutableState, sendMsgEnabled: Boolean, + disabledText: String?, sendMsgButtonDisabled: Boolean, textStyle: MutableState, showDeleteTextButton: MutableState, - userIsObserver: Boolean, placeholder: String, showVoiceButton: Boolean, - onMessageChange: (String) -> Unit, + onMessageChange: (ComposeMessage) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List) -> Unit, + focusRequester: FocusRequester?, onDone: () -> Unit, ) { + val cs = composeState.value - val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val keyboard = LocalSoftwareKeyboardController.current + val focusReq = focusRequester ?: remember { FocusRequester() } LaunchedEffect(cs.contextItem) { if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect // In replying state - focusRequester.requestFocus() + focusReq.requestFocus() delay(50) keyboard?.show() } @@ -74,9 +75,9 @@ actual fun PlatformTextField( keyboard?.hide() } } - val lastTimeWasRtlByCharacters = remember { mutableStateOf(isRtl(cs.message.subSequence(0, min(50, cs.message.length)))) } + val lastTimeWasRtlByCharacters = remember { mutableStateOf(isRtl(cs.message.text.subSequence(0, min(50, cs.message.text.length)))) } val isRtlByCharacters = remember(cs.message) { - if (cs.message.isNotEmpty()) isRtl(cs.message.subSequence(0, min(50, cs.message.length))) else lastTimeWasRtlByCharacters.value + if (cs.message.text.isNotEmpty()) isRtl(cs.message.text.subSequence(0, min(50, cs.message.text.length))) else lastTimeWasRtlByCharacters.value } LaunchedEffect(isRtlByCharacters) { lastTimeWasRtlByCharacters.value = isRtlByCharacters @@ -84,12 +85,12 @@ actual fun PlatformTextField( val isLtrGlobally = LocalLayoutDirection.current == LayoutDirection.Ltr // Different padding here is for a text that is considered RTL with non-RTL locale set globally. // In this case padding from right side should be bigger - val startEndPadding = if (cs.message.isEmpty() && showVoiceButton && isRtlByCharacters && isLtrGlobally) 95.dp else 50.dp + val startEndPadding = if (cs.message.text.isEmpty() && showVoiceButton && isRtlByCharacters && isLtrGlobally) 95.dp else 50.dp val startPadding = if (isRtlByCharacters && isLtrGlobally) startEndPadding else 0.dp val endPadding = if (isRtlByCharacters && isLtrGlobally) 0.dp else startEndPadding val padding = PaddingValues(startPadding, 12.dp, endPadding, 0.dp) - var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) } - val textFieldValue = textFieldValueState.copy(text = cs.message) + var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message.text, selection = cs.message.selection)) } + val textFieldValue = textFieldValueState.copy(text = cs.message.text, selection = cs.message.selection) val clipboard = LocalClipboardManager.current BasicTextField( value = textFieldValue, @@ -105,7 +106,7 @@ actual fun PlatformTextField( } } textFieldValueState = it - onMessageChange(it.text) + onMessageChange(ComposeMessage(it.text, it.selection)) } }, textStyle = textStyle.value, @@ -118,7 +119,7 @@ actual fun PlatformTextField( .padding(start = startPadding, end = endPadding) .offset(y = (-5).dp) .fillMaxWidth() - .focusRequester(focusRequester) + .focusRequester(focusReq) .onPreviewKeyEvent { if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyDown) { if (it.isShiftPressed) { @@ -129,12 +130,12 @@ actual fun PlatformTextField( text = newText, selection = TextRange(textFieldValue.selection.min + 1) ) - onMessageChange(newText) + onMessageChange(ComposeMessage(newText, textFieldValueState.selection)) } else if (!sendMsgButtonDisabled) { onDone() } true - } else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.isEmpty()) { + } else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.text.isEmpty()) { onUpArrow() true } else if (it.key == Key.V && @@ -166,7 +167,7 @@ actual fun PlatformTextField( chatModel.filesToDelete.add(tempFile) tempFile.writeBytes(bytes) - composeState.processPickedMedia(listOf(tempFile.toURI()), composeState.value.message) + composeState.processPickedMedia(listOf(tempFile.toURI()), composeState.value.message.text) } } catch (e: Exception) { Log.e(TAG, "Pasting image exception: ${e.stackTraceToString()}") @@ -200,18 +201,18 @@ actual fun PlatformTextField( } } ) - showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress + showDeleteTextButton.value = cs.message.text.split("\n").size >= 4 && !cs.inProgress if (composeState.value.preview is ComposePreview.VoicePreview) { - ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding) - } else if (userIsObserver) { - ComposeOverlay(MR.strings.you_are_observer, textStyle, padding) + ComposeOverlay(generalGetString(MR.strings.voice_message_send_text), textStyle, padding) + } else if (disabledText != null) { + ComposeOverlay(disabledText, textStyle, padding) } } @Composable -private fun ComposeOverlay(textId: StringResource, textStyle: MutableState, 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/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index 3f5703365d..696e0efde8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -93,7 +93,7 @@ actual fun LazyColumnWithScrollBar( } val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier Box(Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).nestedScroll(connection)) { - LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content = content) ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar) } } @@ -111,7 +111,9 @@ actual fun LazyColumnWithScrollBarNoAppBar( additionalBarOffset: State?, additionalTopBar: State, chatBottomBar: State, - content: LazyListScope.() -> Unit + maxHeight: State?, + containerAlignment: Alignment, + content: LazyListScope.() -> Unit, ) { val scope = rememberCoroutineScope() val scrollBarAlpha = remember { Animatable(0f) } @@ -135,9 +137,11 @@ actual fun LazyColumnWithScrollBarNoAppBar( // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state // (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row) val scrollBarDraggingState = remember { mutableStateOf(false) } - Box { - LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar) + Box(contentAlignment = containerAlignment) { + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content = content) + Box(if (maxHeight?.value != null) Modifier.height(maxHeight.value).fillMaxWidth() else Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) + } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/SimplexService.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/SimplexService.desktop.kt new file mode 100644 index 0000000000..d29871f842 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/SimplexService.desktop.kt @@ -0,0 +1,3 @@ +package chat.simplex.common.platform + +actual fun getWakeLock(timeout: Long): (() -> Unit) = {} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index e3b0642547..9be10a584b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -195,9 +195,11 @@ fun WebRTCController(callCommand: SnapshotStateList, onResponse: ( delay(100) } while (callCommand.isNotEmpty()) { - val cmd = callCommand.removeFirst() + val cmd = callCommand.removeFirstOrNull() Log.d(TAG, "WebRTCController LaunchedEffect executing $cmd") - processCommand(cmd) + if (cmd != null) { + processCommand(cmd) + } } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt index cd206c8e4e..e6f782d816 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt @@ -32,7 +32,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) { @Composable actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState) { - ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { + ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = { val saveIfExists = { when (cItem.content.msgContent) { is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withLongRunningApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt index e295144191..9fd65ec995 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt @@ -51,7 +51,7 @@ private fun ActiveCallInteractiveAreaOneHand(call: Call, showMenu: MutableState< val chat = chatModel.getChat(call.contact.id) if (chat != null) { withBGApi { - openChat(chat.remoteHostId, chat.chatInfo) + openChat(secondaryChatsCtx = null, chat.remoteHostId, chat.chatInfo) } } }, @@ -116,7 +116,7 @@ private fun ActiveCallInteractiveAreaNonOneHand(call: Call, showMenu: MutableSta val chat = chatModel.getChat(call.contact.id) if (chat != null) { withBGApi { - openChat(chat.remoteHostId, chat.chatInfo) + openChat(secondaryChatsCtx = null, chat.remoteHostId, chat.chatInfo) } } }, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt index 3855835ab6..52e845b422 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt @@ -63,7 +63,7 @@ actual fun UserPickerUsersSection( ProfileImage(size = 55.dp, image = user.profile.image, color = iconColor) if (u.unreadCount > 0 && !user.activeUser) { - unreadBadge(u.unreadCount, user.showNtfs, true) + userUnreadBadge(u.unreadCount, user.showNtfs, true) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt index 03c8e51c55..f1103bc516 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt @@ -31,6 +31,7 @@ actual fun CustomTimePicker( mutableStateOf(res) } val values = remember(unit.value) { + // TODO replace with firstOrNull val limit = timeUnitsLimits.first { it.timeUnit == unit.value } val res = ArrayList>() for (i in limit.minValue..limit.maxValue) { diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index e39ba48a0b..60ff535e88 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -1,9 +1,10 @@ +import org.gradle.internal.extensions.stdlib.toDefaultLowerCase import org.jetbrains.compose.desktop.application.dsl.TargetFormat -import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly plugins { kotlin("multiplatform") id("org.jetbrains.compose") + id("org.jetbrains.kotlin.plugin.compose") id("io.github.tomtzook.gradle-cmake") version "1.2.2" } @@ -89,7 +90,7 @@ compose { } } } - val os = System.getProperty("os.name", "generic").toLowerCaseAsciiOnly() + val os = System.getProperty("os.name", "generic").toDefaultLowerCase() if (os.contains("mac") || os.contains("win")) { packageName = "SimpleX" } else { diff --git a/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml index 9f9b2d6d20..2254c6d5bf 100644 --- a/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml +++ b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml @@ -52,7 +52,7 @@ https://github.com/simplex-chat/simplex-chat/issues https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat https://simplex.chat/connect-team - https://github.com/simplex-chat/simplex-chat#help-us-with-donations + https://github.com/simplex-chat/simplex-chat#please-support-us-with-your-donations diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 6c04bf65d7..1501cb43dc 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,12 +24,17 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.3-beta.1 -android.version_code=270 +android.version_name=6.5-beta.2 +android.version_code=330 -desktop.version_name=6.3-beta.1 -desktop.version_code=88 +android.bundle=false -kotlin.version=1.9.23 -gradle.plugin.version=8.2.0 -compose.version=1.7.0 +desktop.version_name=6.5-beta.2 +desktop.version_code=127 + +kotlin.version=2.1.20 +gradle.plugin.version=8.7.0 +compose.version=1.8.2 + +# Choose sqlite or postgres backend +database.backend=sqlite diff --git a/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties b/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties index 4e4a6a3f29..6183001f7b 100644 --- a/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties +++ b/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 14 14:23:51 GMT 2022 +#Fri Mar 21 20:38:56 ICT 2025 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/apps/multiplatform/settings.gradle.kts b/apps/multiplatform/settings.gradle.kts index ba047edf1e..40446f1958 100644 --- a/apps/multiplatform/settings.gradle.kts +++ b/apps/multiplatform/settings.gradle.kts @@ -12,6 +12,7 @@ pluginManagement { id("com.android.application").version(extra["gradle.plugin.version"] as String) id("com.android.library").version(extra["gradle.plugin.version"] as String) id("org.jetbrains.compose").version(extra["compose.version"] as String) + id("org.jetbrains.kotlin.plugin.compose").version(extra["kotlin.version"] as String) id("org.jetbrains.kotlin.plugin.serialization").version(extra["kotlin.version"] as String) } } diff --git a/apps/simplex-bot-advanced/Main.hs b/apps/simplex-bot-advanced/Main.hs index 6c3d8240e4..40e6686065 100644 --- a/apps/simplex-bot-advanced/Main.hs +++ b/apps/simplex-bot-advanced/Main.hs @@ -43,12 +43,12 @@ mySquaringBot :: User -> ChatController -> IO () mySquaringBot _user cc = do initializeBotAddress cc race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - case resp of - CRContactConnected _ contact _ -> do + (_, evt) <- atomically . readTBQueue $ outputQ cc + case evt of + Right (CEvtContactConnected _ contact _) -> do contactConnected contact sendMessage cc contact welcomeMessage - CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do + Right CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do let msg = ciContentToText mc number_ = readMaybe (T.unpack msg) :: Maybe Integer sendMessage cc contact $ case number_ of diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs index 9dc927af9e..86f89f86e8 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs @@ -2,6 +2,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} module Broadcast.Bot where @@ -36,38 +37,31 @@ broadcastBot :: BroadcastBotOpts -> User -> ChatController -> IO () broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _user cc = do initializeBotAddress cc race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - case resp of - CRContactConnected _ ct _ -> do + (_, evt) <- atomically . readTBQueue $ outputQ cc + case evt of + Right (CEvtContactConnected _ ct _) -> do contactConnected ct sendMessage cc ct welcomeMessage - CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) : _} - | publisher `elem` publishers -> - if allowContent mc - then do - sendChatCmd cc ListContacts >>= \case - CRContactsList _ cts -> void . forkIO $ do - let cts' = filter broadcastTo cts - forM_ cts' $ \ct' -> sendComposedMessage cc ct' Nothing mc - sendReply $ "Forwarded to " <> tshow (length cts') <> " contact(s)" - r -> putStrLn $ "Error getting contacts list: " <> show r - else sendReply "!1 Message is not supported!" - | otherwise -> do - sendReply prohibitedMessage - deleteMessage cc ct $ chatItemId' ci + Right CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) : _} + | sender `notElem` publishers -> do + sendReply prohibitedMessage + deleteMessage cc ct $ chatItemId' ci + | allowContent mc -> + void $ forkIO $ + sendChatCmd cc (SendMessageBroadcast mc) >>= \case + Right CRBroadcastSent {successes, failures} -> + sendReply $ "Forwarded to " <> tshow successes <> " contact(s), " <> tshow failures <> " errors" + r -> putStrLn $ "Error broadcasting message: " <> show r + | otherwise -> + sendReply "!1 Message is not supported!" where sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . MCText - publisher = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} + sender = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} allowContent = \case MCText _ -> True MCLink {} -> True MCImage {} -> True _ -> False - broadcastTo Contact {activeConn = Nothing} = False - broadcastTo ct'@Contact {activeConn = Just conn@Connection {connStatus}} = - (connStatus == ConnSndReady || connStatus == ConnReady) - && not (connDisabled conn) - && contactId' ct' /= contactId' ct _ -> pure () where contactConnected ct = putStrLn $ T.unpack (localDisplayName' ct) <> " connected" diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index e695b5069d..d9f091a13a 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -11,10 +11,11 @@ import Data.Text (Text) import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) -import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) +import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP) data BroadcastBotOpts = BroadcastBotOpts { coreOptions :: CoreChatOpts, + botDisplayName :: Text, publishers :: [KnownContact], welcomeMessage :: Text, prohibitedMessage :: Text @@ -29,6 +30,12 @@ defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts broadcastBotOpts appDir defaultDbName = do coreOptions <- coreChatOptsP appDir defaultDbName + botDisplayName <- + strOption + ( long "display-name" + <> metavar "DISPLAY_NAME" + <> help "The display name of the broadcast bot" + ) publishers <- option parseKnownContacts @@ -55,6 +62,7 @@ broadcastBotOpts appDir defaultDbName = do pure BroadcastBotOpts { coreOptions, + botDisplayName, publishers, welcomeMessage = fromMaybe (defaultWelcomeMessage publishers) welcomeMessage_, prohibitedMessage = fromMaybe (defaultProhibitedMessage publishers) prohibitedMessage_ @@ -72,10 +80,9 @@ getBroadcastBotOpts appDir defaultDbName = versionAndUpdate = versionStr <> "\n" <> updateStr mkChatOpts :: BroadcastBotOpts -> ChatOpts -mkChatOpts BroadcastBotOpts {coreOptions} = +mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} = ChatOpts { coreOptions, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatCmdLog = CCLNone, @@ -87,5 +94,6 @@ mkChatOpts BroadcastBotOpts {coreOptions} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, + createBot = Just CreateBotOpts {botDisplayName, allowFiles = False}, maintenance = False } diff --git a/apps/simplex-chat/Server.hs b/apps/simplex-chat/Server.hs index fddad1cf2c..b84bf8b579 100644 --- a/apps/simplex-chat/Server.hs +++ b/apps/simplex-chat/Server.hs @@ -2,17 +2,21 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} module Server where import Control.Monad -import Control.Monad.Except import Control.Monad.Reader -import Data.Aeson (FromJSON, ToJSON) +import Data.Aeson (FromJSON, ToJSON (..)) import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ import Data.Text (Text) import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic) @@ -23,11 +27,46 @@ import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Library.Commands import Simplex.Chat.Options +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON) import Simplex.Messaging.Transport.Server (runLocalTCPServer) import Simplex.Messaging.Util (raceAny_) import UnliftIO.Exception import UnliftIO.STM +data ChatSrvRequest = ChatSrvRequest {corrId :: Text, cmd :: Text} + deriving (Generic, FromJSON) + +data ChatSrvResponse r = ChatSrvResponse {corrId :: Maybe Text, resp :: CSRBody r} + +data CSRBody r = CSRBody {csrBody :: Either ChatError r} + +-- backwards compatible encoding, to avoid breaking any chat bots +data ObjChatCmdError = ObjChatCmdError {chatError :: ChatError} + +data ObjChatError = ObjChatError {chatError :: ChatError} + +$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "Obj") ''ObjChatCmdError) + +$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "Obj") ''ObjChatError) + +-- this encoding preserves the websocket API format when ChatError was sent either with type: "chatError" or type: "chatCmdError" +instance ToJSON (CSRBody ChatResponse) where + toJSON = either (toJSON . ObjChatCmdError) toJSON . csrBody + toEncoding = either (toEncoding . ObjChatCmdError) toEncoding . csrBody + +-- this encoding preserves the websocket API format when ChatError was sent either with type: "chatError" or type: "chatCmdError" +instance ToJSON (CSRBody ChatEvent) where + toJSON = either (toJSON . ObjChatError) toJSON . csrBody + toEncoding = either (toEncoding . ObjChatError) toEncoding . csrBody + +data AChatSrvResponse = forall r. ToJSON (ChatSrvResponse r) => ACR (ChatSrvResponse r) + +$(pure []) + +instance ToJSON (CSRBody r) => ToJSON (ChatSrvResponse r) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''ChatSrvResponse) + toJSON = $(JQ.mkToJSON defaultJSON ''ChatSrvResponse) + simplexChatServer :: ServiceName -> ChatConfig -> ChatOpts -> IO () simplexChatServer chatPort cfg opts = simplexChatCore cfg opts . const $ runChatServer defaultChatServerConfig {chatPort} @@ -44,19 +83,9 @@ defaultChatServerConfig = clientQSize = 1 } -data ChatSrvRequest = ChatSrvRequest {corrId :: Text, cmd :: Text} - deriving (Generic, FromJSON) - -data ChatSrvResponse = ChatSrvResponse {corrId :: Maybe Text, resp :: ChatResponse} - deriving (Generic) - -instance ToJSON ChatSrvResponse where - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - data ChatClient = ChatClient { rcvQ :: TBQueue (Text, ChatCommand), - sndQ :: TBQueue ChatSrvResponse + sndQ :: TBQueue AChatSrvResponse } newChatServerClient :: Natural -> STM ChatClient @@ -78,14 +107,14 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do getConnection sock = WS.makePendingConnection sock WS.defaultConnectionOptions >>= WS.acceptRequest send ws ChatClient {sndQ} = forever $ - atomically (readTBQueue sndQ) >>= WS.sendTextData ws . J.encode + atomically (readTBQueue sndQ) >>= \(ACR r) -> WS.sendTextData ws (J.encode r) client ChatClient {rcvQ, sndQ} = forever $ do atomically (readTBQueue rcvQ) >>= processCommand - >>= atomically . writeTBQueue sndQ + >>= atomically . writeTBQueue sndQ . ACR output ChatClient {sndQ} = forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - atomically $ writeTBQueue sndQ ChatSrvResponse {corrId = Nothing, resp} + (_, r) <- atomically . readTBQueue $ outputQ cc + atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId = Nothing, resp = CSRBody r} receive ws ChatClient {rcvQ, sndQ} = forever $ do s <- WS.receiveData ws case J.decodeStrict' s of @@ -96,11 +125,9 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do Left e -> sendError (Just corrId) e Nothing -> sendError Nothing "invalid request" where - sendError corrId e = atomically $ writeTBQueue sndQ ChatSrvResponse {corrId, resp = chatCmdError Nothing e} + sendError corrId e = atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId, resp = CSRBody $ chatCmdError e} processCommand (corrId, cmd) = - runReaderT (runExceptT $ processChatCommand cmd) cc >>= \case - Right resp -> response resp - Left e -> response $ CRChatCmdError Nothing e + response <$> runReaderT (execChatCommand' cmd 0) cc where - response resp = pure ChatSrvResponse {corrId = Just corrId, resp} + response r = ChatSrvResponse {corrId = Just corrId, resp = CSRBody r} clientDisconnected _ = pure () diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs index 0c6464dbfe..33145497ea 100644 --- a/apps/simplex-directory-service/Main.hs +++ b/apps/simplex-directory-service/Main.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} module Main where @@ -5,13 +6,22 @@ module Main where import Directory.Options import Directory.Service import Directory.Store -import Simplex.Chat.Core +import Directory.Store.Migrate import Simplex.Chat.Terminal (terminalChatConfig) main :: IO () main = do - opts@DirectoryOpts {directoryLog, runCLI} <- welcomeGetOpts - st <- restoreDirectoryStore directoryLog - if runCLI - then directoryServiceCLI st opts - else simplexChatCore terminalChatConfig (mkChatOpts opts) $ directoryService st opts + opts@DirectoryOpts {directoryLog, migrateDirectoryLog, runCLI} <- welcomeGetOpts + case migrateDirectoryLog of + Just cmd -> migrate cmd opts terminalChatConfig + Nothing -> do + st <- openDirectoryLog directoryLog + if runCLI + then directoryServiceCLI st opts + else directoryService st opts terminalChatConfig + where + migrate = \case + MLCheck -> checkDirectoryLog + MLImport -> importDirectoryLogToDB + MLExport -> exportDBToDirectoryLog + MLListing -> saveGroupListingFiles diff --git a/apps/simplex-directory-service/src/Directory/BlockedWords.hs b/apps/simplex-directory-service/src/Directory/BlockedWords.hs new file mode 100644 index 0000000000..a29e2c99e0 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/BlockedWords.hs @@ -0,0 +1,77 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Directory.BlockedWords where + +import Data.Char (isMark, isPunctuation, isSpace) +import Data.List (isPrefixOf) +import Data.Maybe (fromMaybe) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Set (Set) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Normalize as TN + +data BlockedWordsConfig = BlockedWordsConfig + { blockedWords :: Set Text, + blockedFragments :: Set Text, + extensionRules :: [(String, [String])], + spelling :: Map Char [Char] + } + +hasBlockedFragments :: BlockedWordsConfig -> Text -> Bool +hasBlockedFragments BlockedWordsConfig {spelling, blockedFragments} s = + any (\w -> any (`T.isInfixOf` w) blockedFragments) ws + where + ws = S.fromList $ filter (not . T.null) $ normalizeText spelling s + +hasBlockedWords :: BlockedWordsConfig -> Text -> Bool +hasBlockedWords BlockedWordsConfig {spelling, blockedWords} s = + not $ ws1 `S.disjoint` blockedWords && (length ws <= 1 || ws2 `S.disjoint` blockedWords) + where + ws = T.words s + ws1 = normalizeWords ws + ws2 = normalizeWords $ T.splitOn " " s + normalizeWords = S.fromList . filter (not . T.null) . concatMap (normalizeText spelling) + +normalizeText :: Map Char [Char] -> Text -> [Text] +normalizeText spelling' = + map (T.pack . filter (\c -> not $ isSpace c || isPunctuation c || isMark c)) + . allSubstitutions spelling' + . removeTriples + . T.unpack + . T.toLower + . TN.normalize TN.NFKD + +-- replaces triple and larger occurences with doubles +removeTriples :: String -> String +removeTriples xs = go xs '\0' False + where + go [] _ _ = [] + go (c : cs) prev samePrev + | prev /= c = c : go cs c False + | samePrev = go cs c True + | otherwise = c : go cs c True + +-- Generate all possible strings by substituting each character +allSubstitutions :: Map Char [Char] -> String -> [String] +allSubstitutions spelling' = sequence . map substs + where + substs c = fromMaybe [c] $ M.lookup c spelling' + +wordVariants :: [(String, [String])] -> String -> [Text] +wordVariants [] s = [T.pack s] +wordVariants (sub : subs) s = concatMap (wordVariants subs) (replace sub) + where + replace (pat, tos) = go s + where + go [] = [""] + go s'@(c : rest) + | pat `isPrefixOf` s' = + let s'' = drop (length pat) s' + restVariants = go s'' + in map (pat <>) restVariants + <> concatMap (\to -> map (to <>) restVariants) tos + | otherwise = map (c :) (go rest) diff --git a/apps/simplex-directory-service/src/Directory/Captcha.hs b/apps/simplex-directory-service/src/Directory/Captcha.hs new file mode 100644 index 0000000000..d60b09df83 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Captcha.hs @@ -0,0 +1,37 @@ +module Directory.Captcha (getCaptchaStr, matchCaptchaStr) where + +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import System.Random (randomRIO) + +getCaptchaStr :: Int -> String -> IO String +getCaptchaStr 0 s = pure s +getCaptchaStr n s = do + i <- randomRIO (0, length captchaChars - 1) + let c = captchaChars !! i + getCaptchaStr (n - 1) (c : s) + where + captchaChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +matchCaptchaStr :: T.Text -> T.Text -> Bool +matchCaptchaStr captcha guess = T.length captcha == T.length guess && matchChars (T.zip captcha guess) + where + matchChars [] = True + matchChars ((c, g) : cs) = matchChar c == matchChar g && matchChars cs + matchChar c = fromMaybe c $ M.lookup c captchaMatches + captchaMatches = + M.fromList + [ ('0', 'O'), + ('1', 'I'), + ('c', 'C'), + ('l', 'I'), + ('o', 'O'), + ('p', 'P'), + ('s', 'S'), + ('u', 'U'), + ('v', 'V'), + ('w', 'W'), + ('x', 'X'), + ('z', 'Z') + ] diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 19c9405358..1f075c677c 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -11,25 +11,27 @@ module Directory.Events ( DirectoryEvent (..), DirectoryCmd (..), ADirectoryCmd (..), + DirectoryHelpSection (..), DirectoryRole (..), SDirectoryRole (..), crDirectoryEvent, directoryCmdTag, - viewName, ) where -import Control.Applicative ((<|>)) +import Control.Applicative (optional, (<|>)) import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A import Data.Char (isSpace) import Data.Either (fromRight) import Data.Functor (($>)) +import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Directory.Store import Simplex.Chat.Controller +import Simplex.Chat.Markdown (displayNameTextP) import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) @@ -44,7 +46,9 @@ data DirectoryEvent = DEContactConnected Contact | DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} - | DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo} + | DEGroupUpdated {member :: GroupMember, fromGroup :: GroupInfo, toGroup :: GroupInfo} + | DEPendingMember GroupInfo GroupMember + | DEPendingMemberMsg GroupInfo GroupMember ChatItemId Text | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed | DEServiceRoleChanged GroupInfo GroupMemberRole | DEContactRemovedFromGroup ContactId GroupInfo @@ -58,36 +62,47 @@ data DirectoryEvent | DELogChatResponse Text deriving (Show) -crDirectoryEvent :: ChatResponse -> Maybe DirectoryEvent +crDirectoryEvent :: Either ChatError ChatEvent -> Maybe DirectoryEvent crDirectoryEvent = \case - CRContactConnected {contact} -> Just $ DEContactConnected contact - CRReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} - CRUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember - CRGroupUpdated {fromGroup, toGroup, member_} -> (\contactId -> DEGroupUpdated {contactId, fromGroup, toGroup}) <$> (memberContactId =<< member_) - CRMemberRole {groupInfo, member, toRole} + Right evt -> crDirectoryEvent_ evt + Left e -> case e of + ChatErrorAgent {agentError = BROKER _ (NETWORK _)} -> Nothing + ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing + _ -> Just $ DELogChatResponse $ "chat error: " <> tshow e + +crDirectoryEvent_ :: ChatEvent -> Maybe DirectoryEvent +crDirectoryEvent_ = \case + CEvtContactConnected {contact} -> Just $ DEContactConnected contact + CEvtReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} + CEvtUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember + CEvtGroupUpdated {fromGroup, toGroup, member_} -> (\member -> DEGroupUpdated {member, fromGroup, toGroup}) <$> member_ + CEvtJoinedGroupMember {groupInfo, member = m} + | pending m -> Just $ DEPendingMember groupInfo m + | otherwise -> Nothing + CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g _scopeInfo) ci : _} -> case ci of + ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t + _ -> Nothing + CEvtMemberRole {groupInfo, member, toRole} | groupMemberId' member == groupMemberId' (membership groupInfo) -> Just $ DEServiceRoleChanged groupInfo toRole | otherwise -> (\ctId -> DEContactRoleChanged groupInfo ctId toRole) <$> memberContactId member - CRDeletedMember {groupInfo, deletedMember} -> (`DEContactRemovedFromGroup` groupInfo) <$> memberContactId deletedMember - CRLeftMember {groupInfo, member} -> (`DEContactLeftGroup` groupInfo) <$> memberContactId member - CRDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo - CRGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo - CRChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct - CRChatItemsDeleted {chatItemDeletions = ((ChatItemDeletion (AChatItem _ SMDRcv (DirectChat ct) _) _) : _), byUser = False} -> Just $ DEItemDeleteIgnored ct - CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}) : _} -> + CEvtDeletedMember {groupInfo, deletedMember} -> (`DEContactRemovedFromGroup` groupInfo) <$> memberContactId deletedMember + CEvtLeftMember {groupInfo, member} -> (`DEContactLeftGroup` groupInfo) <$> memberContactId member + CEvtDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo + CEvtGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo + CEvtChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct + CEvtChatItemsDeleted {chatItemDeletions = ((ChatItemDeletion (AChatItem _ SMDRcv (DirectChat ct) _) _) : _), byUser = False} -> Just $ DEItemDeleteIgnored ct + CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}) : _} -> Just $ case (mc, itemLive) of (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t _ -> DEUnsupportedMessage ct ciId where ciId = chatItemId' ci err = ADC SDRUser DCUnknownCommand - CRMessageError {severity, errorMessage} -> Just $ DELogChatResponse $ "message error: " <> severity <> ", " <> errorMessage - CRChatCmdError {chatError} -> Just $ DELogChatResponse $ "chat cmd error: " <> tshow chatError - CRChatError {chatError} -> case chatError of - ChatErrorAgent {agentError = BROKER _ NETWORK} -> Nothing - ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing - _ -> Just $ DELogChatResponse $ "chat error: " <> tshow chatError - CRChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors) + CEvtMessageError {severity, errorMessage} -> Just $ DELogChatResponse $ "message error: " <> severity <> ", " <> errorMessage + CEvtChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors) _ -> Nothing + where + pending m = memberStatus m == GSMemPendingApproval data DirectoryRole = DRUser | DRAdmin | DRSuperUser @@ -107,24 +122,31 @@ data DirectoryCmdTag (r :: DirectoryRole) where DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser DCListUserGroups_ :: DirectoryCmdTag 'DRUser DCDeleteGroup_ :: DirectoryCmdTag 'DRUser - DCSetRole_ :: DirectoryCmdTag 'DRUser + DCMemberRole_ :: DirectoryCmdTag 'DRUser + DCGroupFilter_ :: DirectoryCmdTag 'DRUser + DCShowUpgradeGroupLink_ :: DirectoryCmdTag 'DRUser DCApproveGroup_ :: DirectoryCmdTag 'DRAdmin DCRejectGroup_ :: DirectoryCmdTag 'DRAdmin DCSuspendGroup_ :: DirectoryCmdTag 'DRAdmin DCResumeGroup_ :: DirectoryCmdTag 'DRAdmin DCListLastGroups_ :: DirectoryCmdTag 'DRAdmin DCListPendingGroups_ :: DirectoryCmdTag 'DRAdmin - DCShowGroupLink_ :: DirectoryCmdTag 'DRAdmin DCSendToGroupOwner_ :: DirectoryCmdTag 'DRAdmin DCInviteOwnerToGroup_ :: DirectoryCmdTag 'DRAdmin + -- DCAddBlockedWord_ :: DirectoryCmdTag 'DRAdmin + -- DCRemoveBlockedWord_ :: DirectoryCmdTag 'DRAdmin + DCPromoteGroup_ :: DirectoryCmdTag 'DRSuperUser DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser deriving instance Show (DirectoryCmdTag r) data ADirectoryCmdTag = forall r. ADCT (SDirectoryRole r) (DirectoryCmdTag r) +data DirectoryHelpSection = DHSRegistration | DHSCommands + deriving (Show) + data DirectoryCmd (r :: DirectoryRole) where - DCHelp :: DirectoryCmd 'DRUser + DCHelp :: DirectoryHelpSection -> DirectoryCmd 'DRUser DCSearchGroup :: Text -> DirectoryCmd 'DRUser DCSearchNext :: DirectoryCmd 'DRUser DCAllGroups :: DirectoryCmd 'DRUser @@ -133,16 +155,20 @@ data DirectoryCmd (r :: DirectoryRole) where DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCListUserGroups :: DirectoryCmd 'DRUser DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser - DCSetRole :: GroupId -> GroupName -> GroupMemberRole -> DirectoryCmd 'DRUser - DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRAdmin + DCMemberRole :: UserGroupRegId -> Maybe GroupName -> Maybe GroupMemberRole -> DirectoryCmd 'DRUser + DCGroupFilter :: UserGroupRegId -> Maybe GroupName -> Maybe DirectoryMemberAcceptance -> DirectoryCmd 'DRUser + DCShowUpgradeGroupLink :: GroupId -> Maybe GroupName -> DirectoryCmd 'DRUser + DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId, promote :: Maybe Bool} -> DirectoryCmd 'DRAdmin DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCListLastGroups :: Int -> DirectoryCmd 'DRAdmin DCListPendingGroups :: Int -> DirectoryCmd 'DRAdmin - DCShowGroupLink :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCSendToGroupOwner :: GroupId -> GroupName -> Text -> DirectoryCmd 'DRAdmin DCInviteOwnerToGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + -- DCAddBlockedWord :: Text -> DirectoryCmd 'DRAdmin + -- DCRemoveBlockedWord :: Text -> DirectoryCmd 'DRAdmin + DCPromoteGroup :: GroupId -> GroupName -> Bool -> DirectoryCmd 'DRSuperUser DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser DCUnknownCommand :: DirectoryCmd 'DRUser DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r @@ -163,7 +189,7 @@ directoryCmdP = (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) <|> pure (ADC SDRUser DCUnknownCommand) tagP = - A.takeTill (== ' ') >>= \case + A.takeTill isSpace >>= \case "help" -> u DCHelp_ "h" -> u DCHelp_ "next" -> u DCSearchNext_ @@ -174,16 +200,20 @@ directoryCmdP = "list" -> u DCListUserGroups_ "ls" -> u DCListUserGroups_ "delete" -> u DCDeleteGroup_ - "role" -> u DCSetRole_ + "role" -> u DCMemberRole_ + "filter" -> u DCGroupFilter_ + "link" -> u DCShowUpgradeGroupLink_ "approve" -> au DCApproveGroup_ "reject" -> au DCRejectGroup_ "suspend" -> au DCSuspendGroup_ "resume" -> au DCResumeGroup_ "last" -> au DCListLastGroups_ "pending" -> au DCListPendingGroups_ - "link" -> au DCShowGroupLink_ "owner" -> au DCSendToGroupOwner_ "invite" -> au DCInviteOwnerToGroup_ + -- "block_word" -> au DCAddBlockedWord_ + -- "unblock_word" -> au DCRemoveBlockedWord_ + "promote" -> su DCPromoteGroup_ "exec" -> su DCExecuteCommand_ "x" -> su DCExecuteCommand_ _ -> fail "bad command tag" @@ -193,67 +223,107 @@ directoryCmdP = su = pure . ADCT SDRSuperUser cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r) cmdP = \case - DCHelp_ -> pure DCHelp + DCHelp_ -> DCHelp . fromMaybe DHSRegistration <$> optional (A.takeWhile isSpace *> helpSectionP) + where + helpSectionP = + A.takeText >>= \case + "registration" -> pure DHSRegistration + "r" -> pure DHSRegistration + "commands" -> pure DHSCommands + "c" -> pure DHSCommands + _ -> fail "bad help section" DCSearchNext_ -> pure DCSearchNext DCAllGroups_ -> pure DCAllGroups DCRecentGroups_ -> pure DCRecentGroups - DCSubmitGroup_ -> fmap DCSubmitGroup . strDecode . encodeUtf8 <$?> (A.takeWhile1 isSpace *> A.takeText) + DCSubmitGroup_ -> fmap DCSubmitGroup . strDecode . encodeUtf8 <$?> (spacesP *> A.takeText) DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup DCListUserGroups_ -> pure DCListUserGroups DCDeleteGroup_ -> gc DCDeleteGroup - DCSetRole_ -> do - (groupId, displayName) <- gc (,) - memberRole <- A.space *> ("member" $> GRMember <|> "observer" $> GRObserver) - pure $ DCSetRole groupId displayName memberRole + DCMemberRole_ -> do + (groupId, displayName_) <- gc_ (,) + memberRole_ <- optional $ spacesP *> ("member" $> GRMember <|> "observer" $> GRObserver) + pure $ DCMemberRole groupId displayName_ memberRole_ + DCGroupFilter_ -> do + (groupId, displayName_) <- gc_ (,) + acceptance_ <- + (A.takeWhile isSpace >> A.endOfInput) $> Nothing + <|> Just <$> (acceptancePresetsP <|> acceptanceFiltersP) + pure $ DCGroupFilter groupId displayName_ acceptance_ + where + acceptancePresetsP = + spacesP + *> A.choice + [ "off" $> noJoinFilter, + "basic" $> basicJoinFilter, + ("moderate" <|> "mod") $> moderateJoinFilter, + "strong" $> strongJoinFilter + ] + acceptanceFiltersP = do + rejectNames <- filterP "name" + passCaptcha <- filterP "captcha" + makeObserver <- filterP "observer" + pure DirectoryMemberAcceptance {rejectNames, passCaptcha, makeObserver} + filterP :: Text -> Parser (Maybe ProfileCondition) + filterP s = Just <$> (spacesP *> A.string s *> conditionP) <|> pure Nothing + conditionP = + "=all" $> PCAll + <|> ("=noimage" <|> "=no_image" <|> "=no-image") $> PCNoImage + <|> pure PCAll + DCShowUpgradeGroupLink_ -> gc_ DCShowUpgradeGroupLink DCApproveGroup_ -> do (groupId, displayName) <- gc (,) groupApprovalId <- A.space *> A.decimal - pure DCApproveGroup {groupId, displayName, groupApprovalId} + promote <- Just <$> (" promote=" *> onOffP) <|> pure Nothing + pure DCApproveGroup {groupId, displayName, groupApprovalId, promote} DCRejectGroup_ -> gc DCRejectGroup DCSuspendGroup_ -> gc DCSuspendGroup DCResumeGroup_ -> gc DCResumeGroup DCListLastGroups_ -> DCListLastGroups <$> (A.space *> A.decimal <|> pure 10) DCListPendingGroups_ -> DCListPendingGroups <$> (A.space *> A.decimal <|> pure 10) - DCShowGroupLink_ -> gc DCShowGroupLink DCSendToGroupOwner_ -> do (groupId, displayName) <- gc (,) msg <- A.space *> A.takeText pure $ DCSendToGroupOwner groupId displayName msg DCInviteOwnerToGroup_ -> gc DCInviteOwnerToGroup - DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (A.space *> A.takeText) + -- DCAddBlockedWord_ -> DCAddBlockedWord <$> wordP + -- DCRemoveBlockedWord_ -> DCRemoveBlockedWord <$> wordP + DCPromoteGroup_ -> do + (groupId, displayName) <- gc (,) + promote <- A.space *> onOffP + pure $ DCPromoteGroup groupId displayName promote + DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (spacesP *> A.takeText) where - gc f = f <$> (A.space *> A.decimal <* A.char ':') <*> displayNameP - displayNameP = quoted '\'' <|> takeNameTill (== ' ') - takeNameTill p = - A.peekChar' >>= \c -> - if refChar c then A.takeTill p else fail "invalid first character in display name" - quoted c = A.char c *> takeNameTill (== c) <* A.char c - refChar c = c > ' ' && c /= '#' && c /= '@' - -viewName :: Text -> Text -viewName n = if any (== ' ') (T.unpack n) then "'" <> n <> "'" else n + gc f = f <$> (spacesP *> A.decimal) <*> (A.char ':' *> displayNameTextP) + gc_ f = f <$> (spacesP *> A.decimal) <*> optional (A.char ':' *> displayNameTextP) + -- wordP = spacesP *> A.takeTill isSpace + spacesP = A.takeWhile1 isSpace + onOffP = (A.string "on" $> True) <|> (A.string "off" $> False) directoryCmdTag :: DirectoryCmd r -> Text directoryCmdTag = \case - DCHelp -> "help" + DCHelp _ -> "help" DCSearchGroup _ -> "search" DCSearchNext -> "next" DCAllGroups -> "all" DCRecentGroups -> "new" DCSubmitGroup _ -> "submit" DCConfirmDuplicateGroup {} -> "confirm" - DCListUserGroups -> "list" + DCListUserGroups -> "list" DCDeleteGroup {} -> "delete" DCApproveGroup {} -> "approve" - DCSetRole {} -> "role" + DCMemberRole {} -> "role" + DCGroupFilter {} -> "filter" + DCShowUpgradeGroupLink {} -> "link" DCRejectGroup {} -> "reject" DCSuspendGroup {} -> "suspend" DCResumeGroup {} -> "resume" DCListLastGroups _ -> "last" DCListPendingGroups _ -> "pending" - DCShowGroupLink {} -> "link" DCSendToGroupOwner {} -> "owner" DCInviteOwnerToGroup {} -> "invite" + -- DCAddBlockedWord _ -> "block_word" + -- DCRemoveBlockedWord _ -> "unblock_word" + DCPromoteGroup {} -> "promote" DCExecuteCommand _ -> "exec" DCUnknownCommand -> "unknown" DCCommandError _ -> "error" diff --git a/apps/simplex-directory-service/src/Directory/Listing.hs b/apps/simplex-directory-service/src/Directory/Listing.hs new file mode 100644 index 0000000000..0d4e8d351c --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Listing.hs @@ -0,0 +1,148 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Directory.Listing where + +import Control.Applicative ((<|>)) +import Control.Monad +import Crypto.Hash (Digest, MD5) +import qualified Crypto.Hash as CH +import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import qualified Data.ByteArray as BA +import Data.ByteString (ByteString) +import qualified Data.ByteString.Base64 as B64 +import qualified Data.ByteString.Base64.URL as B64URL +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy as LB +import Data.Int (Int64) +import Data.List (isPrefixOf) +import Data.Maybe (catMaybes, fromMaybe) +import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) +import Data.Time.Clock +import Data.Time.Clock.System +import Data.Time.Format.ISO8601 (iso8601Show) +import Directory.Store +import Simplex.Chat.Markdown +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON) +import System.Directory +import System.FilePath + +directoryDataPath :: String +directoryDataPath = "data" + +listingFileName :: String +listingFileName = "listing.json" + +promotedFileName :: String +promotedFileName = "promoted.json" + +listingImageFolder :: String +listingImageFolder = "images" + +data DirectoryEntryType = DETGroup + { admission :: Maybe GroupMemberAdmission, + summary :: GroupSummary + } + +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "DET") ''DirectoryEntryType) + +data DirectoryEntry = DirectoryEntry + { entryType :: DirectoryEntryType, + displayName :: Text, + groupLink :: CreatedLinkContact, + shortDescr :: Maybe MarkdownList, + welcomeMessage :: Maybe MarkdownList, + imageFile :: Maybe String, + activeAt :: Maybe UTCTime, + createdAt :: Maybe UTCTime + } + +$(JQ.deriveJSON defaultJSON ''DirectoryEntry) + +data DirectoryListing = DirectoryListing {entries :: [DirectoryEntry]} + +$(JQ.deriveJSON defaultJSON ''DirectoryListing) + +type ImageFileData = ByteString + +newOrActive :: NominalDiffTime +newOrActive = 30 * nominalDay + +recentRoundedTime :: Int64 -> UTCTime -> UTCTime -> Maybe UTCTime +recentRoundedTime roundTo now t + | diffUTCTime now t > newOrActive = Nothing + | otherwise = + let secs = (systemSeconds (utcToSystemTime t) `div` roundTo) * roundTo + in Just $ systemToUTCTime $ MkSystemTime secs 0 + +groupDirectoryEntry :: UTCTime -> GroupInfo -> Maybe GroupLink -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData)) +groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary} gLink_ = + let GroupProfile {displayName, shortDescr, description, image, memberAdmission} = groupProfile + entryType = DETGroup memberAdmission groupSummary + entry groupLink = + let de = + DirectoryEntry + { entryType, + displayName, + groupLink, + shortDescr = toFormattedText <$> shortDescr, + welcomeMessage = toFormattedText <$> description, + imageFile = fst <$> imgData, + activeAt = recentRoundedTime 900 now $ fromMaybe createdAt chatTs, + createdAt = recentRoundedTime 86400 now createdAt + } + imgData = imgFileData groupLink =<< image + in (de, imgData) + in (entry . connLinkContact) <$> gLink_ + where + imgFileData :: CreatedConnLink 'CMContact -> ImageData -> Maybe (FilePath, ByteString) + imgFileData groupLink (ImageData img) = + let (img', imgExt) = + fromMaybe (img, ".jpg") $ + (,".jpg") <$> T.stripPrefix "data:image/jpg;base64," img + <|> (,".png") <$> T.stripPrefix "data:image/png;base64," img + imgName = B.unpack $ B64URL.encodeUnpadded $ BA.convert $ (CH.hash :: ByteString -> Digest MD5) $ strEncode (connFullLink groupLink) + imgFile = listingImageFolder imgName <> imgExt + in case B64.decode $ encodeUtf8 img' of + Right img'' -> Just (imgFile, img'') + Left _ -> Nothing + +generateListing :: FilePath -> [(GroupInfo, GroupReg, Maybe GroupLink)] -> IO () +generateListing dir gs = do + createDirectoryIfMissing True dir + oldDirs <- filter ((directoryDataPath <> ".") `isPrefixOf`) <$> listDirectory dir + ts <- getCurrentTime + let newDirPath = directoryDataPath <> "." <> iso8601Show ts <> "/" + newDir = dir newDirPath + createDirectoryIfMissing True (newDir listingImageFolder) + gs' <- + fmap catMaybes $ forM gs $ \(g, gr, link_) -> + forM (groupDirectoryEntry ts g link_) $ \(g', img) -> do + forM_ img $ \(imgFile, imgData) -> B.writeFile (newDir imgFile) imgData + pure (g', gr) + saveListing newDir listingFileName gs' + saveListing newDir promotedFileName $ filter (\(_, GroupReg {promoted}) -> promoted) gs' + -- atomically update the link + let newSymLink = newDir <> ".link" + symLink = dir directoryDataPath + createDirectoryLink newDirPath newSymLink + renamePath newSymLink symLink + mapM_ (removePathForcibly . (dir )) oldDirs + where + saveListing newDir f = LB.writeFile (newDir f) . J.encode . DirectoryListing . map fst + +toFormattedText :: Text -> MarkdownList +toFormattedText t = fromMaybe [FormattedText Nothing t] $ parseMaybeMarkdownList t diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index a62939b6ac..fb82c27b78 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -1,34 +1,49 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Directory.Options ( DirectoryOpts (..), + MigrateLog (..), getDirectoryOpts, mkChatOpts, ) where +import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) -import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) +import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP) +import Simplex.Messaging.Parsers (parseAll) data DirectoryOpts = DirectoryOpts { coreOptions :: CoreChatOpts, adminUsers :: [KnownContact], superUsers :: [KnownContact], ownersGroup :: Maybe KnownGroup, + blockedWordsFile :: Maybe FilePath, + blockedFragmentsFile :: Maybe FilePath, + blockedExtensionRules :: Maybe FilePath, + nameSpellingFile :: Maybe FilePath, + profileNameLimit :: Int, + captchaGenerator :: Maybe FilePath, directoryLog :: Maybe FilePath, + migrateDirectoryLog :: Maybe MigrateLog, serviceName :: T.Text, runCLI :: Bool, searchResults :: Int, + webFolder :: Maybe FilePath, testing :: Bool } +data MigrateLog = MLCheck | MLImport | MLExport | MLListing + directoryOpts :: FilePath -> FilePath -> Parser DirectoryOpts directoryOpts appDir defaultDbName = do coreOptions <- coreChatOptsP appDir defaultDbName @@ -55,35 +70,101 @@ directoryOpts appDir defaultDbName = do <> metavar "OWNERS_GROUP" <> help "The group of group owners in the format GROUP_ID:DISPLAY_NAME - owners of listed groups will be invited automatically" ) + blockedWordsFile <- + optional $ + strOption + ( long "blocked-words-file" + <> metavar "BLOCKED_WORDS_FILE" + <> help "File with the basic forms of words not allowed in profiles" + ) + blockedFragmentsFile <- + optional $ + strOption + ( long "blocked-fragments-file" + <> metavar "BLOCKED_WORDS_FILE" + <> help "File with the basic forms of word fragments not allowed in profiles" + ) + blockedExtensionRules <- + optional $ + strOption + ( long "blocked-extenstion-rules" + <> metavar "BLOCKED_EXTENSION_RULES" + <> help "Substitions to extend the list of blocked words" + ) + nameSpellingFile <- + optional $ + strOption + ( long "name-spelling-file" + <> metavar "NAME_SPELLING_FILE" + <> help "File with the character substitions to match in profile names" + ) + profileNameLimit <- + option + auto + ( long "profile-name-limit" + <> metavar "PROFILE_NAME_LIMIT" + <> help "Max length of profile name that will be allowed to connect and to join groups" + <> value maxBound + ) + captchaGenerator <- + optional $ + strOption + ( long "captcha-generator" + <> metavar "CAPTCHA_GENERATOR" + <> help "Executable to generate captcha files, must accept text as parameter and save file to stdout as base64 up to 12500 bytes" + ) directoryLog <- - Just - <$> strOption + optional $ + strOption ( long "directory-file" <> metavar "DIRECTORY_FILE" <> help "Append only log for directory state" ) + migrateDirectoryLog <- + optional $ + option + parseMigrateLog + ( long "migrate-directory-file" + <> metavar "MIGRATE_COMMAND" + <> help "Command to import/export directory log file" + ) serviceName <- strOption ( long "service-name" <> metavar "SERVICE_NAME" - <> help "The display name of the directory service bot, without *'s and spaces (SimpleX-Directory)" - <> value "SimpleX-Directory" + <> help "The display name of the directory service bot, without *'s and spaces (SimpleX Directory)" + <> value "SimpleX Directory" ) - runCLI <- + runCLI <- switch ( long "run-cli" <> help "Run directory service as CLI" ) + webFolder <- + optional $ + strOption + ( long "web-folder" + <> metavar "WEB_FOLDER" + <> help "Folder to store static web assets" + ) pure DirectoryOpts { coreOptions, adminUsers, superUsers, ownersGroup, + blockedWordsFile, + blockedFragmentsFile, + blockedExtensionRules, + nameSpellingFile, + profileNameLimit, + captchaGenerator, directoryLog, + migrateDirectoryLog, serviceName = T.pack serviceName, runCLI, searchResults = 10, + webFolder, testing = False } @@ -99,10 +180,9 @@ getDirectoryOpts appDir defaultDbName = versionAndUpdate = versionStr <> "\n" <> updateStr mkChatOpts :: DirectoryOpts -> ChatOpts -mkChatOpts DirectoryOpts {coreOptions} = +mkChatOpts DirectoryOpts {coreOptions, serviceName} = ChatOpts { coreOptions, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatCmdLog = CCLNone, @@ -114,5 +194,17 @@ mkChatOpts DirectoryOpts {coreOptions} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, + createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False}, maintenance = False } + +parseMigrateLog :: ReadM MigrateLog +parseMigrateLog = eitherReader $ parseAll mlP . encodeUtf8 . T.pack + where + mlP = + A.takeTill (== ' ') >>= \case + "check" -> pure MLCheck + "import" -> pure MLImport + "export" -> pure MLExport + "listing" -> pure MLListing + _ -> fail "bad MigrateLog" diff --git a/apps/simplex-directory-service/src/Directory/Search.hs b/apps/simplex-directory-service/src/Directory/Search.hs index 822182b053..d71c128370 100644 --- a/apps/simplex-directory-service/src/Directory/Search.hs +++ b/apps/simplex-directory-service/src/Directory/Search.hs @@ -1,12 +1,5 @@ -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE NamedFieldPuns #-} - module Directory.Search where -import Data.List (sortOn) -import Data.Ord (Down (..)) -import Data.Set (Set) -import qualified Data.Set as S import Data.Text (Text) import Data.Time.Clock (UTCTime) import Simplex.Chat.Types @@ -14,19 +7,7 @@ import Simplex.Chat.Types data SearchRequest = SearchRequest { searchType :: SearchType, searchTime :: UTCTime, - sentGroups :: Set GroupId + lastGroup :: GroupId -- cursor for search } data SearchType = STAll | STRecent | STSearch Text - -takeTop :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] -takeTop n = take n . sortOn (Down . currentMembers . snd) - -takeRecent :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] -takeRecent n = take n . sortOn (Down . (\GroupInfo {createdAt} -> createdAt) . fst) - -groupIds :: [(GroupInfo, GroupSummary)] -> Set GroupId -groupIds = S.fromList . map (\(GroupInfo {groupId}, _) -> groupId) - -filterNotSent :: Set GroupId -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] -filterNotSent sentGroups = filter (\(GroupInfo {groupId}, _) -> groupId `S.notMember` sentGroups) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index ed51371be3..5901e1d61a 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -1,10 +1,15 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Directory.Service ( welcomeGetOpts, @@ -14,42 +19,64 @@ module Directory.Service where import Control.Concurrent (forkIO) -import Control.Concurrent.Async import Control.Concurrent.STM import Control.Logger.Simple import Control.Monad +import Control.Monad.Except +import Control.Monad.IO.Class +import Data.Bifunctor (first) import Data.List (find, intercalate) -import Data.Maybe (fromMaybe, isJust, maybeToList) -import Data.Set (Set) +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe, isJust, isNothing) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T -import Data.Time.Clock (diffUTCTime, getCurrentTime) +import qualified Data.Text.IO as T +import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) +import Directory.BlockedWords +import Directory.Captcha import Directory.Events +import Directory.Listing import Directory.Options import Directory.Search import Directory.Store +import Directory.Store.Migrate +import Directory.Util import Simplex.Chat.Bot import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller import Simplex.Chat.Core +import Simplex.Chat.Markdown (Format (..), FormattedText (..), parseMaybeMarkdownList, viewName) import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Store.Direct (getContact) +import Simplex.Chat.Store.Groups (getGroupLink, getGroupMember, setGroupCustomData) -- TODO remove setGroupCustomData +import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo) import Simplex.Chat.Store.Shared (StoreError (..)) import Simplex.Chat.Terminal (terminalChatConfig) import Simplex.Chat.Terminal.Main (simplexChatCLI') import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared -import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) +import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) +import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact) import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) +import Simplex.Messaging.Util (eitherToMaybe, raceAny_, safeDecodeUtf8, tshow, unlessM, (<$$>)) import System.Directory (getAppUserDataDirectory) +import System.Exit (exitFailure) +import System.Process (readProcess) -data GroupProfileUpdate = GPNoServiceLink | GPServiceLinkAdded | GPServiceLinkRemoved | GPHasServiceLink | GPServiceLinkError +data GroupProfileUpdate + = GPNoServiceLink + | GPServiceLinkAdded {linkNow :: Text} + | GPServiceLinkRemoved + | GPHasServiceLink {linkBefore :: Text, linkNow :: Text} + | GPServiceLinkError data DuplicateGroup = DGUnique -- display name or full name is unique @@ -64,13 +91,34 @@ data GroupRolesStatus deriving (Eq) data ServiceState = ServiceState - { searchRequests :: TMap ContactId SearchRequest + { searchRequests :: TMap ContactId SearchRequest, + blockedWordsCfg :: BlockedWordsConfig, + pendingCaptchas :: TMap GroupMemberId PendingCaptcha, + updateListingsJob :: TMVar ChatController } -newServiceState :: IO ServiceState -newServiceState = do +data PendingCaptcha = PendingCaptcha + { captchaText :: Text, + sentAt :: UTCTime, + attempts :: Int + } + +captchaLength :: Int +captchaLength = 7 + +maxCaptchaAttempts :: Int +maxCaptchaAttempts = 5 + +captchaTTL :: NominalDiffTime +captchaTTL = 600 -- 10 minutes + +newServiceState :: DirectoryOpts -> IO ServiceState +newServiceState opts = do searchRequests <- TM.emptyIO - pure ServiceState {searchRequests} + blockedWordsCfg <- readBlockedWordsConfig opts + pendingCaptchas <- TM.emptyIO + updateListingsJob <- newEmptyTMVarIO + pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas, updateListingsJob} welcomeGetOpts :: IO DirectoryOpts welcomeGetOpts = do @@ -92,35 +140,150 @@ welcomeGetOpts = do knownContact KnownContact {contactId, localDisplayName = n} = knownName contactId n knownName i n = show i <> ":" <> T.unpack (viewName n) -directoryServiceCLI :: DirectoryStore -> DirectoryOpts -> IO () +directoryServiceCLI :: DirectoryLog -> DirectoryOpts -> IO () directoryServiceCLI st opts = do - env <- newServiceState + env <- newServiceState opts eventQ <- newTQueueIO let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) - race_ - (simplexChatCLI' terminalChatConfig {chatHooks = defaultChatHooks {eventHook}} (mkChatOpts opts) Nothing) - (processEvents eventQ env) + chatHooks = + defaultChatHooks + { preStartHook = Just $ directoryPreStartHook opts, + postStartHook = Just $ directoryPostStartHook opts env, + eventHook = Just eventHook, + acceptMember = Just $ acceptMemberHook opts env + } + raceAny_ $ + [ simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing, + processEvents eventQ env + ] + <> updateListingsThread_ opts env where processEvents eventQ env = forever $ do (cc, resp) <- atomically $ readTQueue eventQ u_ <- readTVarIO (currentUser cc) forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp -directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO () -directoryService st opts@DirectoryOpts {testing} user cc = do - initializeBotAddress' (not testing) cc - env <- newServiceState - race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - directoryServiceEvent st opts env user cc resp +updateListingDelay :: Int +updateListingDelay = 5 * 60 * 1000000 -- update every 5 minutes -directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> ChatResponse -> IO () -directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} ServiceState {searchRequests} user@User {userId} cc event = +updateListingsThread_ :: DirectoryOpts -> ServiceState -> [IO ()] +updateListingsThread_ opts env = maybe [] (\f -> [updateListingsThread f]) $ webFolder opts + where + updateListingsThread f = do + cc <- atomically $ takeTMVar $ updateListingsJob env + forever $ do + u <- readTVarIO $ currentUser cc + forM_ u $ \user -> updateGroupListingFiles cc user f + delay <- registerDelay updateListingDelay + atomically $ void (takeTMVar $ updateListingsJob env) `orElse` unlessM (readTVar delay) retry + +listingsUpdated :: ServiceState -> ChatController -> IO () +listingsUpdated env = void . atomically . tryPutTMVar (updateListingsJob env) + +directoryPreStartHook :: DirectoryOpts -> ChatController -> IO () +directoryPreStartHook opts ChatController {config, chatStore} = runDirectoryMigrations opts config chatStore + +directoryPostStartHook :: DirectoryOpts -> ServiceState -> ChatController -> IO () +directoryPostStartHook opts env cc = + readTVarIO (currentUser cc) >>= \case + Nothing -> putStrLn "No current user" >> exitFailure + Just User {userId, profile = p@LocalProfile {preferences}} -> do + listingsUpdated env cc + let cmds = fromMaybe [] $ preferences >>= commands_ + unless (cmds == directoryCommands) $ do + let prefs = (fromMaybe emptyChatPrefs preferences) {files = Just FilesPreference {allow = FANo}, commands = Just directoryCommands} :: Preferences + p' = (fromLocalProfile p) {displayName = serviceName opts, peerType = Just CPTBot, preferences = Just prefs} :: Profile + liftIO $ + sendChatCmd cc (APIUpdateProfile userId p') >>= \case + Right CRUserProfileUpdated {} -> putStrLn "Updated directory commands" + Right r -> putStrLn ("Error: unexpected response " <> show r) >> exitFailure + Left e -> putStrLn ("Error: " <> show e) >> exitFailure + +directoryCommands :: [ChatBotCommand] +directoryCommands = + [ CBCCommand "new" "New groups" Nothing, + CBCCommand "help" "How to submit your group" Nothing, + CBCCommand "list" "Your own groups" Nothing, + CBCMenu + "Group settings" + [ CBCCommand "role" "View new member role" idParam, + CBCCommand "filter" "Anti-spam filter" idParam, + CBCCommand "link" "View and upgrade group link" idParam, + CBCCommand "delete" "Remove a group from directory" (Just ":''") + ] + ] + where + idParam = Just "" + +directoryService :: DirectoryLog -> DirectoryOpts -> ChatConfig -> IO () +directoryService st opts@DirectoryOpts {testing} cfg = do + env <- newServiceState opts + let chatHooks = + defaultChatHooks + { preStartHook = Just $ directoryPreStartHook opts, + postStartHook = Just $ directoryPostStartHook opts env, + acceptMember = Just $ acceptMemberHook opts env + } + simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \user cc -> do + initializeBotAddress' (not testing) cc + raceAny_ $ + [ forever $ void getLine, + forever $ do + (_, resp) <- atomically . readTBQueue $ outputQ cc + directoryServiceEvent st opts env user cc resp + ] + <> updateListingsThread_ opts env + +acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)) +acceptMemberHook + DirectoryOpts {profileNameLimit} + ServiceState {blockedWordsCfg} + g + GroupLinkInfo {memberRole} + Profile {displayName, image = img} = runExceptT $ do + let a = groupMemberAcceptance g + when (useMemberFilter img $ rejectNames a) checkName + pure $ + if + | useMemberFilter img (passCaptcha a) -> (GAPendingApproval, GRMember) + | useMemberFilter img (makeObserver a) -> (GAAccepted, GRObserver) + | otherwise -> (GAAccepted, memberRole) + where + checkName :: ExceptT GroupRejectionReason IO () + checkName + | T.length displayName > profileNameLimit = throwError GRRLongName + | otherwise = do + when (hasBlockedFragments blockedWordsCfg displayName) $ throwError GRRBlockedName + when (hasBlockedWords blockedWordsCfg displayName) $ throwError GRRBlockedName + +groupMemberAcceptance :: GroupInfo -> DirectoryMemberAcceptance +groupMemberAcceptance GroupInfo {customData} = (\DirectoryGroupData {memberAcceptance = ma} -> ma) $ fromCustomData customData + +useMemberFilter :: Maybe ImageData -> Maybe ProfileCondition -> Bool +useMemberFilter img_ = \case + Just PCAll -> True + Just PCNoImage -> maybe True (\(ImageData i) -> i == "") img_ + Nothing -> False + +readBlockedWordsConfig :: DirectoryOpts -> IO BlockedWordsConfig +readBlockedWordsConfig DirectoryOpts {blockedFragmentsFile, blockedWordsFile, nameSpellingFile, blockedExtensionRules, testing} = do + extensionRules <- maybe (pure []) (fmap read . readFile) blockedExtensionRules + spelling <- maybe (pure M.empty) (fmap (M.fromList . read) . readFile) nameSpellingFile + blockedFragments <- S.fromList <$> maybe (pure []) (fmap T.lines . T.readFile) blockedFragmentsFile + bws <- maybe (pure []) (fmap lines . readFile) blockedWordsFile + let blockedWords = S.fromList $ concatMap (wordVariants extensionRules) bws + unless testing $ putStrLn $ "Blocked fragments: " <> show (length blockedFragments) <> ", blocked words: " <> show (length blockedWords) <> ", spelling rules: " <> show (M.size spelling) + pure BlockedWordsConfig {blockedFragments, blockedWords, extensionRules, spelling} + +directoryServiceEvent :: DirectoryLog -> DirectoryOpts -> ServiceState -> User -> ChatController -> Either ChatError ChatEvent -> IO () +directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc event = forM_ (crDirectoryEvent event) $ \case DEContactConnected ct -> deContactConnected ct DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner - DEGroupUpdated {contactId, fromGroup, toGroup} -> deGroupUpdated contactId fromGroup toGroup + DEGroupUpdated {member, fromGroup, toGroup} -> deGroupUpdated member fromGroup toGroup + DEPendingMember g m -> dePendingMember g m + DEPendingMemberMsg g m ciId t -> dePendingMemberMsg g m ciId t DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role DEServiceRoleChanged g role -> deServiceRoleChanged g role DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g @@ -138,85 +301,88 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own SDRSuperUser -> deSuperUserCommand ct ciId cmd DELogChatResponse r -> logInfo r where + groupLinkText (CCLink cReq sLnk_) = maybe (strEncodeTxt $ simplexChatContact cReq) strEncodeTxt sLnk_ withAdminUsers action = void . forkIO $ do forM_ superUsers $ \KnownContact {contactId} -> action contactId forM_ adminUsers $ \KnownContact {contactId} -> action contactId withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId notifyAdminUsers s = withAdminUsers $ \contactId -> sendMessage' cc contactId s - notifyOwner GroupReg {dbContactId} = sendMessage' cc dbContactId + notifyOwner = sendMessage' cc . dbContactId ctId `isOwner` GroupReg {dbContactId} = ctId == dbContactId - withGroupReg GroupInfo {groupId, localDisplayName} err action = do - atomically (getGroupReg st groupId) >>= \case - Just gr -> action gr - Nothing -> logError $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId - groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} = - n <> (if n == fn || T.null fn then "" else " (" <> fn <> ")") <> maybe "" ("\nWelcome message:\n" <>) d + withGroupReg :: GroupInfo -> Text -> (GroupReg -> IO ()) -> IO () + withGroupReg GroupInfo {groupId, localDisplayName} err action = + getGroupReg cc groupId >>= \case + Right gr -> action gr + Left e -> do + let msg = "Error: " <> err <> ", group: " <> tshow groupId <> " " <> localDisplayName <> ", " <> T.pack e + notifyAdminUsers msg + logError msg + groupInfoText p@GroupProfile {description = d} = groupNameDescr p <> maybe "" ("\nWelcome message:\n" <>) d + groupNameDescr GroupProfile {displayName = n, fullName = fn, shortDescr = sd_} = + n <> maybe "" (\d' -> " (" <> d' <> ")") descr + where + descr + | n == fn || T.null fn = if sd_ == Just "" then Nothing else sd_ + | otherwise = Just fn userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName userGroupReference' GroupReg {userGroupRegId} displayName = groupReference' userGroupRegId displayName groupReference GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = groupReference' groupId displayName groupReference' groupId displayName = "ID " <> tshow groupId <> " (" <> displayName <> ")" - groupAlreadyListed GroupInfo {groupProfile = GroupProfile {displayName, fullName}} = - "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name." + groupAlreadyListed GroupInfo {groupProfile = p} = + "The group " <> groupNameDescr p <> " is already listed in the directory, please choose another name." - getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)]) - getGroups = getGroups_ . Just - - getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)]) - getGroups_ search_ = - sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case - CRGroupsList {groups} -> pure $ Just groups - _ -> pure Nothing - - getDuplicateGroup :: GroupInfo -> IO (Maybe DuplicateGroup) - getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName, fullName}} = - getGroups fullName >>= mapM duplicateGroup + getDuplicateGroup :: GroupInfo -> IO (Either String DuplicateGroup) + getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = + duplicateGroup <$$> getDuplicateGroupRegs cc user displayName where - sameGroup (GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}}, _) = - gId /= groupId && n == displayName && fn == fullName - duplicateGroup [] = pure DGUnique - duplicateGroup groups = do - let gs = filter sameGroup groups - if null gs - then pure DGUnique - else do - (lgs, rgs) <- atomically $ (,) <$> readTVar (listedGroups st) <*> readTVar (reservedGroups st) - let reserved = any (\(GroupInfo {groupId = gId}, _) -> gId `S.member` lgs || gId `S.member` rgs) gs - pure $ if reserved then DGReserved else DGRegistered + duplicateGroup [] = DGUnique + duplicateGroup ((GroupInfo {groupId = gId, membership}, GroupReg {groupRegStatus = status}) : groups) + | gId == groupId || memberRemoved membership = duplicateGroup groups + | otherwise = case grDirectoryStatus status of + DSListed -> DGReserved + DSReserved -> DGReserved + DSRegistered -> case duplicateGroup groups of + DGReserved -> DGReserved + _ -> DGRegistered + DSRemoved -> duplicateGroup groups - processInvitation :: Contact -> GroupInfo -> IO () - processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do - void $ addGroupReg st ct g GRSProposed - r <- sendChatCmd cc $ APIJoinGroup groupId MFNone - sendMessage cc ct $ case r of - CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" - _ -> "Error joining group " <> displayName <> ", please re-send the invitation!" + processInvitation :: Contact -> GroupInfo -> Maybe GroupReg -> IO () + processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = \case + Nothing -> addGroupReg notifyAdminUsers st cc ct g GRSProposed joinGroup + Just _gr -> setGroupStatus notifyAdminUsers st env cc groupId GRSProposed joinGroup + where + joinGroup _ = do + r <- sendChatCmd cc $ APIJoinGroup groupId MFNone + sendMessage cc ct $ case r of + Right CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" + _ -> "Error joining group " <> displayName <> ", please re-send the invitation!" deContactConnected :: Contact -> IO () deContactConnected ct = when (contactDirect ct) $ do logInfo $ (viewContactName ct) <> " connected" sendMessage cc ct $ - ("Welcome to " <> serviceName <> " service!\n") - <> "Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ - \For example, send _privacy_ to find groups about privacy.\n\ - \Or send */all* or */new* to list groups.\n\n\ - \Content and privacy policy: https://simplex.chat/docs/directory.html" + ("Welcome to " <> serviceName <> "!\n\n") + <> "🔍 Send search string to find groups - try _security_.\n\ + \/help - how to submit your group.\n\ + \/new - recent groups.\n\n\ + \[Directory rules](https://simplex.chat/docs/directory.html)." deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO () - deGroupInvitation ct g@GroupInfo {groupProfile = GroupProfile {displayName, fullName}} fromMemberRole memberRole = do + deGroupInvitation ct g@GroupInfo {groupProfile = p@GroupProfile {displayName}} fromMemberRole memberRole = do logInfo $ "invited to group " <> viewGroupName g <> " by " <> viewContactName ct case badRolesMsg $ groupRolesStatus fromMemberRole memberRole of Just msg -> sendMessage cc ct msg Nothing -> getDuplicateGroup g >>= \case - Just DGUnique -> processInvitation ct g - Just DGRegistered -> askConfirmation - Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g - Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." + Right DGUnique -> processInvitation ct g Nothing + Right DGRegistered -> askConfirmation + Right DGReserved -> sendMessage cc ct $ groupAlreadyListed g + Left e -> sendMessage cc ct $ "Error: getDuplicateGroup. Please notify the developers.\n" <> T.pack e where - askConfirmation = do - ugrId <- addGroupReg st ct g GRSPendingConfirmation - sendMessage cc ct $ "The group " <> displayName <> " (" <> fullName <> ") is already submitted to the directory.\nTo confirm the registration, please send:" - sendMessage cc ct $ "/confirm " <> tshow ugrId <> ":" <> viewName displayName + askConfirmation = + addGroupReg notifyAdminUsers st cc ct g GRSPendingConfirmation $ \GroupReg {userGroupRegId} -> do + sendMessage cc ct $ "The group " <> groupNameDescr p <> " is already submitted to the directory.\nTo confirm the registration, please send:" + sendMessage cc ct $ "/confirm " <> tshow userGroupRegId <> ":" <> viewName displayName badRolesMsg :: GroupRolesStatus -> Maybe Text badRolesMsg = \case @@ -225,9 +391,9 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own GRSContactNotOwner -> Just "You must have a group *owner* role to register the group" GRSBadRoles -> Just "You must have a group *owner* role and you must grant directory service *admin* role to register the group" - getGroupRolesStatus :: GroupInfo -> GroupReg -> IO (Maybe GroupRolesStatus) - getGroupRolesStatus GroupInfo {membership = GroupMember {memberRole = serviceRole}} gr = - rStatus <$$> getGroupMember gr + getGroupRolesStatus :: GroupInfo -> GroupReg -> IO (Either String GroupRolesStatus) + getGroupRolesStatus GroupInfo {groupId, membership = GroupMember {memberRole = serviceRole}} gr = + rStatus <$$> getOwnerGroupMember groupId gr where rStatus GroupMember {memberRole} = groupRolesStatus memberRole serviceRole @@ -238,156 +404,263 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own (GROwner, _) -> GRSServiceNotAdmin _ -> GRSBadRoles - getGroupMember :: GroupReg -> IO (Maybe GroupMember) - getGroupMember GroupReg {dbGroupId, dbOwnerMemberId} = - readTVarIO dbOwnerMemberId - $>>= \mId -> resp <$> sendChatCmd cc (APIGroupMemberInfo dbGroupId mId) - where - resp = \case - CRGroupMemberInfo {member} -> Just member - _ -> Nothing + getOwnerGroupMember :: GroupId -> GroupReg -> IO (Either String GroupMember) + getOwnerGroupMember gId GroupReg {dbOwnerMemberId} = case dbOwnerMemberId of + Just mId -> withDB "getGroupMember" cc $ \db -> withExceptT show $ getGroupMember db (vr cc) user gId mId + Nothing -> pure $ Left "no owner member in group registration" deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO () - deServiceJoinedGroup ctId g owner = do + deServiceJoinedGroup ctId g@GroupInfo {groupId} owner = do logInfo $ "service joined group " <> viewGroupName g withGroupReg g "joined group" $ \gr -> when (ctId `isOwner` gr) $ do - setGroupRegOwner st gr owner - let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g - notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" - sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case - CRGroupLinkCreated {connReqContact} -> do - setGroupStatus st gr GRSPendingUpdate - notifyOwner - gr - "Created the public link to join the group via this directory service that is always online.\n\n\ - \Please add it to the group welcome message.\n\ - \For example, add:" - notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact connReqContact) - CRChatCmdError _ (ChatError e) -> case e of - CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." - CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." - CEGroupNotJoined _ -> notifyOwner gr $ unexpectedError "group not joined" - CEGroupMemberNotActive -> notifyOwner gr $ unexpectedError "service membership is not active" - _ -> notifyOwner gr $ unexpectedError "can't create group link" - _ -> notifyOwner gr $ unexpectedError "can't create group link" + let GroupInfo {groupProfile = GroupProfile {displayName}} = g + setGroupRegOwner cc groupId owner >>= \case + Left e -> do + let msg = "Error updating group " <> tshow groupId <> " owner: " <> T.pack e + logError msg + notifyOwner gr msg + Right () -> do + logGUpdateOwner st groupId $ groupMemberId' owner + notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" + sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case + Right CRGroupLinkCreated {groupLink = GroupLink {connLinkContact = gLink}} -> + setGroupStatus notifyAdminUsers st env cc groupId GRSPendingUpdate $ \gr' -> do + notifyOwner + gr' + "Created the public link to join the group via this directory service that is always online.\n\n\ + \Please add it to the group welcome message.\n\ + \For example, add:" + notifyOwner gr' $ "Link to join the group " <> displayName <> ": " <> groupLinkText gLink + Left (ChatError e) -> case e of + CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." + CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." + CEGroupNotJoined _ -> notifyOwner gr $ unexpectedError "group not joined" + CEGroupMemberNotActive -> notifyOwner gr $ unexpectedError "service membership is not active" + _ -> notifyOwner gr $ unexpectedError "can't create group link" + _ -> notifyOwner gr $ unexpectedError "can't create group link" - deGroupUpdated :: ContactId -> GroupInfo -> GroupInfo -> IO () - deGroupUpdated ctId fromGroup toGroup = do + deGroupUpdated :: GroupMember -> GroupInfo -> GroupInfo -> IO () + deGroupUpdated m@GroupMember {memberProfile = LocalProfile {displayName = mName}} fromGroup toGroup = do logInfo $ "group updated " <> viewGroupName toGroup unless (sameProfile p p') $ do - withGroupReg toGroup "group updated" $ \gr -> do + withGroupReg toGroup "group updated" $ \gr@GroupReg {groupRegStatus} -> do let userGroupRef = userGroupReference gr toGroup - readTVarIO (groupRegStatus gr) >>= \case + byMember = case memberContactId m of + Just ctId | ctId `isOwner` gr -> "" -- group registration owner, not any group owner. + _ -> " by " <> mName -- owner notification from directory will include the name. + case groupRegStatus of GRSPendingConfirmation -> pure () GRSProposed -> pure () GRSPendingUpdate -> groupProfileUpdate >>= \case GPNoServiceLink -> - when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message." - GPServiceLinkAdded - | ctId `isOwner` gr -> groupLinkAdded gr - | otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself." - GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it." - GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr + notifyOwner gr $ "The profile updated for " <> userGroupRef <> byMember <> ", but the group link is not added to the welcome message." + GPServiceLinkAdded _ -> groupLinkAdded gr byMember + GPServiceLinkRemoved -> + notifyOwner gr $ + "The group link of " <> userGroupRef <> " is removed from the welcome message" <> byMember <> ", please add it." + GPHasServiceLink {} -> groupLinkAdded gr byMember GPServiceLinkError -> do - when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers." + notifyOwner gr $ + ("Error: " <> serviceName <> " has no group link for " <> userGroupRef) + <> " after profile was updated" + <> byMember + <> ". Please report the error to the developers." logError $ "Error: no group link for " <> userGroupRef - GRSPendingApproval n -> processProfileChange gr $ n + 1 - GRSActive -> processProfileChange gr 1 - GRSSuspended -> processProfileChange gr 1 - GRSSuspendedBadRoles -> processProfileChange gr 1 + GRSPendingApproval n -> processProfileChange gr byMember False $ n + 1 + GRSActive -> processProfileChange gr byMember True 1 + GRSSuspended -> processProfileChange gr byMember False 1 + GRSSuspendedBadRoles -> processProfileChange gr byMember False 1 GRSRemoved -> pure () where - isInfix l d_ = l `T.isInfixOf` fromMaybe "" d_ GroupInfo {groupId, groupProfile = p} = fromGroup GroupInfo {groupProfile = p'} = toGroup sameProfile - GroupProfile {displayName = n, fullName = fn, image = i, description = d} - GroupProfile {displayName = n', fullName = fn', image = i', description = d'} = - n == n' && fn == fn' && i == i' && d == d' - groupLinkAdded gr = do + GroupProfile {displayName = n, fullName = fn, shortDescr = sd, image = i, description = d} + GroupProfile {displayName = n', fullName = fn', shortDescr = sd', image = i', description = d'} = + n == n' && fn == fn' && i == i' && sd == sd' && (T.words <$> d) == (T.words <$> d') + groupLinkAdded gr byMember = getDuplicateGroup toGroup >>= \case - Nothing -> notifyOwner gr "Error: getDuplicateGroup. Please notify the developers." - Just DGReserved -> notifyOwner gr $ groupAlreadyListed toGroup - _ -> do - let gaId = 1 - setGroupStatus st gr $ GRSPendingApproval gaId - notifyOwner gr $ "Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message.\nYou will be notified once the group is added to the directory - it may take up to 48 hours." - checkRolesSendToApprove gr gaId - processProfileChange gr n' = do - setGroupStatus st gr GRSPendingUpdate + Left e -> notifyOwner gr $ "Error: getDuplicateGroup. Please notify the developers.\n" <> T.pack e + Right DGReserved -> notifyOwner gr $ groupAlreadyListed toGroup + _ -> setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval gaId) $ \gr' -> do + notifyOwner gr' $ + ("Thank you! The group link for " <> userGroupReference gr' toGroup <> " is added to the welcome message" <> byMember) + <> ".\nYou will be notified once the group is added to the directory - it may take up to 48 hours." + checkRolesSendToApprove gr' gaId + where + gaId = 1 + processProfileChange gr byMember isActive n' = do let userGroupRef = userGroupReference gr toGroup groupRef = groupReference toGroup groupProfileUpdate >>= \case - GPNoServiceLink -> do - notifyOwner gr $ "The group profile is updated " <> userGroupRef <> ", but no link is added to the welcome message.\n\nThe group will remain hidden from the directory until the group link is added and the group is re-approved." - GPServiceLinkRemoved -> do - notifyOwner gr $ "The group link for " <> userGroupRef <> " is removed from the welcome message.\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." + GPNoServiceLink -> setGroupStatus notifyAdminUsers st env cc groupId GRSPendingUpdate $ \gr' -> do + notifyOwner gr' $ + ("The group profile is updated for " <> userGroupRef <> byMember <> ", but no link is added to the welcome message.\n\n") + <> "The group will remain hidden from the directory until the group link is added and the group is re-approved." + GPServiceLinkRemoved -> setGroupStatus notifyAdminUsers st env cc groupId GRSPendingUpdate $ \gr' -> do + notifyOwner gr' $ + ("The group link for " <> userGroupRef <> " is removed from the welcome message" <> byMember) + <> ".\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." notifyAdminUsers $ "The group link is removed from " <> groupRef <> ", de-listed." - GPServiceLinkAdded -> do - setGroupStatus st gr $ GRSPendingApproval n' - notifyOwner gr $ "The group link is added to " <> userGroupRef <> "!\nIt is hidden from the directory until approved." - notifyAdminUsers $ "The group link is added to " <> groupRef <> "." - checkRolesSendToApprove gr n' - GPHasServiceLink -> do - setGroupStatus st gr $ GRSPendingApproval n' - notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved." - notifyAdminUsers $ "The group " <> groupRef <> " is updated." + GPServiceLinkAdded _ -> setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval n') $ \gr' -> do + notifyOwner gr' $ + ("The group link is added to " <> userGroupRef <> byMember) + <> "!\nIt is hidden from the directory until approved." + notifyAdminUsers $ "The group link is added to " <> groupRef <> byMember <> "." checkRolesSendToApprove gr n' + GPHasServiceLink {linkBefore, linkNow} + | isActive && onlyLinkChanged p p' -> do + notifyOwner gr $ + ("The group " <> userGroupRef <> " is updated" <> byMember) + <> "!\nThe group is listed in directory." + notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> " - only link or whitespace changes.\nThe group remained listed in directory." + | otherwise -> setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval n') $ \gr' -> do + notifyOwner gr' $ + ("The group " <> userGroupRef <> " is updated" <> byMember) + <> "!\nIt is hidden from the directory until approved." + notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> "." + checkRolesSendToApprove gr' n' + where + onlyLinkChanged + GroupProfile {displayName = dn, fullName = fn, shortDescr = sd, image = i, description = d} + GroupProfile {displayName = dn', fullName = fn', shortDescr = sd', image = i', description = d'} = + dn == dn' && fn == fn' && i == i' && sd == sd' && (T.words . T.replace linkBefore "" <$> d) == (T.words . T.replace linkNow "" <$> d') GPServiceLinkError -> logError $ "Error: no group link for " <> groupRef <> " pending approval." groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) where profileUpdate = \case - CRGroupLink {connReqContact} -> - let groupLink1 = strEncodeTxt connReqContact - groupLink2 = strEncodeTxt $ simplexChatContact connReqContact - hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p - hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p' - in if - | hadLinkBefore && hasLinkNow -> GPHasServiceLink - | hadLinkBefore -> GPServiceLinkRemoved - | hasLinkNow -> GPServiceLinkAdded - | otherwise -> GPNoServiceLink + Right CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink cr sl_}} -> + let linkBefore_ = profileGroupLinkText fromGroup + linkNow_ = profileGroupLinkText toGroup + profileGroupLinkText GroupInfo {groupProfile = gp} = + maybe Nothing (fmap (\(FormattedText _ t) -> t) . find ftHasLink) $ parseMaybeMarkdownList =<< description gp + ftHasLink = \case + FormattedText (Just SimplexLink {simplexUri = ACL SCMContact cLink}) _ -> case cLink of + CLFull cr' -> sameConnReqContact cr' cr + CLShort sl' -> maybe False (sameShortLinkContact sl') sl_ + _ -> False + in case (linkBefore_, linkNow_) of + (Just linkBefore, Just linkNow) -> GPHasServiceLink linkBefore linkNow + (Just _, Nothing) -> GPServiceLinkRemoved + (Nothing, Just linkNow) -> GPServiceLinkAdded linkNow + (Nothing, Nothing) -> GPNoServiceLink _ -> GPServiceLinkError checkRolesSendToApprove gr gaId = do (badRolesMsg <$$> getGroupRolesStatus toGroup gr) >>= \case - Nothing -> notifyOwner gr "Error: getGroupRolesStatus. Please notify the developers." - Just (Just msg) -> notifyOwner gr msg - Just Nothing -> sendToApprove toGroup gr gaId + Left e -> notifyOwner gr $ "Error: getGroupRolesStatus. Please notify the developers.\n" <> T.pack e + Right (Just msg) -> notifyOwner gr msg + Right Nothing -> sendToApprove toGroup gr gaId + + dePendingMember :: GroupInfo -> GroupMember -> IO () + dePendingMember g@GroupInfo {groupProfile = GroupProfile {displayName}} m + | memberRequiresCaptcha a m = sendMemberCaptcha g m Nothing captchaNotice 0 + | otherwise = approvePendingMember a g m + where + a = groupMemberAcceptance g + captchaNotice = "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." + + sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> IO () + sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts = do + s <- getCaptchaStr captchaLength "" + mc <- getCaptcha s + sentAt <- getCurrentTime + let captcha = PendingCaptcha {captchaText = T.pack s, sentAt, attempts = prevAttempts + 1} + atomically $ TM.insert gmId captcha $ pendingCaptchas env + sendCaptcha mc + where + getCaptcha s = case captchaGenerator opts of + Nothing -> pure textMsg + Just script -> content <$> readProcess script [s] "" + where + textMsg = MCText $ T.pack s + content r = case T.lines $ T.pack r of + [] -> textMsg + "" : _ -> textMsg + img : _ -> MCImage "" $ ImageData img + sendCaptcha mc = sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [(quotedId, MCText noticeText), (Nothing, mc)] + gmId = groupMemberId' m + + approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO () + approvePendingMember a g@GroupInfo {groupId} m@GroupMember {memberProfile = LocalProfile {displayName, image}} = do + gli_ <- join . eitherToMaybe <$> withDB' "getGroupLinkInfo" cc (\db -> getGroupLinkInfo db userId groupId) + let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_ + gmId = groupMemberId' m + sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case + Right CRMemberAccepted {member} -> do + atomically $ TM.delete gmId $ pendingCaptchas env + if memberStatus member == GSMemPendingReview + then logInfo $ "Member " <> viewName displayName <> " accepted and pending review, group " <> tshow groupId <> ":" <> viewGroupName g + else logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g + r -> logError $ "unexpected accept member response: " <> tshow r + + dePendingMemberMsg :: GroupInfo -> GroupMember -> ChatItemId -> Text -> IO () + dePendingMemberMsg g@GroupInfo {groupId, groupProfile = GroupProfile {displayName = n}} m@GroupMember {memberProfile = LocalProfile {displayName}} ciId msgText + | memberRequiresCaptcha a m = do + ts <- getCurrentTime + atomically (TM.lookup (groupMemberId' m) $ pendingCaptchas env) >>= \case + Just PendingCaptcha {captchaText, sentAt, attempts} + | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired $ attempts - 1 + | matchCaptchaStr captchaText msgText -> do + sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just $ groupMemberId' m)) [(Just ciId, MCText $ "Correct, you joined the group " <> n)] + approvePendingMember a g m + | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts + | otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts + Nothing -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 + | otherwise = approvePendingMember a g m + where + a = groupMemberAcceptance g + rejectPendingMember rjctNotice = do + let gmId = groupMemberId' m + sendComposedMessages cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [MCText rjctNotice] + sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case + Right (CRUserDeletedMembers _ _ (_ : _) _) -> do + atomically $ TM.delete gmId $ pendingCaptchas env + logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g + r -> logError $ "unexpected remove member response: " <> tshow r + captchaExpired = "Captcha expired, please try again." + wrongCaptcha attempts + | attempts == maxCaptchaAttempts - 1 = "Incorrect text, please try again - this is your last attempt." + | otherwise = "Incorrect text, please try again." + noCaptcha = "Unexpected message, please try again." + tooManyAttempts = "Too many failed attempts, you can't join group." + + memberRequiresCaptcha :: DirectoryMemberAcceptance -> GroupMember -> Bool + memberRequiresCaptcha a GroupMember {memberProfile = LocalProfile {image}} = + useMemberFilter image $ passCaptcha a sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () - sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do - ct_ <- getContact cc dbContactId - gr_ <- getGroupAndSummary cc dbGroupId - let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_ + sendToApprove GroupInfo {groupId, groupProfile = p@GroupProfile {displayName, image = image'}, groupSummary} GroupReg {dbContactId, promoted} gaId = do + ct_ <- getContact' cc user dbContactId + let membersStr = "_" <> tshow (currentMembers groupSummary) <> " members_\n" text = - maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ + either (\_ -> "The group ID " <> tshow groupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow groupId <> ": ") ct_ <> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:") msg = maybe (MCText text) (\image -> MCImage {text, image}) image' withAdminUsers $ \cId -> do sendComposedMessage' cc cId Nothing msg - sendMessage' cc cId $ "/approve " <> tshow dbGroupId <> ":" <> viewName displayName <> " " <> tshow gaId + sendMessage' cc cId $ "/approve " <> tshow groupId <> ":" <> viewName displayName <> " " <> tshow gaId <> if promoted then " promote=on" else "" deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () - deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do + deContactRoleChanged g@GroupInfo {groupId, membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do logInfo $ "contact ID " <> tshow ctId <> " role changed in group " <> viewGroupName g <> " to " <> tshow contactRole - withGroupReg g "contact role changed" $ \gr -> do + withGroupReg g "contact role changed" $ \gr@GroupReg {groupRegStatus} -> do let userGroupRef = userGroupReference gr g uCtRole = "Your role in the group " <> userGroupRef <> " is changed to " <> ctRole - when (ctId `isOwner` gr) $ do - readTVarIO (groupRegStatus gr) >>= \case - GRSSuspendedBadRoles -> when (rStatus == GRSOk) $ do - setGroupStatus st gr GRSActive - notifyOwner gr $ uCtRole <> ".\n\nThe group is listed in the directory again." - notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suCtRole - GRSPendingApproval gaId -> when (rStatus == GRSOk) $ do + when (ctId `isOwner` gr) $ + case groupRegStatus of + GRSSuspendedBadRoles | rStatus == GRSOk -> + setGroupStatus notifyAdminUsers st env cc groupId GRSActive $ \gr' -> do + notifyOwner gr' $ uCtRole <> ".\n\nThe group is listed in the directory again." + notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suCtRole + GRSPendingApproval gaId | rStatus == GRSOk -> do sendToApprove g gr gaId notifyOwner gr $ uCtRole <> ".\n\nThe group is submitted for approval." - GRSActive -> when (rStatus /= GRSOk) $ do - setGroupStatus st gr GRSSuspendedBadRoles - notifyOwner gr $ uCtRole <> ".\n\nThe group is no longer listed in the directory." - notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole + GRSActive | rStatus /= GRSOk -> + setGroupStatus notifyAdminUsers st env cc groupId GRSSuspendedBadRoles $ \gr' -> do + notifyOwner gr' $ uCtRole <> ".\n\nThe group is no longer listed in the directory." + notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole _ -> pure () where rStatus = groupRolesStatus contactRole serviceRole @@ -396,212 +669,296 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own suCtRole = "(user role is set to " <> ctRole <> ")." deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () - deServiceRoleChanged g serviceRole = do + deServiceRoleChanged g@GroupInfo {groupId} serviceRole = do logInfo $ "service role changed in group " <> viewGroupName g <> " to " <> tshow serviceRole - withGroupReg g "service role changed" $ \gr -> do + withGroupReg g "service role changed" $ \gr@GroupReg {groupRegStatus} -> do let userGroupRef = userGroupReference gr g uSrvRole = serviceName <> " role in the group " <> userGroupRef <> " is changed to " <> srvRole - readTVarIO (groupRegStatus gr) >>= \case - GRSSuspendedBadRoles -> when (serviceRole == GRAdmin) $ - whenContactIsOwner gr $ do - setGroupStatus st gr GRSActive - notifyOwner gr $ uSrvRole <> ".\n\nThe group is listed in the directory again." - notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole - GRSPendingApproval gaId -> when (serviceRole == GRAdmin) $ + case groupRegStatus of + GRSSuspendedBadRoles | serviceRole == GRAdmin -> + whenContactIsOwner gr $ + setGroupStatus notifyAdminUsers st env cc groupId GRSActive $ \gr' -> do + notifyOwner gr' $ uSrvRole <> ".\n\nThe group is listed in the directory again." + notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole + GRSPendingApproval gaId | serviceRole == GRAdmin -> whenContactIsOwner gr $ do sendToApprove g gr gaId notifyOwner gr $ uSrvRole <> ".\n\nThe group is submitted for approval." - GRSActive -> when (serviceRole /= GRAdmin) $ do - setGroupStatus st gr GRSSuspendedBadRoles - notifyOwner gr $ uSrvRole <> ".\n\nThe group is no longer listed in the directory." - notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole + GRSActive | serviceRole /= GRAdmin -> + setGroupStatus notifyAdminUsers st env cc groupId GRSSuspendedBadRoles $ \gr' -> do + notifyOwner gr' $ uSrvRole <> ".\n\nThe group is no longer listed in the directory." + notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole _ -> pure () where groupRef = groupReference g srvRole = "*" <> strEncodeTxt serviceRole <> "*" suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." whenContactIsOwner gr action = - getGroupMember gr + getOwnerGroupMember groupId gr >>= mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action) deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO () - deContactRemovedFromGroup ctId g = do + deContactRemovedFromGroup ctId g@GroupInfo {groupId} = do logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g withGroupReg g "contact removed" $ \gr -> do - when (ctId `isOwner` gr) $ do - setGroupStatus st gr GRSRemoved - notifyOwner gr $ "You are removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." + when (ctId `isOwner` gr) $ + setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \gr' -> do + notifyOwner gr' $ "You are removed from the group " <> userGroupReference gr' g <> ".\n\nThe group is no longer listed in the directory." + notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." deContactLeftGroup :: ContactId -> GroupInfo -> IO () - deContactLeftGroup ctId g = do + deContactLeftGroup ctId g@GroupInfo {groupId} = do logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g - withGroupReg g "contact left" $ \gr -> do - when (ctId `isOwner` gr) $ do - setGroupStatus st gr GRSRemoved - notifyOwner gr $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." + -- TODO combine + withGroupReg g "contact left" $ \gr -> + when (ctId `isOwner` gr) $ + setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \gr' -> do + notifyOwner gr' $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." + notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." deServiceRemovedFromGroup :: GroupInfo -> IO () - deServiceRemovedFromGroup g = do + deServiceRemovedFromGroup g@GroupInfo {groupId} = do logInfo $ "service removed from group " <> viewGroupName g - withGroupReg g "service removed" $ \gr -> do - setGroupStatus st gr GRSRemoved + setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \gr -> do notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." deGroupDeleted :: GroupInfo -> IO () - deGroupDeleted g = do + deGroupDeleted g@GroupInfo {groupId} = do logInfo $ "group removed " <> viewGroupName g - withGroupReg g "group removed" $ \gr -> do - setGroupStatus st gr GRSRemoved + setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \gr -> do notifyOwner gr $ "The group " <> userGroupReference gr g <> " is deleted.\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group is deleted)." deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () deUserCommand ct ciId = \case - DCHelp -> + DCHelp DHSRegistration -> sendMessage cc ct $ - "You must be the owner to add the group to the directory:\n\ - \1. Invite " + "You must be the group owner to add it to the directory:\n\n\ + \1️⃣ *Invite* " <> serviceName - <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\ - \2. " - <> serviceName - <> " bot will create a public group link for the new members to join even when you are offline.\n\ - \3. You will then need to add this link to the group welcome message.\n\ - \4. Once the link is added, service admins will approve the group (it can take up to 48 hours), and everybody will be able to find it in directory.\n\n\ - \Start from inviting the bot to your group as admin - it will guide you through the process" - DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s + <> " bot to your group as *admin* - it will create a link for new members to join.\n\ + \2️⃣ *Add* this link to the group's welcome message.\n\ + \3️⃣ We *review* your group. Once *approved*, anybody can find it.\n\n\ + \_We usually approve within a day, except holidays_. [More details](https://simplex.chat/docs/directory.html#adding-groups-to-the-directory)." + DCHelp DHSCommands -> + sendMessage cc ct $ + "/'help commands' - receive this help message.\n\ + \/help - how to register your group to be added to directory.\n\ + \/list - list the groups you registered.\n\ + \`/role ` - view and set default member role for your group.\n\ + \`/filter ` - view and set spam filter settings for group.\n\ + \`/link ` - view and upgrade group link.\n\ + \`/delete :` - remove the group you submitted from directory, with _ID_ and _name_ as shown by /list command.\n\n\ + \To search for groups, send the search text." + DCSearchGroup s -> + sendFoundListedGroups (STSearch s) Nothing "No groups found" $ \gs n -> -- $ sendSearchResults s + let more = if n > length gs then ", sending top " <> tshow (length gs) else "" + in "Found " <> tshow n <> " group(s)" <> more <> "." DCSearchNext -> atomically (TM.lookup (contactId' ct) searchRequests) >>= \case - Just search@SearchRequest {searchType, searchTime} -> do + Just SearchRequest {searchType, searchTime, lastGroup} -> do currentTime <- getCurrentTime if diffUTCTime currentTime searchTime > 300 -- 5 minutes then do atomically $ TM.delete (contactId' ct) searchRequests showAllGroups - else case searchType of - STSearch s -> withFoundListedGroups (Just s) $ sendNextSearchResults takeTop search - STAll -> withFoundListedGroups Nothing $ sendNextSearchResults takeTop search - STRecent -> withFoundListedGroups Nothing $ sendNextSearchResults takeRecent search + else + sendFoundListedGroups searchType (Just lastGroup) "No more groups" $ \gs _ -> + "Sending " <> tshow (length gs) <> " more group(s)." Nothing -> showAllGroups where showAllGroups = deUserCommand ct ciId DCAllGroups - DCAllGroups -> withFoundListedGroups Nothing $ sendAllGroups takeTop "top" STAll - DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent + DCAllGroups -> sendFoundListedGroups STAll Nothing "No groups listed" $ allGroupsReply "top" + DCRecentGroups -> sendFoundListedGroups STRecent Nothing "No groups listed" $ allGroupsReply "the most recent" DCSubmitGroup _link -> pure () DCConfirmDuplicateGroup ugrId gName -> - withUserGroupReg ugrId gName $ \g@GroupInfo {groupProfile = GroupProfile {displayName}} gr -> - readTVarIO (groupRegStatus gr) >>= \case - GRSPendingConfirmation -> - getDuplicateGroup g >>= \case - Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." - Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g - _ -> processInvitation ct g - _ -> sendReply $ "Error: the group ID " <> tshow ugrId <> " (" <> displayName <> ") is not pending confirmation." + withUserGroupReg ugrId gName $ \g@GroupInfo {groupProfile = GroupProfile {displayName}} gr@GroupReg {groupRegStatus} -> case groupRegStatus of + GRSPendingConfirmation -> + getDuplicateGroup g >>= \case + Left e -> sendMessage cc ct $ "Error: getDuplicateGroup. Please notify the developers.\n" <> T.pack e + Right DGReserved -> sendMessage cc ct $ groupAlreadyListed g + _ -> processInvitation ct g $ Just gr + _ -> sendReply $ "Error: the group ID " <> tshow ugrId <> " (" <> displayName <> ") is not pending confirmation." DCListUserGroups -> - atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do - sendReply $ tshow (length grs) <> " registered group(s)" - void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {userGroupRegId} -> - sendGroupInfo ct gr userGroupRegId Nothing - DCDeleteGroup ugrId gName -> - withUserGroupReg ugrId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} gr -> do - delGroupReg st gr - sendReply $ "Your group " <> displayName <> " is deleted from the directory" - DCSetRole gId gName mRole -> - (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ - \GroupInfo {groupId, groupProfile = GroupProfile {displayName}} _gr -> do - gLink_ <- setGroupLinkRole cc groupId mRole - sendReply $ case gLink_ of - Nothing -> "Error: the initial member role for the group " <> displayName <> " was NOT upgated" - Just gLink -> - ("The initial member role for the group " <> displayName <> " is set to *" <> strEncodeTxt mRole <> "*\n\n") - <> ("*Please note*: it applies only to members joining via this link: " <> strEncodeTxt (simplexChatContact gLink)) + getUserGroupRegs cc user (contactId' ct) >>= \case + Left e -> sendReply $ "Error reading groups: " <> T.pack e + Right gs -> sendGroupsInfo ct ciId isAdmin (gs, length gs) + DCDeleteGroup gId gName -> + (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} GroupReg {dbGroupId} -> do + delGroupReg cc dbGroupId >>= \case + Right () -> do + logGDelete st dbGroupId + sendReply $ (if isAdmin then "The group " else "Your group ") <> displayName <> " is deleted from the directory" + Left e -> sendReply $ "Error deleting group " <> displayName <> ": " <> T.pack e + DCMemberRole gId gName_ mRole_ -> + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do + let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g + case mRole_ of + Nothing -> + getGroupLink' cc user g >>= \case + Right GroupLink {connLinkContact = gLink, acceptMemberRole} -> do + let anotherRole = case acceptMemberRole of GRObserver -> GRMember; _ -> GRObserver + sendReply $ + initialRole n acceptMemberRole + <> ("Send /'role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "' to change it.\n\n") + <> onlyViaLink gLink + Left _ -> sendReply $ "Error: failed reading the initial member role for the group " <> n + Just mRole -> do + setGroupLinkRole cc g mRole >>= \case + Just gLink -> sendReply $ initialRole n mRole <> "\n" <> onlyViaLink gLink + Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated." + where + initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n" + onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> groupLinkText gLink + DCGroupFilter gId gName_ acceptance_ -> + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do + let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g + a = groupMemberAcceptance g + case acceptance_ of + Just a' | a /= a' -> do + let d = toCustomData $ DirectoryGroupData a' + withDB' "setGroupCustomData" cc (\db -> setGroupCustomData db user g $ Just d) >>= \case + Right () -> sendSettigns n a' " set to" + Left e -> sendReply $ "Error changing spam filter settings for group " <> n <> ": " <> T.pack e + _ -> sendSettigns n a "" + where + sendSettigns n a setTo = + sendReply $ + T.unlines $ + [ "Spam filter settings for group " <> n <> setTo <> ":", + "- reject long/inappropriate names: " <> showCondition (rejectNames a), + "- pass captcha to join: " <> showCondition (passCaptcha a), + -- "- make observer: " <> showCondition (makeObserver a) <> (if isJust (makeObserver a) then "" else " (use default set with /role command)"), + "" + -- "Use */filter " <> tshow gId <> " * to change spam filter level: no (disable), basic, moderate, strong.", + -- "Or use */filter " <> tshow gId <> " [name[=noimage]] [captcha[=noimage]] [observer[=noimage]]* for advanced filter configuration." + ] + <> ["/'filter " <> tshow gId <> " name' - enable name filter" | isNothing (rejectNames a)] + <> ["/'filter " <> tshow gId <> " captcha' - enable captcha challenge" | isNothing (passCaptcha a)] + <> ["/'filter " <> tshow gId <> " name captcha' - enable both" | isNothing (rejectNames a) || isNothing (passCaptcha a)] + <> ["/'filter " <> tshow gId <> " off' - disable filter" | isJust (rejectNames a) || isJust (passCaptcha a)] + showCondition = \case + Nothing -> "_disabled_" + Just PCAll -> "_enabled_" + Just PCNoImage -> "_enabled for profiles without image_" + DCShowUpgradeGroupLink gId gName_ -> + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \GroupInfo {groupId, localDisplayName = gName} _ -> do + let groupRef = groupReference' gId gName + withGroupLinkResult groupRef (sendChatCmd cc $ APIGetGroupLink groupId) $ + \GroupLink {connLinkContact = gLink@(CCLink _ sLnk_), acceptMemberRole, shortLinkDataSet, shortLinkLargeDataSet = BoolDef slLargeDataSet} -> do + let shouldBeUpgraded = isNothing sLnk_ || not shortLinkDataSet || not slLargeDataSet + sendReply $ + T.unlines $ + [ "The link to join the group " <> groupRef <> ":", + groupLinkText gLink, + "New member role: " <> strEncodeTxt acceptMemberRole + ] + <> ["The link is being upgraded..." | shouldBeUpgraded] + when shouldBeUpgraded $ do + let send = sendComposedMessage cc ct Nothing . MCText . T.unlines + withGroupLinkResult groupRef (sendChatCmd cc $ APIAddGroupShortLink groupId) $ + \GroupLink {connLinkContact = CCLink _ sLnk_'} -> case (sLnk_, sLnk_') of + (Just _, Just _) -> + send ["The group link is upgraded for: " <> groupRef, "No changes to group needed."] + (Nothing, Just sLnk) -> + sendComposedMessages + cc + (SRDirect $ contactId' ct) + [ MCText $ + T.unlines + [ "Please replace the old link in welcome message of your group " <> groupRef, + "If this is the only change, the group will remain listed in directory without re-approval.", + "", + "The new link:" + ], + MCText $ strEncodeTxt sLnk + ] + (_, Nothing) -> + send ["The short link is not created for " <> groupRef, "Please report it to the developers."] + where + withGroupLinkResult groupRef a cb = + a >>= \case + Right CRGroupLink {groupLink} -> cb groupLink + Left (ChatErrorStore (SEGroupLinkNotFound _)) -> + sendReply $ "The group " <> groupRef <> " has no public link." + Right r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + let resp = T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r + sendReply $ "Unexpected error:\n" <> resp + Left e -> do + let resp = T.pack $ serializeChatError True (config cc) e + sendReply $ "Unexpected error:\n" <> resp DCUnknownCommand -> sendReply "Unknown command" DCCommandError tag -> sendReply $ "Command error: " <> tshow tag where knownCt = knownContact ct isAdmin = knownCt `elem` adminUsers || knownCt `elem` superUsers - withUserGroupReg ugrId gName action = - atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case - Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found" - Just gr@GroupReg {dbGroupId} -> do - getGroup cc dbGroupId >>= \case - Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found" - Just g@GroupInfo {groupProfile = GroupProfile {displayName}} - | displayName == gName -> action g gr - | otherwise -> sendReply $ "Group ID " <> tshow ugrId <> " has the display name " <> displayName + withUserGroupReg ugrId = withUserGroupReg_ ugrId . Just + withUserGroupReg_ ugrId gName_ action = + getUserGroupReg cc user (contactId' ct) ugrId >>= \case + -- TODO differentiate group not found error + Left e -> sendReply $ "Group ID " <> tshow ugrId <> " error:" <> T.pack e + Right (g@GroupInfo {groupProfile = GroupProfile {displayName}}, gr) + | maybe True (displayName ==) gName_ -> action g gr + | otherwise -> sendReply $ "Group ID " <> tshow ugrId <> " has the display name " <> displayName sendReply = mkSendReply ct ciId - withFoundListedGroups s_ action = - getGroups_ s_ >>= \case - Just groups -> atomically (filterListedGroups st groups) >>= action - Nothing -> sendReply "Error: getGroups. Please notify the developers." - sendSearchResults s = \case - [] -> sendReply "No groups found" - gs -> do - let gs' = takeTop searchResults gs - moreGroups = length gs - length gs' - more = if moreGroups > 0 then ", sending top " <> tshow (length gs') else "" - sendReply $ "Found " <> tshow (length gs) <> " group(s)" <> more <> "." - updateSearchRequest (STSearch s) $ groupIds gs' - sendFoundGroups gs' moreGroups - sendAllGroups takeFirst sortName searchType = \case - [] -> sendReply "No groups listed" - gs -> do - let gs' = takeFirst searchResults gs - moreGroups = length gs - length gs' - more = if moreGroups > 0 then ", sending " <> sortName <> " " <> tshow (length gs') else "" - sendReply $ tshow (length gs) <> " group(s) listed" <> more <> "." - updateSearchRequest searchType $ groupIds gs' - sendFoundGroups gs' moreGroups - sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case - [] -> do - sendReply "Sorry, no more groups" - atomically $ TM.delete (contactId' ct) searchRequests - gs -> do - let gs' = takeFirst searchResults $ filterNotSent sentGroups gs - sentGroups' = sentGroups <> groupIds gs' - moreGroups = length gs - S.size sentGroups' - sendReply $ "Sending " <> tshow (length gs') <> " more group(s)." - updateSearchRequest searchType sentGroups' - sendFoundGroups gs' moreGroups - updateSearchRequest :: SearchType -> Set GroupId -> IO () - updateSearchRequest searchType sentGroups = do + sendFoundListedGroups searchType lastGroup_ notFound replyStr = + searchListedGroups cc user searchType lastGroup_ searchResults >>= \case + Right ([], _) -> do + atomically $ TM.delete (contactId' ct) searchRequests + sendReply notFound + Right (gs, n) -> do + let moreGroups = n - length gs + updateSearchRequest searchType $ last gs + sendFoundGroups (replyStr gs n) gs moreGroups + Left e -> sendReply $ "Error: searchListedGroups. Please notify the developers.\n" <> T.pack e + allGroupsReply sortName gs n = + let more = if n > length gs then ", sending " <> sortName <> " " <> tshow (length gs) else "" + in tshow n <> " group(s) listed" <> more <> "." + updateSearchRequest :: SearchType -> (GroupInfo, GroupReg) -> IO () + updateSearchRequest searchType (GroupInfo {groupId}, _) = do searchTime <- getCurrentTime - let search = SearchRequest {searchType, searchTime, sentGroups} + let search = SearchRequest {searchType, searchTime, lastGroup = groupId} atomically $ TM.insert (contactId' ct) search searchRequests - sendFoundGroups gs moreGroups = - void . forkIO $ do - forM_ gs $ - \(GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do - let membersStr = "_" <> tshow currentMembers <> " members_" - showId = if isAdmin then tshow groupId <> ". " else "" - text = showId <> groupInfoText p <> "\n" <> membersStr - msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ - sendComposedMessage cc ct Nothing msg - when (moreGroups > 0) $ - sendComposedMessage cc ct Nothing $ - MCText $ - "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." + sendFoundGroups reply gs moreGroups = + void . forkIO $ sendComposedMessages_ cc (SRDirect $ contactId' ct) msgs + where + msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0] + replyMsg = (Just ciId, MCText reply) + foundGroup (GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}, groupSummary = GroupSummary {currentMembers}}, _) = + let membersStr = "_" <> tshow currentMembers <> " members_" + showId = if isAdmin then tshow groupId <> ". " else "" + text = showId <> groupInfoText p <> "\n" <> membersStr + in (Nothing, maybe (MCText text) (\image -> MCImage {text, image}) image_) + moreMsg = (Nothing, MCText $ "Send /next for " <> tshow moreGroups <> " more result(s).") deAdminCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRAdmin -> IO () deAdminCommand ct ciId cmd | knownCt `elem` adminUsers || knownCt `elem` superUsers = case cmd of - DCApproveGroup {groupId, displayName = n, groupApprovalId} -> - withGroupAndReg sendReply groupId n $ \g gr -> - readTVarIO (groupRegStatus gr) >>= \case + DCApproveGroup {groupId, displayName = n, groupApprovalId, promote} -> + withGroupAndReg sendReply groupId n $ \g gr@GroupReg {userGroupRegId = ugrId, promoted} -> + case groupRegStatus gr of GRSPendingApproval gaId - | gaId == groupApprovalId -> do + | gaId == groupApprovalId -> getDuplicateGroup g >>= \case - Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." - Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." - _ -> do - getGroupRolesStatus g gr >>= \case - Just GRSOk -> do - setGroupStatus st gr GRSActive + Left e -> sendReply $ "Error: getDuplicateGroup. Please notify the developers.\n" <> T.pack e + Right DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." + _ -> getGroupRolesStatus g gr >>= \case + Right GRSOk -> do + let grPromoted' + | promoted || knownCt `elem` superUsers = fromMaybe promoted promote + | otherwise = False + setGroupStatusPromo sendReply st env cc gr GRSActive grPromoted' $ do let approved = "The group " <> userGroupReference' gr n <> " is approved" - notifyOwner gr $ approved <> " and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." + notifyOwner gr $ + (approved <> " and listed in directory - please moderate it!\n") + <> "_Please note_: if you change the group profile it will be hidden from directory until it is re-approved.\n\n" + <> "Supported commands:\n" + <> ("/'filter " <> tshow ugrId <> "' - to configure anti-spam filter.\n") + <> ("/'role " <> tshow ugrId <> "' - to set default member role.\n") + <> ("/'link " <> tshow ugrId <> "' - to view/upgrade group link.") invited <- forM ownersGroup $ \og@KnownGroup {localDisplayName = ogName} -> do inviteToOwnersGroup og gr $ \case @@ -609,12 +966,12 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own owner <- groupOwnerInfo groupRef $ dbContactId gr pure $ "Invited " <> owner <> " to owners' group " <> viewName ogName Left err -> pure err - sendReply $ "Group approved!" <> maybe "" ("\n" <>) invited - notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct) <> fromMaybe "" invited - Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin - Just GRSContactNotOwner -> replyNotApproved "user is not an owner." - Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin - Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." + sendReply $ "Group approved" <> (if grPromoted' then " (promoted)" else "") <> "!" <> maybe "" ("\n" <>) invited + notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct) <> maybe "" ("\n" <>) invited + Right GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin + Right GRSContactNotOwner -> replyNotApproved "user is not an owner." + Right GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin + Left e -> sendReply $ "Error: getGroupRolesStatus. Please notify the developers.\n" <> T.pack e where replyNotApproved reason = sendReply $ "Group is not approved: " <> reason serviceNotAdmin = serviceName <> " is not an admin." @@ -626,49 +983,36 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own DCSuspendGroup groupId gName -> do let groupRef = groupReference' groupId gName withGroupAndReg sendReply groupId gName $ \_ gr -> - readTVarIO (groupRegStatus gr) >>= \case - GRSActive -> do - setGroupStatus st gr GRSSuspended + case groupRegStatus gr of + GRSActive -> setGroupStatus sendReply st env cc groupId GRSSuspended $ \gr' -> do let suspended = "The group " <> userGroupReference' gr gName <> " is suspended" - notifyOwner gr $ suspended <> " and hidden from directory. Please contact the administrators." + notifyOwner gr' $ suspended <> " and hidden from directory. Please contact the administrators." sendReply "Group suspended!" notifyOtherSuperUsers $ suspended <> " by " <> viewName (localDisplayName' ct) _ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended." DCResumeGroup groupId gName -> do let groupRef = groupReference' groupId gName withGroupAndReg sendReply groupId gName $ \_ gr -> - readTVarIO (groupRegStatus gr) >>= \case - GRSSuspended -> do - setGroupStatus st gr GRSActive + case groupRegStatus gr of + GRSSuspended -> setGroupStatus sendReply st env cc groupId GRSActive $ \gr' -> do let groupStr = "The group " <> userGroupReference' gr gName - notifyOwner gr $ groupStr <> " is listed in the directory again!" + notifyOwner gr' $ groupStr <> " is listed in the directory again!" sendReply "Group listing resumed!" notifyOtherSuperUsers $ groupStr <> " listing resumed by " <> viewName (localDisplayName' ct) _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." - DCListLastGroups count -> listGroups count False - DCListPendingGroups count -> listGroups count True - DCShowGroupLink groupId gName -> do - let groupRef = groupReference' groupId gName - withGroupAndReg sendReply groupId gName $ \_ _ -> - sendChatCmd cc (APIGetGroupLink groupId) >>= \case - CRGroupLink {connReqContact, memberRole} -> - sendReply $ T.unlines - [ "The link to join the group " <> groupRef <> ":", - strEncodeTxt $ simplexChatContact connReqContact, - "New member role: " <> strEncodeTxt memberRole - ] - CRChatCmdError _ (ChatErrorStore (SEGroupLinkNotFound _)) -> - sendReply $ "The group " <> groupRef <> " has no public link." - r -> do - ts <- getCurrentTime - tz <- getCurrentTimeZone - let resp = T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r - sendReply $ "Unexpected error:\n" <> resp + DCListLastGroups count -> + listLastGroups cc user count >>= \case + Left e -> sendReply $ "Error reading groups: " <> T.pack e + Right gs -> sendGroupsInfo ct ciId True $ first reverse gs + DCListPendingGroups count -> + listPendingGroups cc user count >>= \case + Left e -> sendReply $ "Error reading groups: " <> T.pack e + Right gs -> sendGroupsInfo ct ciId True $ first reverse gs DCSendToGroupOwner groupId gName msg -> do let groupRef = groupReference' groupId gName - withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {dbContactId} -> do + withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {dbContactId = ctId} -> do notifyOwner gr msg - owner <- groupOwnerInfo groupRef dbContactId + owner <- groupOwnerInfo groupRef ctId sendReply $ "Forwarded to " <> owner DCInviteOwnerToGroup groupId gName -> case ownersGroup of Just og@KnownGroup {localDisplayName = ogName} -> @@ -677,60 +1021,60 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own Right () -> do let groupRef = groupReference' groupId gName owner <- groupOwnerInfo groupRef ctId - let invited = " invited " <> owner <> " to owners' group " <> viewName ogName + let invited = " invited " <> owner <> " to owners' group " <> viewName ogName notifyOtherSuperUsers $ viewName (localDisplayName' ct) <> invited sendReply $ "you" <> invited Left err -> sendReply err Nothing -> sendReply "owners' group is not specified" + -- DCAddBlockedWord _word -> pure () + -- DCRemoveBlockedWord _word -> pure () DCCommandError tag -> sendReply $ "Command error: " <> tshow tag | otherwise = sendReply "You are not allowed to use this command" where knownCt = knownContact ct sendReply = mkSendReply ct ciId notifyOtherSuperUsers s = withSuperUsers $ \ctId -> unless (ctId == contactId' ct) $ sendMessage' cc ctId s - listGroups count pending = - readTVarIO (groupRegs st) >>= \groups -> do - grs <- - if pending - then filterM (fmap pendingApproval . readTVarIO . groupRegStatus) groups - else pure groups - sendReply $ tshow (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> tshow count else "") - void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do - ct_ <- getContact cc dbContactId - let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ - sendGroupInfo ct gr dbGroupId $ Just ownerStr inviteToOwnersGroup :: KnownGroup -> GroupReg -> (Either Text () -> IO a) -> IO a inviteToOwnersGroup KnownGroup {groupId = ogId} GroupReg {dbContactId = ctId} cont = sendChatCmd cc (APIListMembers ogId) >>= \case - CRGroupMembers _ (Group _ ms) + Right (CRGroupMembers _ (Group _ ms)) | alreadyMember ms -> cont $ Left "Owner is already a member of owners' group" | otherwise -> do sendChatCmd cc (APIAddMember ogId ctId GRMember) >>= \case - CRSentGroupInvitation {} -> do + Right CRSentGroupInvitation {} -> do printLog cc CLLInfo $ "invited contact ID " <> show ctId <> " to owners' group" cont $ Right () r -> contErr r r -> contErr r where - alreadyMember = isJust . find ((Just ctId == ) . memberContactId) + alreadyMember = isJust . find ((Just ctId ==) . memberContactId) contErr r = do let err = "error inviting contact ID " <> tshow ctId <> " to owners' group: " <> tshow r putStrLn $ T.unpack err cont $ Left err groupOwnerInfo groupRef dbContactId = do - owner_ <- getContact cc dbContactId + owner_ <- getContact' cc user dbContactId let ownerInfo = "the owner of the group " <> groupRef ownerName ct' = "@" <> viewName (localDisplayName' ct') <> ", " - pure $ maybe "" ownerName owner_ <> ownerInfo + pure $ either (const "") ownerName owner_ <> ownerInfo deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () deSuperUserCommand ct ciId cmd | knownContact ct `elem` superUsers = case cmd of + DCPromoteGroup groupId gName promote' -> + withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {groupRegStatus, promoted} -> do + let notify = sendReply $ "Group promotion " <> (if promote' then "enabled" <> (if groupRegStatus == GRSActive then "." else ", but the group is not listed.") else "disabled.") + if promote' /= promoted + then setGroupPromoted sendReply st env cc gr promote' notify + else notify DCExecuteCommand cmdStr -> - sendChatCmdStr cc cmdStr >>= \r -> do - ts <- getCurrentTime - tz <- getCurrentTimeZone - sendReply $ T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r + sendChatCmdStr cc cmdStr >>= \case + Right r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + sendReply $ T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r + Left e -> + sendReply $ T.pack $ serializeChatError True (config cc) e DCCommandError tag -> sendReply $ "Command error: " <> tshow tag | otherwise = sendReply "You are not allowed to use this command" where @@ -743,59 +1087,100 @@ directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, own mkSendReply ct ciId = sendComposedMessage cc ct (Just ciId) . MCText withGroupAndReg :: (Text -> IO ()) -> GroupId -> GroupName -> (GroupInfo -> GroupReg -> IO ()) -> IO () - withGroupAndReg sendReply gId gName action = - getGroup cc gId >>= \case - Nothing -> sendReply $ "Group ID " <> tshow gId <> " not found (getGroup)" - Just g@GroupInfo {groupProfile = GroupProfile {displayName}} - | displayName == gName -> - atomically (getGroupReg st gId) >>= \case - Nothing -> sendReply $ "Registration for group ID " <> tshow gId <> " not found (getGroupReg)" - Just gr -> action g gr + withGroupAndReg sendReply gId = withGroupAndReg_ sendReply gId . Just + + withGroupAndReg_ :: (Text -> IO ()) -> GroupId -> Maybe GroupName -> (GroupInfo -> GroupReg -> IO ()) -> IO () + withGroupAndReg_ sendReply gId gName_ action = + getGroupAndReg cc user gId >>= \case + Left e -> sendReply $ "Group " <> tshow gId <> " error (getGroup): " <> T.pack e + Right (g@GroupInfo {groupProfile = GroupProfile {displayName}}, gr) + | maybe False (displayName ==) gName_ -> + action g gr | otherwise -> sendReply $ "Group ID " <> tshow gId <> " has the display name " <> displayName - sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO () - sendGroupInfo ct gr@GroupReg {dbGroupId} useGroupId ownerStr_ = do - grStatus <- readTVarIO $ groupRegStatus gr - let statusStr = "Status: " <> groupRegStatusText grStatus - getGroupAndSummary cc dbGroupId >>= \case - Just (GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do - let membersStr = "_" <> tshow currentMembers <> " members_" - text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] <> maybeToList ownerStr_ <> [membersStr, statusStr] + getOwnersInfo :: [(GroupInfo, GroupReg)] -> IO [((GroupInfo, GroupReg), Maybe (Either String Contact))] + getOwnersInfo gs = + fmap (either (\e -> map (,Just (Left e)) gs) id) $ withDB' "getOwnersInfo" cc $ \db -> + mapM (\g@(_, gr) -> fmap ((g,) . Just . first show) $ runExceptT $ getContact db (vr cc) user $ dbContactId gr) gs + + sendGroupsInfo :: Contact -> ChatItemId -> Bool -> ([(GroupInfo, GroupReg)], Int) -> IO () + sendGroupsInfo ct ciId isAdmin (gs, n) = do + let more = if n > length gs then ", showing the last " <> tshow (length gs) else "" + replyMsg = (Just ciId, MCText $ tshow n <> " registered group(s)" <> more) + gs' <- if isAdmin then getOwnersInfo gs else pure $ map (,Nothing) gs + sendComposedMessages_ cc (SRDirect $ contactId' ct) $ replyMsg :| map groupMessage gs' + where + groupMessage ((g, gr), ct_) = + let GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}, groupSummary} = g + GroupReg {userGroupRegId, groupRegStatus} = gr + useGroupId = if isAdmin then groupId else userGroupRegId + statusStr = "Status: " <> groupRegStatusText groupRegStatus + membersStr = "_" <> tshow (currentMembers groupSummary) <> " members_" + cmds = "/'role " <> tshow useGroupId <> "', /'filter " <> tshow useGroupId <> "'" + ownerStr = maybe "" (("Owner: " <>) . either (("getContact error: " <>) . T.pack) localDisplayName') ct_ + text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] ++ [ownerStr | isAdmin] ++ [membersStr, statusStr, cmds] msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ - sendComposedMessage cc ct Nothing msg - Nothing -> do - let text = T.unlines $ [tshow useGroupId <> ". Error: getGroup. Please notify the developers."] <> maybeToList ownerStr_ <> [statusStr] - sendComposedMessage cc ct Nothing $ MCText text + in (Nothing, msg) -getContact :: ChatController -> ContactId -> IO (Maybe Contact) -getContact cc ctId = resp <$> sendChatCmd cc (APIGetChat (ChatRef CTDirect ctId) Nothing (CPLast 0) Nothing) - where - resp :: ChatResponse -> Maybe Contact - resp = \case - CRApiChat _ (AChat SCTDirect Chat {chatInfo = DirectChat ct}) _ -> Just ct - _ -> Nothing +setGroupStatusPromo :: (Text -> IO ()) -> DirectoryLog -> ServiceState -> ChatController -> GroupReg -> GroupRegStatus -> Bool -> IO () -> IO () +setGroupStatusPromo sendReply st env cc GroupReg {dbGroupId = gId} grStatus' grPromoted' continue = do + let status' = grDirectoryStatus grStatus' + setGroupStatusPromoStore cc gId grStatus' grPromoted' >>= \case + Left e -> sendReply $ "Error updating group " <> tshow gId <> " status: " <> T.pack e + Right (status, grPromoted) -> do + when ((status == DSListed || status' == DSListed) && (status /= status' || grPromoted /= grPromoted')) $ + listingsUpdated env cc + logGUpdateStatus st gId grStatus' + logGUpdatePromotion st gId grPromoted' + continue -getGroup :: ChatController -> GroupId -> IO (Maybe GroupInfo) -getGroup cc gId = resp <$> sendChatCmd cc (APIGroupInfo gId) - where - resp :: ChatResponse -> Maybe GroupInfo - resp = \case - CRGroupInfo {groupInfo} -> Just groupInfo - _ -> Nothing +addGroupReg :: (Text -> IO ()) -> DirectoryLog -> ChatController -> Contact -> GroupInfo -> GroupRegStatus -> (GroupReg -> IO ()) -> IO () +addGroupReg sendMsg st cc ct g@GroupInfo {groupId} grStatus continue = + addGroupRegStore cc ct g grStatus >>= \case + Left e -> sendMsg $ "Error creating group registation for group " <> tshow groupId <> ": " <> T.pack e + Right gr -> do + logGCreate st gr + continue gr -getGroupAndSummary :: ChatController -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) -getGroupAndSummary cc gId = resp <$> sendChatCmd cc (APIGroupInfo gId) +setGroupStatus :: (Text -> IO ()) -> DirectoryLog -> ServiceState -> ChatController -> GroupId -> GroupRegStatus -> (GroupReg -> IO ()) -> IO () +setGroupStatus sendMsg st env cc gId grStatus' continue = do + let status' = grDirectoryStatus grStatus' + setGroupStatusStore cc gId grStatus' >>= \case + Left e -> sendMsg $ "Error updating group " <> tshow gId <> " status: " <> T.pack e + Right (grStatus, gr) -> do + let status = grDirectoryStatus grStatus + when ((status == DSListed || status' == DSListed) && status /= status') $ listingsUpdated env cc + logGUpdateStatus st gId grStatus' + continue gr + +setGroupPromoted :: (Text -> IO ()) -> DirectoryLog -> ServiceState -> ChatController -> GroupReg -> Bool -> IO () -> IO () +setGroupPromoted sendReply st env cc GroupReg {dbGroupId = gId} grPromoted' continue = + setGroupPromotedStore cc gId grPromoted' >>= \case + Left e -> sendReply $ "Error updating group " <> tshow gId <> " status: " <> T.pack e + Right (status, grPromoted) -> do + when (status == DSListed && grPromoted' /= grPromoted) $ listingsUpdated env cc + logGUpdatePromotion st gId grPromoted' + continue + +updateGroupListingFiles :: ChatController -> User -> FilePath -> IO () +updateGroupListingFiles cc u dir = + getAllListedGroups cc u >>= \case + Right gs -> generateListing dir gs + Left e -> logError $ "generateListing error: failed to read groups: " <> T.pack e + +getContact' :: ChatController -> User -> ContactId -> IO (Either String Contact) +getContact' cc user ctId = withDB "getContact" cc $ \db -> withExceptT show $ getContact db (vr cc) user ctId + +getGroupLink' :: ChatController -> User -> GroupInfo -> IO (Either String GroupLink) +getGroupLink' cc user gInfo = + withDB "getGroupLink" cc $ \db -> withExceptT groupDBError $ getGroupLink db user gInfo + +setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe CreatedLinkContact) +setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole) where resp = \case - CRGroupInfo {groupInfo, groupSummary} -> Just (groupInfo, groupSummary) - _ -> Nothing - -setGroupLinkRole :: ChatController -> GroupId -> GroupMemberRole -> IO (Maybe ConnReqContact) -setGroupLinkRole cc gId mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole gId mRole) - where - resp = \case - CRGroupLink _ _ gLink _ -> Just gLink + Right (CRGroupLink {groupLink = GroupLink {connLinkContact}}) -> Just connLinkContact _ -> Nothing unexpectedError :: Text -> Text diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index cecb253e8d..b78b446821 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -1,72 +1,170 @@ {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeOperators #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Directory.Store - ( DirectoryStore (..), + ( DirectoryLog (..), GroupReg (..), GroupRegStatus (..), UserGroupRegId, GroupApprovalId, - restoreDirectoryStore, - addGroupReg, + DirectoryGroupData (..), + DirectoryMemberAcceptance (..), + DirectoryStatus (..), + ProfileCondition (..), + DirectoryLogRecord (..), + openDirectoryLog, + readDirectoryLogData, + addGroupRegStore, + insertGroupReg, delGroupReg, - setGroupStatus, + deleteGroupReg, + setGroupStatusStore, + setGroupStatusPromoStore, + setGroupPromotedStore, + grDirectoryStatus, setGroupRegOwner, - getGroupReg, getUserGroupReg, getUserGroupRegs, - filterListedGroups, + getAllGroupRegs_, + getDuplicateGroupRegs, + getGroupReg, + getGroupAndReg, + listLastGroups, + listPendingGroups, + getAllListedGroups, + getAllListedGroups_, + searchListedGroups, groupRegStatusText, pendingApproval, + groupRemoved, + fromCustomData, + toCustomData, + noJoinFilter, + basicJoinFilter, + moderateJoinFilter, + strongJoinFilter, + groupDBError, + logGCreate, + logGDelete, + logGUpdateOwner, + logGUpdateStatus, + logGUpdatePromotion, ) where -import Control.Concurrent.STM +import Control.Applicative ((<|>)) import Control.Monad +import Control.Monad.Except +import Control.Monad.IO.Class +import Data.Aeson ((.:), (.=)) +import qualified Data.Aeson.KeyMap as JM +import qualified Data.Aeson.TH as JQ +import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Composition ((.:)) import Data.Int (Int64) -import Data.List (find, foldl', sortOn) +import Data.List (sortOn) import Data.Map (Map) import qualified Data.Map.Strict as M -import Data.Maybe (isJust) -import Data.Set (Set) -import qualified Data.Set as S +import Data.Maybe (fromMaybe, isJust) import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) +import Data.Time.Clock (UTCTime (..), getCurrentTime) +import Data.Time.Clock.System (systemEpochDay) +import Directory.Search +import Directory.Util +import Simplex.Chat.Controller +import Simplex.Chat.Protocol (supportedChatVRange) +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) +import Simplex.Chat.Store +import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.Shared (groupInfoQueryFields, groupInfoQueryFrom) import Simplex.Chat.Types +import Simplex.Messaging.Agent.Store.DB (BoolInt (..), fromTextField_) +import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Util (ifM) -import System.Directory (doesFileExist, renameFile) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) +import Simplex.Messaging.Util (eitherToMaybe, firstRow, maybeFirstRow', safeDecodeUtf8) import System.IO (BufferMode (..), Handle, IOMode (..), hSetBuffering, openFile) -data DirectoryStore = DirectoryStore - { groupRegs :: TVar [GroupReg], - listedGroups :: TVar (Set GroupId), - reservedGroups :: TVar (Set GroupId), - directoryLogFile :: Maybe Handle +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), Query, (:.) (..)) +import Database.SQLite.Simple.QQ (sql) +#endif + +data DirectoryLog = DirectoryLog + { directoryLogFile :: Maybe Handle } data GroupReg = GroupReg { dbGroupId :: GroupId, userGroupRegId :: UserGroupRegId, dbContactId :: ContactId, - dbOwnerMemberId :: TVar (Maybe GroupMemberId), - groupRegStatus :: TVar GroupRegStatus + dbOwnerMemberId :: Maybe GroupMemberId, + groupRegStatus :: GroupRegStatus, + promoted :: Bool, + createdAt :: UTCTime } -data GroupRegData = GroupRegData - { dbGroupId_ :: GroupId, - userGroupRegId_ :: UserGroupRegId, - dbContactId_ :: ContactId, - dbOwnerMemberId_ :: Maybe GroupMemberId, - groupRegStatus_ :: GroupRegStatus +data DirectoryGroupData = DirectoryGroupData + { memberAcceptance :: DirectoryMemberAcceptance } +-- these filters are applied in the order of fields, depending on ProfileCondition: +-- Nothing - do not apply +-- Just +-- PCAll - apply to all profiles +-- PCNoImage - apply to profiles without images +data DirectoryMemberAcceptance = DirectoryMemberAcceptance + { rejectNames :: Maybe ProfileCondition, -- reject long names and names with profanity + passCaptcha :: Maybe ProfileCondition, -- run captcha challenge with joining members + makeObserver :: Maybe ProfileCondition -- the role assigned in the end, after captcha challenge + } + deriving (Eq, Show) + +data ProfileCondition = PCAll | PCNoImage deriving (Eq, Show) + +noJoinFilter :: DirectoryMemberAcceptance +noJoinFilter = DirectoryMemberAcceptance Nothing Nothing Nothing + +basicJoinFilter :: DirectoryMemberAcceptance +basicJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCNoImage, + passCaptcha = Nothing, + makeObserver = Nothing + } + +moderateJoinFilter :: DirectoryMemberAcceptance +moderateJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCAll, + passCaptcha = Just PCNoImage, + makeObserver = Nothing + } + +strongJoinFilter :: DirectoryMemberAcceptance +strongJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCAll, + passCaptcha = Just PCAll, + makeObserver = Nothing + } + type UserGroupRegId = Int64 type GroupApprovalId = Int64 @@ -80,13 +178,20 @@ data GroupRegStatus | GRSSuspended | GRSSuspendedBadRoles | GRSRemoved + deriving (Eq, Show) pendingApproval :: GroupRegStatus -> Bool pendingApproval = \case GRSPendingApproval _ -> True _ -> False -data DirectoryStatus = DSListed | DSReserved | DSRegistered +groupRemoved :: GroupRegStatus -> Bool +groupRemoved = \case + GRSRemoved -> True + _ -> False + +data DirectoryStatus = DSListed | DSReserved | DSRegistered | DSRemoved + deriving (Eq) groupRegStatusText :: GroupRegStatus -> Text groupRegStatusText = \case @@ -104,118 +209,282 @@ grDirectoryStatus = \case GRSActive -> DSListed GRSSuspended -> DSReserved GRSSuspendedBadRoles -> DSReserved + GRSRemoved -> DSRemoved _ -> DSRegistered -addGroupReg :: DirectoryStore -> Contact -> GroupInfo -> GroupRegStatus -> IO UserGroupRegId -addGroupReg st ct GroupInfo {groupId} grStatus = do - grData <- atomically addGroupReg_ - logGCreate st grData - pure $ userGroupRegId_ grData +$(JQ.deriveJSON (enumJSON $ dropPrefix "PC") ''ProfileCondition) + +$(JQ.deriveJSON defaultJSON ''DirectoryMemberAcceptance) + +$(JQ.deriveJSON defaultJSON ''DirectoryGroupData) + +fromCustomData :: Maybe CustomData -> DirectoryGroupData +fromCustomData cd_ = + let memberAcceptance = fromMaybe noJoinFilter $ cd_ >>= \(CustomData o) -> JT.parseMaybe (.: "memberAcceptance") o + in DirectoryGroupData {memberAcceptance} + +toCustomData :: DirectoryGroupData -> CustomData +toCustomData DirectoryGroupData {memberAcceptance} = + CustomData $ JM.fromList ["memberAcceptance" .= memberAcceptance] + +addGroupRegStore :: ChatController -> Contact -> GroupInfo -> GroupRegStatus -> IO (Either String GroupReg) +addGroupRegStore cc Contact {contactId = dbContactId} GroupInfo {groupId = dbGroupId} groupRegStatus = + withDB' "addGroupRegStore" cc $ \db -> do + createdAt <- getCurrentTime + maxUgrId <- + maybeFirstRow' 0 (fromMaybe 0 . fromOnly) $ + DB.query db "SELECT MAX(user_group_reg_id) FROM sx_directory_group_regs WHERE contact_id = ?" (Only dbContactId) + let gr = GroupReg {dbGroupId, userGroupRegId = maxUgrId + 1, dbContactId, dbOwnerMemberId = Nothing, groupRegStatus, promoted = False, createdAt} + insertGroupReg db gr + pure gr + +insertGroupReg :: DB.Connection -> GroupReg -> IO () +insertGroupReg db GroupReg {dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, promoted, createdAt} = do + DB.execute + db + [sql| + INSERT INTO sx_directory_group_regs + (group_id, user_group_reg_id, contact_id, owner_member_id, group_reg_status, group_promoted, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?) + |] + (dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, BI promoted, createdAt, createdAt) + +delGroupReg :: ChatController -> GroupId -> IO (Either String ()) +delGroupReg cc gId = withDB' "delGroupReg" cc (`deleteGroupReg` gId) + +deleteGroupReg :: DB.Connection -> GroupId -> IO () +deleteGroupReg db gId = DB.execute db "DELETE FROM sx_directory_group_regs WHERE group_id = ?" (Only gId) + +setGroupStatusStore :: ChatController -> GroupId -> GroupRegStatus -> IO (Either String (GroupRegStatus, GroupReg)) +setGroupStatusStore cc gId grStatus' = + withDB "setGroupStatusStore" cc $ \db -> do + gr <- getGroupReg_ db gId + ts <- liftIO getCurrentTime + liftIO $ DB.execute db "UPDATE sx_directory_group_regs SET group_reg_status = ?, updated_at = ? WHERE group_id = ?" (grStatus', ts, gId) + pure (groupRegStatus gr, gr {groupRegStatus = grStatus'}) + +setGroupStatusPromoStore :: ChatController -> GroupId -> GroupRegStatus -> Bool -> IO (Either String (DirectoryStatus, Bool)) +setGroupStatusPromoStore cc gId grStatus' grPromoted' = + withDB "setGroupStatusPromoStore" cc $ \db -> do + GroupReg {groupRegStatus, promoted} <- getGroupReg_ db gId + ts <- liftIO getCurrentTime + liftIO $ DB.execute db "UPDATE sx_directory_group_regs SET group_reg_status = ?, group_promoted = ?, updated_at = ? WHERE group_id = ?" (grStatus', BI grPromoted', ts, gId) + pure (grDirectoryStatus groupRegStatus, promoted) + +setGroupPromotedStore :: ChatController -> GroupId -> Bool -> IO (Either String (DirectoryStatus, Bool)) +setGroupPromotedStore cc gId grPromoted' = + withDB "setGroupPromotedStore" cc $ \db -> do + GroupReg {groupRegStatus, promoted} <- getGroupReg_ db gId + ts <- liftIO getCurrentTime + liftIO $ DB.execute db "UPDATE sx_directory_group_regs SET group_promoted = ?, updated_at = ? WHERE group_id = ?" (BI grPromoted', ts, gId) + pure (grDirectoryStatus groupRegStatus, promoted) + +groupDBError :: StoreError -> String +groupDBError = \case + SEGroupNotFound _ -> "group not found" + e -> show e + +setGroupRegOwner :: ChatController -> GroupId -> GroupMember -> IO (Either String ()) +setGroupRegOwner cc gId owner = do + ts <- getCurrentTime + withDB' "setGroupRegOwner" cc $ \db -> + DB.execute + db + [sql| + UPDATE sx_directory_group_regs + SET owner_member_id = ?, updated_at = ? + WHERE group_id = ? + |] + (groupMemberId' owner, ts, gId) + +getGroupReg :: ChatController -> GroupId -> IO (Either String GroupReg) +getGroupReg cc gId = withDB "getGroupReg" cc (`getGroupReg_` gId) + +getGroupReg_ :: DB.Connection -> GroupId -> ExceptT String IO GroupReg +getGroupReg_ db gId = + ExceptT $ firstRow rowToGroupReg "group registration not found" $ + DB.query + db + [sql| + SELECT group_id, user_group_reg_id, contact_id, owner_member_id, group_reg_status, group_promoted, created_at + FROM sx_directory_group_regs + WHERE group_id = ? + |] + (Only gId) + +getGroupAndReg :: ChatController -> User -> GroupId -> IO (Either String (GroupInfo, GroupReg)) +getGroupAndReg cc user@User {userId, userContactId} gId = + withDB "getGroupAndReg" cc $ \db -> + ExceptT $ firstRow (toGroupInfoReg (vr cc) user) ("group " ++ show gId ++ " not found") $ + DB.query db (groupReqQuery <> " AND g.group_id = ?") (userId, userContactId, gId) + +getUserGroupReg :: ChatController -> User -> ContactId -> UserGroupRegId -> IO (Either String (GroupInfo, GroupReg)) +getUserGroupReg cc user@User {userId, userContactId} ctId ugrId = + withDB "getUserGroupReg" cc $ \db -> + ExceptT $ firstRow (toGroupInfoReg (vr cc) user) ("group " ++ show ugrId ++ " not found") $ + DB.query db (groupReqQuery <> " AND r.contact_id = ? AND r.user_group_reg_id = ?") (userId, userContactId, ctId, ugrId) + +getUserGroupRegs :: ChatController -> User -> ContactId -> IO (Either String [(GroupInfo, GroupReg)]) +getUserGroupRegs cc user@User {userId, userContactId} ctId = + withDB' "getUserGroupRegs" cc $ \db -> + map (toGroupInfoReg (vr cc) user) + <$> DB.query db (groupReqQuery <> " AND r.contact_id = ? ORDER BY r.user_group_reg_id") (userId, userContactId, ctId) + +getAllListedGroups :: ChatController -> User -> IO (Either String [(GroupInfo, GroupReg, Maybe GroupLink)]) +getAllListedGroups cc user = withDB' "getAllListedGroups" cc $ \db -> getAllListedGroups_ db (vr cc) user + +getAllListedGroups_ :: DB.Connection -> VersionRangeChat -> User -> IO [(GroupInfo, GroupReg, Maybe GroupLink)] +getAllListedGroups_ db vr' user@User {userId, userContactId} = + DB.query db (groupReqQuery <> " AND r.group_reg_status = ?") (userId, userContactId, GRSActive) + >>= mapM (withGroupLink . toGroupInfoReg vr' user) where - addGroupReg_ = do - let grData = GroupRegData {dbGroupId_ = groupId, userGroupRegId_ = 1, dbContactId_ = ctId, dbOwnerMemberId_ = Nothing, groupRegStatus_ = grStatus} - gr <- dataToGroupReg grData - stateTVar (groupRegs st) $ \grs -> - let ugrId = 1 + foldl' maxUgrId 0 grs - grData' = grData {userGroupRegId_ = ugrId} - gr' = gr {userGroupRegId = ugrId} - in (grData', gr' : grs) - ctId = contactId' ct - maxUgrId mx GroupReg {dbContactId, userGroupRegId} - | dbContactId == ctId && userGroupRegId > mx = userGroupRegId - | otherwise = mx + withGroupLink (g, gr) = (g,gr,) . eitherToMaybe <$> runExceptT (getGroupLink db user g) -delGroupReg :: DirectoryStore -> GroupReg -> IO () -delGroupReg st GroupReg {dbGroupId = gId} = do - logGDelete st gId - atomically $ unlistGroup st gId - atomically $ modifyTVar' (groupRegs st) $ filter ((gId ==) . dbGroupId) - -setGroupStatus :: DirectoryStore -> GroupReg -> GroupRegStatus -> IO () -setGroupStatus st gr grStatus = do - logGUpdateStatus st (dbGroupId gr) grStatus - atomically $ do - writeTVar (groupRegStatus gr) grStatus - updateListing st $ dbGroupId gr +searchListedGroups :: ChatController -> User -> SearchType -> Maybe GroupId -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int)) +searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pageSize = + withDB' "searchListedGroups" cc $ \db -> + case searchType of + STAll -> case lastGroup_ of + Nothing -> do + gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize) + n <- count $ DB.query db countQuery' (Only GRSActive) + pure (gs, n) + Just gId -> do + gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId) + pure (gs, n) + where + countQuery' = countQuery <> " WHERE r.group_reg_status = ? " + orderBy = " ORDER BY g.summary_current_members_count DESC " + STRecent -> case lastGroup_ of + Nothing -> do + gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize) + n <- count $ DB.query db countQuery' (Only GRSActive) + pure (gs, n) + Just gId -> do + gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId) + pure (gs, n) + where + countQuery' = countQuery <> " WHERE r.group_reg_status = ? " + orderBy = " ORDER BY r.created_at DESC " + STSearch search -> case lastGroup_ of + Nothing -> do + gs <- groups $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize) + n <- count $ DB.query db (countQuery' <> searchCond) (GRSActive, s, s, s, s) + pure (gs, n) + Just gId -> do + gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond <> orderBy) (GRSActive, gId, s, s, s, s) + pure (gs, n) + where + s = T.toLower search + countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? " + orderBy = " ORDER BY g.summary_current_members_count DESC " where - updateListing = case grDirectoryStatus grStatus of - DSListed -> listGroup - DSReserved -> reserveGroup - DSRegistered -> unlistGroup + groups = (map (toGroupInfoReg (vr cc) user) <$>) + count = maybeFirstRow' 0 fromOnly + listedGroupQuery = groupReqQuery <> " AND r.group_reg_status = ? " + countQuery = "SELECT COUNT(1) FROM groups g JOIN sx_directory_group_regs r ON g.group_id = r.group_id " + searchCond = + [sql| + AND (LOWER(gp.display_name) LIKE '%' || ? || '%' + OR LOWER(gp.full_name) LIKE '%' || ? || '%' + OR LOWER(gp.short_descr) LIKE '%' || ? || '%' + OR LOWER(gp.description) LIKE '%' || ? || '%' + ) + |] -setGroupRegOwner :: DirectoryStore -> GroupReg -> GroupMember -> IO () -setGroupRegOwner st gr owner = do - let memberId = groupMemberId' owner - logGUpdateOwner st (dbGroupId gr) memberId - atomically $ writeTVar (dbOwnerMemberId gr) (Just memberId) +getAllGroupRegs_ :: DB.Connection -> User -> IO [(GroupInfo, GroupReg)] +getAllGroupRegs_ db user@User {userId, userContactId} = + map (toGroupInfoReg supportedChatVRange user) + <$> DB.query db groupReqQuery (userId, userContactId) -getGroupReg :: DirectoryStore -> GroupId -> STM (Maybe GroupReg) -getGroupReg st gId = find ((gId ==) . dbGroupId) <$> readTVar (groupRegs st) +getDuplicateGroupRegs :: ChatController -> User -> Text -> IO (Either String [(GroupInfo, GroupReg)]) +getDuplicateGroupRegs cc user@User {userId, userContactId} displayName = + withDB' "getDuplicateGroupRegs" cc $ \db -> + map (toGroupInfoReg (vr cc) user) + <$> DB.query db (groupReqQuery <> " AND gp.display_name = ?") (userId, userContactId, displayName) -getUserGroupReg :: DirectoryStore -> ContactId -> UserGroupRegId -> STM (Maybe GroupReg) -getUserGroupReg st ctId ugrId = find (\r -> ctId == dbContactId r && ugrId == userGroupRegId r) <$> readTVar (groupRegs st) +listLastGroups :: ChatController -> User -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int)) +listLastGroups cc user@User {userId, userContactId} count = + withDB' "getUserGroupRegs" cc $ \db -> do + gs <- + map (toGroupInfoReg (vr cc) user) + <$> DB.query db (groupReqQuery <> " ORDER BY group_reg_id DESC LIMIT ?") (userId, userContactId, count) + n <- maybeFirstRow' 0 fromOnly $ DB.query_ db "SELECT COUNT(1) FROM sx_directory_group_regs" + pure (gs, n) -getUserGroupRegs :: DirectoryStore -> ContactId -> STM [GroupReg] -getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVar (groupRegs st) +listPendingGroups :: ChatController -> User -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int)) +listPendingGroups cc user@User {userId, userContactId} count = + withDB' "getUserGroupRegs" cc $ \db -> do + gs <- + map (toGroupInfoReg (vr cc) user) + <$> DB.query db (groupReqQuery <> " AND r.group_reg_status LIKE 'pending_approval%' ORDER BY group_reg_id DESC LIMIT ?") (userId, userContactId, count) + n <- maybeFirstRow' 0 fromOnly $ DB.query_ db "SELECT COUNT(1) FROM sx_directory_group_regs WHERE group_reg_status LIKE 'pending_approval%'" + pure (gs, n) -filterListedGroups :: DirectoryStore -> [(GroupInfo, GroupSummary)] -> STM [(GroupInfo, GroupSummary)] -filterListedGroups st gs = do - lgs <- readTVar $ listedGroups st - pure $ filter (\(GroupInfo {groupId}, _) -> groupId `S.member` lgs) gs +toGroupInfoReg :: VersionRangeChat -> User -> (GroupInfoRow :. GroupRegRow) -> (GroupInfo, GroupReg) +toGroupInfoReg vr' User {userContactId} (groupRow :. grRow) = + (toGroupInfo vr' userContactId [] groupRow, rowToGroupReg grRow) -listGroup :: DirectoryStore -> GroupId -> STM () -listGroup st gId = do - modifyTVar' (listedGroups st) $ S.insert gId - modifyTVar' (reservedGroups st) $ S.delete gId +type GroupRegRow = (GroupId, UserGroupRegId, ContactId, Maybe GroupMemberId, GroupRegStatus, BoolInt, UTCTime) -reserveGroup :: DirectoryStore -> GroupId -> STM () -reserveGroup st gId = do - modifyTVar' (listedGroups st) $ S.delete gId - modifyTVar' (reservedGroups st) $ S.insert gId +rowToGroupReg :: GroupRegRow -> GroupReg +rowToGroupReg (dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, BI promoted, createdAt) = + GroupReg {dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, promoted, createdAt} -unlistGroup :: DirectoryStore -> GroupId -> STM () -unlistGroup st gId = do - modifyTVar' (listedGroups st) $ S.delete gId - modifyTVar' (reservedGroups st) $ S.delete gId +groupReqQuery :: Query +groupReqQuery = groupInfoQueryFields <> groupRegFields <> groupInfoQueryFrom <> groupRegFromCond + where + groupRegFields = ", r.group_id, r.user_group_reg_id, r.contact_id, r.owner_member_id, r.group_reg_status, r.group_promoted, r.created_at " + groupRegFromCond = " JOIN sx_directory_group_regs r ON r.group_id = g.group_id WHERE g.user_id = ? AND mu.contact_id = ? " data DirectoryLogRecord - = GRCreate GroupRegData + = GRCreate GroupReg | GRDelete GroupId | GRUpdateStatus GroupId GroupRegStatus + | GRUpdatePromotion GroupId Bool | GRUpdateOwner GroupId GroupMemberId data DLRTag = GRCreate_ | GRDelete_ | GRUpdateStatus_ + | GRUpdatePromotion_ | GRUpdateOwner_ -logDLR :: DirectoryStore -> DirectoryLogRecord -> IO () +logDLR :: DirectoryLog -> DirectoryLogRecord -> IO () logDLR st r = forM_ (directoryLogFile st) $ \h -> B.hPutStrLn h (strEncode r) -logGCreate :: DirectoryStore -> GroupRegData -> IO () +logGCreate :: DirectoryLog -> GroupReg -> IO () logGCreate st = logDLR st . GRCreate -logGDelete :: DirectoryStore -> GroupId -> IO () +logGDelete :: DirectoryLog -> GroupId -> IO () logGDelete st = logDLR st . GRDelete -logGUpdateStatus :: DirectoryStore -> GroupId -> GroupRegStatus -> IO () -logGUpdateStatus st = logDLR st .: GRUpdateStatus +logGUpdateStatus :: DirectoryLog -> GroupId -> GroupRegStatus -> IO () +logGUpdateStatus st gId = logDLR st . GRUpdateStatus gId -logGUpdateOwner :: DirectoryStore -> GroupId -> GroupMemberId -> IO () -logGUpdateOwner st = logDLR st .: GRUpdateOwner +logGUpdatePromotion :: DirectoryLog -> GroupId -> Bool -> IO () +logGUpdatePromotion st gId = logDLR st . GRUpdatePromotion gId + +logGUpdateOwner :: DirectoryLog -> GroupId -> GroupMemberId -> IO () +logGUpdateOwner st gId = logDLR st . GRUpdateOwner gId instance StrEncoding DLRTag where strEncode = \case GRCreate_ -> "GCREATE" GRDelete_ -> "GDELETE" GRUpdateStatus_ -> "GSTATUS" + GRUpdatePromotion_ -> "GPROMOTE" GRUpdateOwner_ -> "GOWNER" strP = A.takeTill (== ' ') >>= \case "GCREATE" -> pure GRCreate_ "GDELETE" -> pure GRDelete_ "GSTATUS" -> pure GRUpdateStatus_ + "GPROMOTE" -> pure GRUpdatePromotion_ "GOWNER" -> pure GRUpdateOwner_ _ -> fail "invalid DLRTag" @@ -224,30 +493,35 @@ instance StrEncoding DirectoryLogRecord where GRCreate gr -> strEncode (GRCreate_, gr) GRDelete gId -> strEncode (GRDelete_, gId) GRUpdateStatus gId grStatus -> strEncode (GRUpdateStatus_, gId, grStatus) + GRUpdatePromotion gId promoted -> strEncode (GRUpdatePromotion_, gId, promoted) GRUpdateOwner gId grOwnerId -> strEncode (GRUpdateOwner_, gId, grOwnerId) strP = strP_ >>= \case GRCreate_ -> GRCreate <$> strP GRDelete_ -> GRDelete <$> strP GRUpdateStatus_ -> GRUpdateStatus <$> A.decimal <*> _strP + GRUpdatePromotion_ -> GRUpdatePromotion <$> A.decimal <*> _strP GRUpdateOwner_ -> GRUpdateOwner <$> A.decimal <* A.space <*> A.decimal -instance StrEncoding GroupRegData where - strEncode GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = - B.unwords - [ "group_id=" <> strEncode dbGroupId_, - "user_group_id=" <> strEncode userGroupRegId_, - "contact_id=" <> strEncode dbContactId_, - "owner_member_id=" <> strEncode dbOwnerMemberId_, - "status=" <> strEncode groupRegStatus_ +instance StrEncoding GroupReg where + strEncode GroupReg {dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, promoted} = + B.unwords $ + [ "group_id=" <> strEncode dbGroupId, + "user_group_id=" <> strEncode userGroupRegId, + "contact_id=" <> strEncode dbContactId, + "owner_member_id=" <> strEncode dbOwnerMemberId, + "status=" <> strEncode groupRegStatus ] + <> ["promoted=" <> strEncode promoted | promoted] strP = do - dbGroupId_ <- "group_id=" *> strP_ - userGroupRegId_ <- "user_group_id=" *> strP_ - dbContactId_ <- "contact_id=" *> strP_ - dbOwnerMemberId_ <- "owner_member_id=" *> strP_ - groupRegStatus_ <- "status=" *> strP - pure GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} + dbGroupId <- "group_id=" *> strP_ + userGroupRegId <- "user_group_id=" *> strP_ + dbContactId <- "contact_id=" *> strP_ + dbOwnerMemberId <- "owner_member_id=" *> strP_ + groupRegStatus <- "status=" *> strP + promoted <- (" promoted=" *> strP) <|> pure False + let createdAt = UTCTime systemEpochDay 0 + pure GroupReg {dbGroupId, userGroupRegId, dbContactId, dbOwnerMemberId, groupRegStatus, promoted, createdAt} instance StrEncoding GroupRegStatus where strEncode = \case @@ -271,70 +545,30 @@ instance StrEncoding GroupRegStatus where "removed" -> pure GRSRemoved _ -> fail "invalid GroupRegStatus" -dataToGroupReg :: GroupRegData -> STM GroupReg -dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = do - dbOwnerMemberId <- newTVar dbOwnerMemberId_ - groupRegStatus <- newTVar groupRegStatus_ - pure - GroupReg - { dbGroupId = dbGroupId_, - userGroupRegId = userGroupRegId_, - dbContactId = dbContactId_, - dbOwnerMemberId, - groupRegStatus - } +instance ToField GroupRegStatus where toField = toField . safeDecodeUtf8 . strEncode -restoreDirectoryStore :: Maybe FilePath -> IO DirectoryStore -restoreDirectoryStore = \case - Just f -> ifM (doesFileExist f) (restore f) (newFile f >>= new . Just) - Nothing -> new Nothing +instance FromField GroupRegStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 + +openDirectoryLog :: Maybe FilePath -> IO DirectoryLog +openDirectoryLog = \case + Just f -> DirectoryLog . Just <$> openLogFile f + Nothing -> pure $ DirectoryLog Nothing where - new = atomically . newDirectoryStore - newFile f = do - h <- openFile f WriteMode + openLogFile f = do + h <- openFile f AppendMode hSetBuffering h LineBuffering pure h - restore f = do - grs <- readDirectoryData f - renameFile f (f <> ".bak") - h <- writeDirectoryData f grs -- compact - atomically $ mkDirectoryStore h grs -emptyStoreData :: ([GroupReg], Set GroupId, Set GroupId) -emptyStoreData = ([], S.empty, S.empty) - -newDirectoryStore :: Maybe Handle -> STM DirectoryStore -newDirectoryStore = (`mkDirectoryStore_` emptyStoreData) - -mkDirectoryStore :: Handle -> [GroupRegData] -> STM DirectoryStore -mkDirectoryStore h groups = - foldM addGroupRegData emptyStoreData groups >>= mkDirectoryStore_ (Just h) - where - addGroupRegData (!grs, !listed, !reserved) gr@GroupRegData {dbGroupId_ = gId} = do - gr' <- dataToGroupReg gr - let grs' = gr' : grs - pure $ case grDirectoryStatus $ groupRegStatus_ gr of - DSListed -> (grs', S.insert gId listed, reserved) - DSReserved -> (grs', listed, S.insert gId reserved) - DSRegistered -> (grs', listed, reserved) - -mkDirectoryStore_ :: Maybe Handle -> ([GroupReg], Set GroupId, Set GroupId) -> STM DirectoryStore -mkDirectoryStore_ h (grs, listed, reserved) = do - groupRegs <- newTVar grs - listedGroups <- newTVar listed - reservedGroups <- newTVar reserved - pure DirectoryStore {groupRegs, listedGroups, reservedGroups, directoryLogFile = h} - -readDirectoryData :: FilePath -> IO [GroupRegData] -readDirectoryData f = - sortOn dbGroupId_ . M.elems +readDirectoryLogData :: FilePath -> IO [GroupReg] +readDirectoryLogData f = + sortOn dbGroupId . M.elems <$> (foldM processDLR M.empty . B.lines =<< B.readFile f) where - processDLR :: Map GroupId GroupRegData -> ByteString -> IO (Map GroupId GroupRegData) + processDLR :: Map GroupId GroupReg -> ByteString -> IO (Map GroupId GroupReg) processDLR m l = case strDecode l of Left e -> m <$ putStrLn ("Error parsing log record: " <> e <> ", " <> B.unpack (B.take 80 l)) Right r -> case r of - GRCreate gr@GroupRegData {dbGroupId_ = gId} -> do + GRCreate gr@GroupReg {dbGroupId = gId} -> do when (isJust $ M.lookup gId m) $ putStrLn $ "Warning: duplicate group with ID " <> show gId <> ", group replaced." @@ -342,16 +576,12 @@ readDirectoryData f = GRDelete gId -> case M.lookup gId m of Just _ -> pure $ M.delete gId m Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", deletion ignored.") - GRUpdateStatus gId groupRegStatus_ -> case M.lookup gId m of - Just gr -> pure $ M.insert gId gr {groupRegStatus_} m + GRUpdateStatus gId groupRegStatus -> case M.lookup gId m of + Just gr -> pure $ M.insert gId gr {groupRegStatus} m Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", status update ignored.") + GRUpdatePromotion gId promoted -> case M.lookup gId m of + Just gr -> pure $ M.insert gId gr {promoted} m + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", promotion update ignored.") GRUpdateOwner gId grOwnerId -> case M.lookup gId m of - Just gr -> pure $ M.insert gId gr {dbOwnerMemberId_ = Just grOwnerId} m + Just gr -> pure $ M.insert gId gr {dbOwnerMemberId = Just grOwnerId} m Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", owner update ignored.") - -writeDirectoryData :: FilePath -> [GroupRegData] -> IO Handle -writeDirectoryData f grs = do - h <- openFile f WriteMode - hSetBuffering h LineBuffering - forM_ grs $ B.hPutStrLn h . strEncode . GRCreate - pure h diff --git a/apps/simplex-directory-service/src/Directory/Store/Migrate.hs b/apps/simplex-directory-service/src/Directory/Store/Migrate.hs new file mode 100644 index 0000000000..e22f4ed470 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Store/Migrate.hs @@ -0,0 +1,148 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Directory.Store.Migrate where + +import Control.Concurrent.STM +import Control.Monad +import Control.Monad.Except +import qualified Data.ByteString.Char8 as B +import Data.List (find) +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import Directory.Listing +import Directory.Options +import Directory.Store +import Simplex.Chat (createChatDatabase) +import Simplex.Chat.Controller (ChatConfig (..), ChatDatabase (..)) +import Simplex.Chat.Options (CoreChatOpts (..)) +import Simplex.Chat.Options.DB +import Simplex.Chat.Protocol (supportedChatVRange) +import Simplex.Chat.Store.Groups (getGroupInfo, getHostMember) +import Simplex.Chat.Store.Profiles (getUsers) +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Store.Common +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Agent.Store.Interface (closeDBStore, migrateDBSchema) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..)) +import Simplex.Messaging.Encoding.String +import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Util (whenM) +import System.Directory (doesFileExist, renamePath) +import System.Exit (exitFailure) +import System.IO (IOMode (..), withFile) + +#if defined(dbPostgres) +import Directory.Store.Postgres.Migrations +#else +import Directory.Store.SQLite.Migrations +#endif + +runDirectoryMigrations :: DirectoryOpts -> ChatConfig -> DBStore -> IO () +runDirectoryMigrations opts ChatConfig {confirmMigrations} chatStore = + migrateDBSchema + chatStore + (toDBOpts dbOptions chatSuffix False []) + (Just "sx_directory_migrations") + directorySchemaMigrations + MigrationConfig {confirm, backupPath = Nothing} + >>= either (exit . ("directory migrations " <>) . show) pure + where + DirectoryOpts {coreOptions = CoreChatOpts {dbOptions, yesToUpMigrations}} = opts + confirm = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations + +checkDirectoryLog :: DirectoryOpts -> ChatConfig -> IO () +checkDirectoryLog opts cfg = + withDirectoryLog opts $ \logFile -> withChatStore opts $ \st -> do + runDirectoryMigrations opts cfg st + gs <- readDirectoryLogData logFile + withActiveUser st $ \user -> withTransaction st $ \db -> do + mapM_ (verifyGroupRegistration db user) gs + putStrLn $ show (length gs) <> " group registrations OK" + +importDirectoryLogToDB :: DirectoryOpts -> ChatConfig -> IO () +importDirectoryLogToDB opts cfg = do + withDirectoryLog opts $ \logFile -> withChatStore opts $ \st -> do + runDirectoryMigrations opts cfg st + gs <- readDirectoryLogData logFile + ctRegs <- TM.emptyIO + withActiveUser st $ \user -> withTransaction st $ \db -> do + forM_ gs $ \gr -> + whenM (verifyGroupRegistration db user gr) $ do + putStrLn $ "importing group " <> show (dbGroupId gr) + insertGroupReg db =<< fixUserGroupRegId ctRegs gr + renamePath logFile (logFile ++ ".bak") + putStrLn $ show (length gs) <> " group registrations imported" + where + fixUserGroupRegId ctRegs gr@GroupReg {dbGroupId, dbContactId} = do + ugIds <- fromMaybe [] <$> TM.lookupIO dbContactId ctRegs + gr' <- + if userGroupRegId gr `elem` ugIds + then do + let ugId = maximum ugIds + 1 + putStrLn $ "Warning: updating userGroupRegId for group " <> show dbGroupId <> ", contact " <> show dbContactId + pure gr {userGroupRegId = ugId} + else pure gr + atomically $ TM.insert dbContactId (userGroupRegId gr' : ugIds) ctRegs + pure gr' + +exit :: String -> IO a +exit err = putStrLn ("Error: " <> err) >> exitFailure + +exportDBToDirectoryLog :: DirectoryOpts -> ChatConfig -> IO () +exportDBToDirectoryLog opts cfg = + withDirectoryLog opts $ \logFile -> withChatStore opts $ \st -> do + whenM (doesFileExist logFile) $ exit $ "directory log file " ++ logFile ++ " already exists" + runDirectoryMigrations opts cfg st + withActiveUser st $ \user -> do + gs <- withFile logFile WriteMode $ \h -> withTransaction st $ \db -> do + gs <- getAllGroupRegs_ db user + forM_ gs $ \(_, gr) -> + whenM (verifyGroupRegistration db user gr) $ + B.hPutStrLn h $ strEncode $ GRCreate gr + pure gs + putStrLn $ show (length gs) <> " group registrations exported" + +saveGroupListingFiles :: DirectoryOpts -> ChatConfig -> IO () +saveGroupListingFiles opts _cfg = case webFolder opts of + Nothing -> exit "use --web-folder to generate listings" + Just dir -> + withChatStore opts $ \st -> withActiveUser st $ \user -> + withTransaction st $ \db -> + getAllListedGroups_ db supportedChatVRange user >>= generateListing dir + +verifyGroupRegistration :: DB.Connection -> User -> GroupReg -> IO Bool +verifyGroupRegistration db user GroupReg {dbGroupId = gId, dbContactId = ctId, dbOwnerMemberId, groupRegStatus} = + runExceptT (getGroupInfo db supportedChatVRange user gId) >>= \case + Left e -> False <$ putStrLn ("Error: loading group " <> show gId <> " (skipping): " <> show e) + Right GroupInfo {localDisplayName} -> do + let groupRef = show gId <> " " <> T.unpack localDisplayName + runExceptT (getHostMember db supportedChatVRange user gId) >>= \case + Left e -> False <$ putStrLn ("Error: loading host member of group " <> groupRef <> " (skipping): " <> show e) + Right GroupMember {groupMemberId = mId', memberContactId = ctId'} -> case dbOwnerMemberId of + Nothing -> True <$ putStrLn ("Warning: group " <> groupRef <> " has no owner member ID, host member ID is " <> show mId' <> ", registration status: " <> B.unpack (strEncode groupRegStatus)) + Just mId + | mId /= mId' -> False <$ putStrLn ("Error: different host member ID of " <> groupRef <> " (skipping): " <> show mId') + | otherwise -> True <$ unless (Just ctId == ctId') (putStrLn $ "Warning: bad group " <> groupRef <> " contact ID: " <> show ctId') + +withDirectoryLog :: DirectoryOpts -> (FilePath -> IO ()) -> IO () +withDirectoryLog DirectoryOpts {directoryLog} action = + maybe (exit "directory log file not specified") action directoryLog + +withChatStore :: DirectoryOpts -> (DBStore -> IO ()) -> IO () +withChatStore DirectoryOpts {coreOptions = CoreChatOpts {dbOptions, yesToUpMigrations, migrationBackupPath}} action = + createChatDatabase dbOptions migrationConfig >>= \case + Left e -> exit $ show e + Right ChatDatabase {chatStore, agentStore} -> do + action chatStore + closeDBStore chatStore + closeDBStore agentStore + where + migrationConfig = MigrationConfig (if yesToUpMigrations then MCYesUp else MCConsole) migrationBackupPath + +withActiveUser :: DBStore -> (User -> IO ()) -> IO () +withActiveUser st action = withTransaction st getUsers >>= maybe (exit "no active user") action . find activeUser diff --git a/apps/simplex-directory-service/src/Directory/Store/Postgres/Migrations.hs b/apps/simplex-directory-service/src/Directory/Store/Postgres/Migrations.hs new file mode 100644 index 0000000000..4a801fee74 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Store/Postgres/Migrations.hs @@ -0,0 +1,52 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE QuasiQuotes #-} + +module Directory.Store.Postgres.Migrations where + +import Data.List (sortOn) +import Data.Text (Text) +import qualified Data.Text as T +import Simplex.Messaging.Agent.Store.Shared (Migration (..)) +import Text.RawString.QQ (r) + +directorySchemaMigrations :: [Migration] +directorySchemaMigrations = sortOn name $ map migration schemaMigrations + where + migration (name, up, down) = Migration {name, up, down} + +schemaMigrations :: [(String, Text, Maybe Text)] +schemaMigrations = + [ ("20250924_directory_schema", m20250924_directory_schema, Just down_m20250924_directory_schema) + ] + +m20250924_directory_schema :: Text +m20250924_directory_schema = + T.pack + [r| +CREATE TABLE sx_directory_group_regs( + group_reg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_id BIGINT NOT NULL REFERENCES groups ON UPDATE RESTRICT ON DELETE CASCADE, + user_group_reg_id BIGINT NOT NULL, + contact_id BIGINT NOT NULL REFERENCES contacts(contact_id) ON UPDATE RESTRICT ON DELETE CASCADE, + owner_member_id BIGINT REFERENCES group_members(group_member_id) ON UPDATE RESTRICT ON DELETE CASCADE, + group_reg_status TEXT NOT NULL, + group_promoted SMALLINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); + +CREATE UNIQUE INDEX idx_sx_directory_group_regs_group_id ON sx_directory_group_regs(group_id); +CREATE UNIQUE INDEX idx_sx_directory_group_regs_owner_member_id ON sx_directory_group_regs(owner_member_id); +CREATE UNIQUE INDEX idx_sx_directory_group_regs_owner_contact_id_user_group_reg_id ON sx_directory_group_regs(contact_id, user_group_reg_id); + |] + +down_m20250924_directory_schema :: Text +down_m20250924_directory_schema = + T.pack + [r| +DROP INDEX idx_sx_directory_group_regs_group_id; +DROP INDEX idx_sx_directory_group_regs_owner_member_id; +DROP INDEX idx_sx_directory_group_regs_owner_contact_id_user_group_reg_id; + +DROP TABLE sx_directory_group_regs; + |] diff --git a/apps/simplex-directory-service/src/Directory/Store/SQLite/Migrations.hs b/apps/simplex-directory-service/src/Directory/Store/SQLite/Migrations.hs new file mode 100644 index 0000000000..f35f9e250a --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Store/SQLite/Migrations.hs @@ -0,0 +1,49 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE QuasiQuotes #-} + +module Directory.Store.SQLite.Migrations (directorySchemaMigrations) where + +import Data.List (sortOn) +import Database.SQLite.Simple (Query (..)) +import Database.SQLite.Simple.QQ (sql) +import Simplex.Messaging.Agent.Store.Shared (Migration (..)) + +directorySchemaMigrations :: [Migration] +directorySchemaMigrations = sortOn name $ map migration schemaMigrations + where + migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down} + +schemaMigrations :: [(String, Query, Maybe Query)] +schemaMigrations = + [ ("20250924_directory_schema", m20250924_directory_schema, Just down_m20250924_directory_schema) + ] + +m20250924_directory_schema :: Query +m20250924_directory_schema = + [sql| +CREATE TABLE sx_directory_group_regs( + group_reg_id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id INTEGER NOT NULL REFERENCES groups ON UPDATE RESTRICT ON DELETE CASCADE, + user_group_reg_id INTEGER NOT NULL, + contact_id INTEGER NOT NULL REFERENCES contacts(contact_id) ON UPDATE RESTRICT ON DELETE CASCADE, + owner_member_id INTEGER REFERENCES group_members(group_member_id) ON UPDATE RESTRICT ON DELETE CASCADE, + group_reg_status TEXT NOT NULL, + group_promoted INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); + +CREATE UNIQUE INDEX idx_sx_directory_group_regs_group_id ON sx_directory_group_regs(group_id); +CREATE UNIQUE INDEX idx_sx_directory_group_regs_owner_member_id ON sx_directory_group_regs(owner_member_id); +CREATE UNIQUE INDEX idx_sx_directory_group_regs_owner_contact_id_user_group_reg_id ON sx_directory_group_regs(contact_id, user_group_reg_id); + |] + +down_m20250924_directory_schema :: Query +down_m20250924_directory_schema = + [sql| +DROP INDEX idx_sx_directory_group_regs_group_id; +DROP INDEX idx_sx_directory_group_regs_owner_member_id; +DROP INDEX idx_sx_directory_group_regs_owner_contact_id_user_group_reg_id; + +DROP TABLE sx_directory_group_regs; + |] diff --git a/apps/simplex-directory-service/src/Directory/Util.hs b/apps/simplex-directory-service/src/Directory/Util.hs new file mode 100644 index 0000000000..a4b79a1bef --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Util.hs @@ -0,0 +1,31 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Directory.Util where + +import Control.Logger.Simple +import Control.Monad.Except +import Data.Text (Text) +import qualified Data.Text as T +import Simplex.Chat.Controller +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Store.Common (withTransaction) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Util (catchAll) + +vr :: ChatController -> VersionRangeChat +vr ChatController {config = ChatConfig {chatVRange}} = chatVRange +{-# INLINE vr #-} + +withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Either String a) +withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a + +withDB :: Text -> ChatController -> (DB.Connection -> ExceptT String IO a) -> IO (Either String a) +withDB cxt ChatController {chatStore} action = do + r_ <- withTransaction chatStore (runExceptT . action) `catchAll` (pure . Left . show) + case r_ of + Left e -> logError $ "Database error: " <> cxt <> " " <> T.pack e + Right _ -> pure () + pure r_ diff --git a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md index 13a514c175..55158130f8 100644 --- a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md +++ b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md @@ -164,7 +164,7 @@ The main objective here is to establish the framework for comparing the security 2 Post-quantum cryptography is available in beta version, as opt-in only for direct conversations. See below how it will be rolled-out further. Some columns are marked with a yellow checkmark: -- when messages are padded, but not to a fixed size. +- when messages are padded, but not to a fixed size (Briar pads messages to the size rounded up to 1024 bytes, Signal - to 160 bytes). - when repudiation does not include client-server connection. In case of Cwtch it appears that the presence of cryptographic signatures compromises repudiation (deniability), but it needs to be clarified. - when 2-factor key exchange is optional (via security code verification). - when post-quantum cryptography is only added to the initial key agreement and does not protect break-in recovery. diff --git a/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md index 57c4f69981..c7087f1657 100644 --- a/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md +++ b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md @@ -89,7 +89,7 @@ Matrix network does not provide connection privacy, as not only user identity ex **Server operator transparency** -Operator transparency means that network users know who operates the servers they use. +Operator transparency means that network users know who operates the servers they use. You may argue that when the operators are known, the servers data can be requested by the authorities. But such requests, in particular when multiple operators are used by all users, will follow a due legal process, and will not result in compromising the privacy of all users. @@ -139,8 +139,8 @@ Evgeny SimpleX Chat founder -[1] You can also to self-host your own SimpleX servers on [Flux decentralized cloud](https://home.runonflux.io/apps/marketplace?q=simplex). +[1]: You can also to self-host your own SimpleX servers on [Flux decentralized cloud](https://home.runonflux.io/apps/marketplace?q=simplex). -[2] The probability of connection being de-anonymized and the number of random server choices follow this equation: `(1 - s ^ 2) ^ n = 1 - p`, where `s` is the share of attacker-controlled servers in the network, `n` is the number of random choices of entry and exit nodes for the circuit, and `p` is the probability of both entry and exit nodes, and the connection privacy being compromised. Substituting `0.02` (2%) for `s`, `0.5` (50%) for `p`, and solving this equation for `n` we obtain that `1733` random circuits have 50% probability of privacy being compromised. +[2]: The probability of connection being de-anonymized and the number of random server choices follow this equation: `(1 - s ^ 2) ^ n = 1 - p`, where `s` is the share of attacker-controlled servers in the network, `n` is the number of random choices of entry and exit nodes for the circuit, and `p` is the probability of both entry and exit nodes, and the connection privacy being compromised. Substituting `0.02` (2%) for `s`, `0.5` (50%) for `p`, and solving this equation for `n` we obtain that `1733` random circuits have 50% probability of privacy being compromised. Also see [this presentation about Tor](https://ritter.vg/p/tor-v1.6.pdf), specifically the approximate calculations on page 76, and also [Tor project post](https://blog.torproject.org/announcing-vanguards-add-onion-services/) about the changes that made attack on hidden service anonymity harder, but still viable in case the it is used for a long time. diff --git a/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md b/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md new file mode 100644 index 0000000000..f1aa3a243f --- /dev/null +++ b/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md @@ -0,0 +1,188 @@ +--- +layout: layouts/article.html +title: "SimpleX Chat v6.3: new user experience and safety in public groups" +date: 2025-03-08 +previewBody: blog_previews/20250308.html +image: images/20250308-captcha.png +imageBottom: true +permalink: "/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html" +--- + +# SimpleX Chat v6.3: new user experience and safety in public groups + +**Published:** Mar 8, 2025 + +**What's new in v6.3**: +- [preventing spam and abuse in public groups](#preventing-spam-and-abuse-in-public-groups). +- [group improvements](#group-improvements): [mention other members](#mention-other-members-and-get-notified-when-mentioned), [improved performance](#better-group-performance). +- [better chat navigation](#better-chat-navigation): [organize chats into lists](#organize-chats-into-lists) and [jump to found and forwarded messages](#jump-to-found-and-forwarded-messages). +- [privacy and security improvements](#privacy-and-security-improvements): [chat retention period](#set-message-retention-period-in-chats) and [private media file names](#private-media-file-names). + +Also, we added Catalan interface language to Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +The last but not the least - [server builds are now reproducible](#reproducible-server-builds). + +## What's new in v6.3 + +## Preventing spam and abuse in public groups + +[We wrote before](./20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md): as the network grows, it becomes more attractive to attackers. This release adds several features that reduce the possibility of attacks and abuse. + +### Spam in groups that are listed in our group directory + +There is no built-in group discovery in SimpleX Chat apps. Instead, we offer an experimental chat bot that allows to submit and to discover public groups. Not so long ago, spammers started sending messages via bots attempting to disrupt these groups. + +We released several changes to the groups directory to protect from spam attacks. + +**Optional captcha verification** + + + +Group owners can enable the requirement to pass captcha challenge before joining the group. Captcha is generated in the directory bot itself, without any 3rd party servers, and is sent to the joining member. The new member must reply with the text in the image to be accepted to the group. While not a perfect protection, this basic measure complicates programming automatic bots to join public groups. It also provides a foundation to implement "knocking" - a conversation with dedicated group admins prior to joining the group. We plan to release support for knocking in March. + +**Profanity filter for member names** + +While group settings support giving all joining member an "observer" role - that is, without the right to send messages - the attackers tried spaming groups by joining and leaving. We added an optional filter for member names that group owners can enable for groups listed in directory - if a member name contains profanity, they will be rejected. Further improvements will be released in March as well. + +The current SimpleX directory chatbot is a hybrid of [future chat relays](./20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md#can-large-groups-scale) (a.k.a. super-peers) we are now developing to support large groups, and of a directory service that will be embedded in the app UI later this year, allowing to search and to discover public groups. Anybody is able to run their own directory bots now, and there will be possibility to use third party directories via the app UI in the future too. + +Read more about [SimpleX group directory](../docs/DIRECTORY.md), how to submit your groups, and which groups we now accept. Currently we accept groups related to a limited list of topics that will be expanded once we have better moderation functionality for the groups. + +### More power to group owners and moderators + +This release includes two new features to help group moderators. + + + +**Private reports** + +Group members can privately bring to group moderators attention specific messages and members, even if the group does not allow direct messages. The simply need to choose report in the message context menu and choose the report reason. This report will be visible to all group owners and moderators, but not to other members. + +Group moderators can see all member reports in a separate view, and quickly find the problematic messages, making moderation much easier in public groups. These reports are private to groups, they are not sent to server operators. + +Please note: in the groups listed in our directory, the directory bot acts as admin, so it will receive all reports as well. + +**Acting on multiple members at once** + +When attackers come, they often use multiple profiles. This version allows selecting multiple members at once and perform these actions on all selected members: +- switch members role between "observer" and "member". +- block and unblock members - this is a "shadow" block, so when you block multiple members who you believe are attackers, their messages will be blocked for all other members but not for them. +- remove members from the group. + +The next version will also allow to remove members together with all messages they sent - for example, if a spam bot joined and sent a lot of spam, but nothing of value. + +## Group improvements + +### Mention other members and get notified when mentioned + + + +This feature allows you to mention other members in the group in the usual way - type `@` character, and choose the member you want to mention from the menu. Even that there is no user accounts and persistent identities we made it work by referencing members by their random group ID that is also used for replies and all other interactions in the group. + +You can also now switch message notifications in the group to "mentions only" mode. You will be notified only when you are mentioned in a message, or when somebody replies to your message. Simply choose "Mute" in the context menu of the group in the list of chats to switch group notifications to "mentions only" mode. After that you can choose "Mute all" to disable all notifications, including mentions. + +### Better group performance + +**Send messages faster** + +We didn't reduce the required network traffic to send messages to large groups yet - your client still has to send message to each member individually. But we redesigned the process of sending a message, reducing temporary storage required to schedule the message for delivery by about 100x. This creates a significant storage saving - e.g, to send one message to a group of 1,000 members previously required ~20Mb, and now it is reduced to ~200kb. It also reduces the time and battery used to send a message. + +**Faster group deletion** + +When you leave the group, the app preserves a copy of all your communications in the group. You can choose to keep it or to delete it completely. This final group deletion was very slow prior to this release - depending on the number of groups on your device it could sometimes take several minutes. + +This release solved this problem – the time it takes to delete the group is reduced to seconds, and even in cases when the app is terminated half-way, it either rolls back or completes, but it cannot leave the group in a partially deleted state. It improves both user experience and privacy, as gives you better control over your data. + +## Better chat navigation + +### Organize chats into lists + + + +It is a common feature in many messengers – it helps organizing your conversations. + +The lists also show a blue mark when any chat in the list has new messages. + +There are several preset lists: contacts, groups, private notes, business chats, favourite chats and also groups with member reports - the last list is automatically shown if members of any groups where you are the moderator or the owner sent private reports, until these reports are acted on or archived. + +### Jump to found and forwarded messages + +This version allows to quickly navigate from message in the search results to the point in the conversation when it was sent. + +You can also navigate from the forwarded message (or from the message saved to private notes) to the original message in the chat where it was forwarded or saved from. + +## Privacy and security improvements + +### Set message retention period in chats + +Before this version, you could enable message retention period for all chats in your profile. While helpful in some cases, many of us have conversations that we want to keep for a long time, and some other conversations that we want to remove quicker. + +This version allows it - you can set different retention periods in different conversations. It can be 1 day, 1 week, 1 month or 1 year. We may allow custom retention time in the future. + +### Private media file names + +Previously there were scenarios when original media file names were preserved - e.g., when sending a video file or when forwarding any media file. The latter problem was worse, as media file name is generated automatically, and includes timestamp. So the same name could have been used to correlate files between conversations, as one of our users pointed out. + +This version fixes this problem - media file name is now changed when forwarding it to match the time of forwarding, so no additional metadata is revealed. + +Please also note: +- the apps remove metadata from all static images, +- iOS app removes metadata from videos, but android and desktop apps do not do it yet, +- animated images are sent as is, +- other file types are sent as is, and their names are left unchanged - we believe that for ordinary files their name is part of their content. + +We plan further improvements to reduce metadata in files in the near future – please let us know what you believe is the most important to reduce first. + +## Reproducible server builds + +Starting from v6.3 server releases are reproducible! + +**Why it is important** + +With reproducible builds anybody can build servers from our code following the same process, and the build would produce identical binaries. + +This also allows us to sign releases, as we reproduce GitHub builds ourselves and by signing them we attest that our builds resulted in identical binaries. + +**How to reproduce builds** + +You can reproduce our builds on Linux with x86 CPU in docker container - please follow the instructions [here](../docs/SERVER.md#reproduce-builds). + +We are looking for support from open-source contributors or security researchers who would also reproduce and sign our releases. + +**How to verify release signature** + +Please see the instructions [here](../docs/SERVER.md#verifying-server-binaries). + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.md b/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.md new file mode 100644 index 0000000000..077d438e6b --- /dev/null +++ b/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.md @@ -0,0 +1,192 @@ +--- +layout: layouts/article.html +title: "SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4" +date: 2025-07-03 +preview: Now you can start talking to your contacts much faster, as soon as you scan the link. This technical post covers the technology that enabled this new user experience — short links and associated data of messaging queues. +image: images/20250703-connect1a.png +imageBottom: true +permalink: "/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.html" +--- + +# SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4 + +**Published:** Jul 3, 2025 + +The mission of communication network is connecting people [1]. The process of connecting in SimpleX network is really secure — it is protected from server MITM attacks. But before this beta version connecting to contacts had a really bad user experience. + +## What was the problem? + +How did it work before: + +1. Your contact created a large link (1-time invitation or contact address [2]) and shared with you via another messenger, email, social profile or website. Sharing the link "out-of-band" (i.e., not via the server) is necessary for security. But it was not working well in some cases: + - the link was incorrectly changed by some applications, e.g. Telegram, because of link complexity, preventing people from connecting. + - some people were worried that the link "looks like malware". + - the QR code for the link was large, and sometimes difficult to scan. + - the link did not fit the size limit in social media profiles. +2. Once you received the link, you used it in the app. But when using the link you could not see who you were connecting to. The only choice you had is to share your current profile or to use incognito profile. And as people didn't know which profile will be shared with them, many were choosing to share incognito profile, making recognizing contacts more complex — you don't know who is who unless you attach aliases to incognito contacts. +3. Once you tap Connect, all you see is the line in the list of chats that said "Connecting via link". The process of connection required your contact to be online, and in some cases to approve the request, so you may had contact in "connecting" state for a really long time. + +So it is not surprising that a large number of people failed connecting to friends — either they refused to engage because of large and scary looking links, or their application made the link unusable, or they abandoned the process at step 3, deciding that the app is not working correctly. + +## Why can't we just use usernames? + +Many people asked — why don't you just use usernames or a link shortener for some really short links, as other networks and apps do. + +The problem is that usernames or very short links make e2e encryption security of your chats dependent on the servers. Unless the link you share contains enough randomness in it and is cryptographically linked to the encryption keys, the servers can substitute the e2e encryption keys and read all your communication without you knowing it. We see this risk as unacceptable. + +Mitigation against this "man-in-the-middle" attack by the server [3] is offered by Signal and other apps via security code verification [4], when you compare the numeric code in your app with your contact's app, but: +- most people do not verify security codes, and even if they do, they do not re-verify them every time security code changes, so their security is dependent on the server not being compromised, which is not a great security, +- the servers can still compromise the initial messages, where profile names are exchanged, before you had the chance to verify the security codes. + +When we design communication protocols for SimpleX network we always aim to protect you from the attack via your network operators — this is what sets SimpleX network design apart from many other communication networks and platforms. + +Even though you choose the servers that you trust, and they are bound by privacy policy, and we follow the best security practices to protect servers from any 3rd party attacks, there is still a possibility that servers may be compromised by some attackers, and unless your communications are not protected from the servers, they are not protected from whoever can compromise the servers [5]. + +## What is the new way to make connections? + +Before diving into the details of technology, let's walk though the new process of connecting to people that is introduced in [v6.4-beta.4](https://github.com/simplex-chat/simplex-chat/releases/tag/v6.4.0-beta.4). + + + +1. As before, to connect you or your contact need to create a 1-time invitation or a contact address link. All the past problems of the long links are now solved: + - this link is correctly processed by all applications, as it has a simple structure, + - it is short and simple, e.g. the SimpleX Chat team address for support is: https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw + - the QR code is now much smaller, fits a standard business card, and is easy to scan from all devices, + - it fits in most social media profiles. + +While the link is short, it still contains 256 bits of key material with additional 192 bits of server-generated link ID for one-time invitation links, so the connection is as secure as before, and in case of one-time invitations it became more secure (see below). + + + +2. As before, you have to use this link in the app, either by pasting the link or scanning QR code. But now you instantly see the name of your contact or group you are connecting to, and from v6.4.1 that will be released this July you will also see a profile image (currently disabled for backward compatibility). + +3. Once you tap "Open new chat", the app will instantly open a conversation with your contact. As you now can see which profile is shared with you via the link, you can choose which of your profiles to use to connect. If your contact shared an incognito pseudonym, then you may also choose to connect incognito. But if your contact shared a real name, you may want to share your real name as well, making it easier for your contact to recognize you — the law of reciprocity in action! **In any case, your and your contact's profile names are inaccessible to messaging servers — they are e2e encrypted**. + +If you are connecting via a contact address you can also add a message to your request, making it more likely to be accepted when connecting to somebody you don't know well. And from v6.4.1 contact addresses can include a welcome message that you would see before connecting, right in the conversation. This way, you effectively become connected to your contact and start a secure conversation even before you tap "Connect" button. + +If you are connecting via a one-time invitation link, all you need to do is to tap "Connect", and then you can send messages straight after that, without waiting for your contact to be online — they will be securely received later. + +This new experience of connecting is very similar to commonly used messengers, but it protects your security. We hope that it will be much easier for the new users to connect to their friends. + +## What about security? + +We took a great care to design the protocol extension for the new experience of connecting in a way that not only preserves security at the same level as before, but also increases security of connecting via one-time invitation links. + +First, because all the keys are now included in encrypted link data on the server, and not in the link itself as before, we can include the keys for post-quantum (PQ) key exchange and make the first message sent via one-time link (your profile) encrypted with PQ e2e encryption. Previously, PQ encryption started only after the response from your contact. + +Second, whoever can observe the link is not able to determine which public keys are used in key exchange and what messaging queue address is used, and this data is removed from the server once the connection is established. Previously, the invitation link contained public keys and the actual queue address that could have been used for a long time, unless you rotated it. + +Third, if somebody retrieves the associated data of one-time invitation link they observed in transit, this link would become inaccessible for the intended recipient, so the recipient would know that the connection was potentially compromised, and would alert the contact that sent the link. + +## How does it work? + +In short, a new short link references a container with the encrypted data on the server that contains: +- the original full link that now include quantum-resistant keys that previously were not included because of their size, +- contact's or group's profile and conversation preferences, from v6.4.1 it will include profile images, +- also, it will include an optional contact's or group's welcome message from v6.4.1. + +Making user profile and welcome message included in the encrypted link data allows to start conversation as soon as you scan the link, as described in the previous section. + +### Design objectives and cryptographic primitives that achieve them + +This section is not a formal specification of the protocol, but an informal technical explanation of objectives we had for this design and how they were achieved. The technical details are available in [this RFC document](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2025-03-16-smp-queues.md). + +1. **Encrypted link data cannot be accessed by the server**. + +It means that while the client apps should use the link to derive both the link ID for the server and decryption key for the associated link data, the server should not be able to derive the link and decryption key from the link ID that it knows, and can't access the link data. + +This objective is achieved by using `secret_box` encryption of link data with the symmetric key derived from link URI, which is different from link ID known to the server. As it is a symmetric encryption, it is secure against quantum computers. + +2. **Allow changing encrypted link data without changing the link itself**. + +This is necessary to allow changes in user profile, chat preferences and welcome messages. + +This is possible via a specific server request that allows to change user-defined part of the link data to the link owner. Because the link is derived from fixed part of the link data, the link itself remains the same. + +3. **Prevent MITM attack on the link data by the server, even if the server obtained the link**. + +It means that the server should not be able to replace the associated link data even if it somehow obtained the link and can decrypt the data. + +This objective is achieved by deriving encryption key from the hash of the fixed part of the link data — if server changes the link data, it would be rejected by the client, as its hash won't match the link. Server also cannot replace the user-defined part of the link data, because it is signed and will be verified with the key included in the fixed part of link data. + +Clients use `HKDF` for key derivation, `SHA3-256` to compute hash of the fixed part of link data, and `ED25519` signature to sign user-defined link data. + +4. **Prevent undetectably accessing encrypted link data of one-time links**. + +This is explained in the previous section — if link observers retrieve the link data, the link will become inaccessible for the intended recipient. + +This objective is achieved because the link data of 1-time invitation link data can only be accessed with the server request that locks queue on the first access. Any subsequent access to the queue must uses the same authorization key (`ED25519`). + +5. **The link owner cannot include address of another queue in the link**. + +It means that the link cannot redirect the connecting party to another server or to another queue on the same server — the apps would reject the links that attempt to do it. While allowing redirects may be seen as higher security from the server, it would open the possibility of resource exhaustion attacks, as the server would not know if the links were actually used to connect or not, and when the link data can be removed. So we decided that preventing redirects is a better tradeoff. This cryptographically enforced association between link and queue allows to remove link data from the server once the connection is established, or once some time passes (e.g., 3 weeks for unused one-time invitation links). + +This objective is achieved by including queue ID and link data into the same server response. + +6. **Prevent link owner from being able to change the queue address and encryption keys in the link**. + +This quality prevents the MITM attack on e2e encryption via break-in attack on the client of the link owner. + +The server does not provide any API to change the fixed part of the link data. Also, changing fixed data would require changing the link, as otherwise the hash of the data won't match the link. + +7. **It should be impossible to check the existence of a messaging queue for one-time invitation links**. + +It means that any 3rd party that observed 1-time invitation link (e.g., by reading the message in the messenger or email where it was sent) must not be able to undetectably confirm whether this messaging queue still exists, by attempting to create another queue with the same link ID and the same link data. + +This objective is achieved by servers requiring that sender ID is derived (using `SHA3-384`) from request correlation ID, so an arbitrary sender ID cannot be used, and by generating link ID on the server — for 1-time invitation link, the link ID is included in the link in addition to link key, and is not derived from the link data. + +See the detailed [threat model](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2025-03-16-smp-queues.md#threat-model) for protocol extension supporting the new user experience of making connections. + +## Let us know what you think! + +We worked really hard to deliver this big change — the simplicity of user experience required to hide a lot of complexity under the hood. We really hope that it will help you to bring more of your friends to SimpleX network and to benefit from using secure communications. + +The stable versions v6.4 and v6.4.1 will be released this July, but you can already use the beta version available via [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app) (Android), [Test Flight](https://testflight.apple.com/join/DWuT2LQu) (iOS) and [GitHub](https://github.com/simplex-chat/simplex-chat/releases/tag/v6.4.0-beta.4) (Android and desktop). + +Big thank you to everybody who uses SimpleX network, even though the experience of connecting to people was complex before this release. + +With your help, SimpleX network should be able to get over the million active users now! + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder + +[1]: An interesting case study is the rise and fall of Nokia as the dominant supplier of mobile phones. The slogan "Connecting people" was created in 1992 by Ove Strandberg, an intern at Nokia, and as it was adopted as the core mission of the company, we saw it rise as the world's main mobile phone supplier. The fall of Nokia is usually attributed on iPhone success. But it may also be attributed to internal cultural changes, with Nokia's communications chief leaving in early 2000s, and Nokia failure to understand how the definition of "Connecting people" should evolve with time. + +[2]: One-time invitation can only be used once by the person you gave it too. Once your contact scans the one-time link, nobody else can connect to you via this link. Contact address can be used by multiple people, and even if you later delete the address, everybody who connected to you will remain connected. You can read more about the differences between one-time invitation links and contact addresses [here](../docs/guide/making-connections.md#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + +[3]: Read more about how man-in-the-middle attack works in our [post about e2e encryption properties](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#5-man-in-the-middle-attack-mitigated-by-two-factor-key-exchange). It also has the comparison of e2e encryption security in different messengers. + +[4]: SimpleX apps also allow security code verification, but it protects against different attack — the link substitution by the channel you use to pass it, not from the attacks by the servers — SimpleX servers cannot compromise e2e encryption. + +[5]: That is also why "securely scanning users' communications", also known as "Chat Control" is impossible — what communication operator can access, cyber-criminals will also access, and instead of reducing crime it would expose users to more crime. diff --git a/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md b/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md new file mode 100644 index 0000000000..7a227d4667 --- /dev/null +++ b/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md @@ -0,0 +1,151 @@ +--- +layout: layouts/article.html +title: "SimpleX Chat v6.4.1: welcome your contacts, review members to protect groups, and more." +date: 2025-07-29 +previewBody: blog_previews/20250729.html +image: images/20250729-join2.png +imageBottom: true +permalink: "/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html" +--- + +# SimpleX Chat v6.4.1: welcome your contacts, review members to protect groups, and more. + +**Published:** Jul 29, 2025 + +**What's new in v6.4.1**: + +- [welcome your contacts](#welcome-your-contacts-the-new-experience-of-making-connections): set your profile bio and welcome message. +- [protect your communities](#protect-your-groups) from spam and abuse: + - review new members ("knocking"), + - moderator role to delegate message moderation to trusted members, + - receive direct feedback from your group members. +- [other improvements](#other-improvements): set default time to delete messages for new contacts. +- [improved app integrity](#improved-app-integrity). + +Also, we added 3 new interface languages to Android and desktop apps: Indonesian, Romanian and Vietnamese. + +Huge thanks to our users who [contributed translations](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +## What's new in v6.4.1 + +### Welcome your contacts: the new experience of making connections + + + +The new simple way to connect to your friends is fully available in this version. + +We received many compliments from our users who started using it in beta versions and in v6.4 about how it simplifies connecting with friends. We agree - this is the biggest UX revolution since the app was released. + +Instead of connecting blindly, and waiting until your contact is online, as it was before, you can now see profile and welcome message of the person you connect to, before you connect. + +When you tap Open new chat you can decide which profile to use to connect or if you want to connect incognito, and in some cases you can include a message with your connection request. + +This way, the conversation with your friends starts even before they connect to you! + +For previously created SimpleX addresses and group links you have an option to upgrade. The links will become short, and will include profile information into link data. Old long links will continue to work, so you won't lose any contacts or members during the upgrade. + +These links are now short enough to be shared in your social media profiles - they are less than 80 characters. + +And as before, it is as secure - servers cannot see you profiles, unless they have the link, and cannot modify them even if they somehow get the link. You can read more about security property and other technical details in our [post about SimpleX protocols extension](./20250703-simplex-network-protocol-extension-for-securely-connecting-people.md) supporting this new user experience. + +Thank you for bringing your friends to SimpleX network! + +### Protect your groups + + + +**Review new members** + +Since v6.4 there are some major improvements in your ability to protect your group from spam and abuse. + +You can enable an option to review all new group members. It is also commonly called "knocking". It allows you to: +- ask prospective members any questions, +- explain the group rules, +- make sure their profile is appropriate for the group, +- decide whether to allow them joining the group, and whether they should be able to send messages in the group. + +Some small groups may enable member review permanently, while larger public groups may enable it temporarily during spam/troll attacks. + +**New role for group moderators** + +In addition to that, there is a new group role - moderator. + +This role allows: +- to approve members in review, +- moderate messages, +- block members for all. + +Unlike admins, moderators can't add new members or permanently remove members from the group. This allows you to delegate group moderation to your community members without risking that they may disrupt the group. + +**Receive direct feedback from group members** + +Your group members now can send messages to group admins. Each conversation with a group member is a mini-group where all group owners, admins and moderators can talk to a member. Reports that members can send [since v6.3](./20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md) are also added to chat with member, allowing you to discuss the report. + +### Other improvements + +**Enable disappearing messages for new contacts** + +Now you can enable disappearing messages for all new contacts automatically. Tap your profile image in the corner, then tap Chat preferences and set time for messages to disappear. + +**Improved message delivery** + +We improved networking layer by increasing request timeouts for all background requests. It substantially reduces traffic on slow networks. + +### Improved app integrity + +**Supply chain security** + +The app security depends on security of its components and its build process, and many of these components are created by third parties. In this version we improved the build process to control the upgrades of these components: +- all 3rd party GitHub actions used during the build are now moved to [the forks we control](https://github.com/simplex-chat?q=action&type=fork&sort=name) - it prevents supply chain attacks via build actions. +- we now build VLC library for all platforms from the source code ourselves, in [this repository](https://github.com/simplex-chat/vlc). +- SQLCipher and [Haskell dependencies](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/dependencies/HASKELL.md) versions were already "locked" prior to this version. + +**Automatic virus scanning** + +We now run automatic daily virus scanning of all apps released via GitHub using [VirusTotal.com](https://www.virustotal.com/). + +You can see the scan results [here](https://github.com/simplex-chat/simplex-virutstotal-scan). + +**Reproducible builds** + +In addition to [server builds](https://github.com/simplex-chat/simplexmq/releases/tag/v6.4.1) that were reproducible since v6.3, the builds of Linux CLI and desktop apps are now reproducible too. You can build Linux apps from source using [this script](https://github.com/simplex-chat/simplex-chat/blob/master/scripts/simplex-chat-reproduce-builds.sh). + +*Please note*: Linux package upgrades may change the build. + +Stable builds of Linux apps are now independently reproduced and [signed by our and Flux teams](https://github.com/simplex-chat/simplex-chat/releases/tag/v6.4.1) - it verifies the integrity of GitHub builds. + +Huge thanks to [Flux](https://runonflux.com/) for doing that and for providing their servers via the app. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/README.md b/blog/README.md index 1432d95de5..4544dc0f45 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,47 @@ # Blog +Jul 29, 2025 [SimpleX Chat v6.4.1: welcome your contacts, review members to protect groups, and more.](./20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md) + +What's new in v6.4.1: + +- welcome your contacts: set your profile bio and welcome message. +- protect your communities from spam and abuse: + - review new members ("knocking"), + - moderator role to delegate message moderation to trusted members, + - receive direct feedback from your group members. +- set default time to delete messages for new contacts. +- improved app integrity: Linux app builds are now reproducible. + +Also, we added 3 new interface languages to Android and desktop apps, thanks to our users: Indonesian, Romanian and Vietnamese. + +--- + +Jul 3, 2025 [SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4](./20250703-simplex-network-protocol-extension-for-securely-connecting-people.md) + +Now you can start talking to your contacts much faster, as soon as you scan the link. This technical post covers the technology that enabled this new user experience — short links and associated data of messaging queues. + +--- + +Mar 8, 2025 [SimpleX Chat v6.3: new user experience and safety in public groups](./20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html) + +What's new in v6.3: +- preventing spam and abuse in public groups. +- group improvements: mention other members and improved performance. +- better chat navigation: organize chats into lists and jump to found and forwarded messages. +- privacy and security improvements: chat retention period and private media file names. + +Also, we added Catalan interface language to Android and desktop apps, thanks to our users and Weblate. + +The last but not the least - server builds are now reproducible! + +--- + +Jan 14, 2025 [SimpleX network: large groups and privacy-preserving content moderation](./20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md) + +This post explains how server operators can moderate end-to-end encrypted conversations without compromising user privacy or end-to-end encryption. + +--- + Dec 10, 2024 [SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) - SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app to improve metadata privacy in SimpleX network. @@ -7,7 +49,7 @@ Dec 10, 2024 [SimpleX network: preset servers operated by Flux, business chats a - Better user experience: open on the first unread, jump to quoted messages, see who reacted. - Improving notifications in iOS app. --- +--- Nov 25, 2024 [Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md) @@ -19,7 +61,7 @@ Nov 25, 2024 [Servers operated by Flux - true privacy and decentralization for a --- -Oct 14, 2024 [SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) +Oct 14, 2024 [SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience](./20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) New security audit: Trail of Bits reviewed the cryptographic design of protocols used in SimpleX network and apps. diff --git a/blog/images/20240314-comparison.jpg b/blog/images/20240314-comparison.jpg index 5ff22be005..815922aa7a 100644 Binary files a/blog/images/20240314-comparison.jpg and b/blog/images/20240314-comparison.jpg differ diff --git a/blog/images/20250308-captcha.png b/blog/images/20250308-captcha.png new file mode 100644 index 0000000000..7f43b47bd1 Binary files /dev/null and b/blog/images/20250308-captcha.png differ diff --git a/blog/images/20250308-lists.png b/blog/images/20250308-lists.png new file mode 100644 index 0000000000..cd15adbcbc Binary files /dev/null and b/blog/images/20250308-lists.png differ diff --git a/blog/images/20250308-mentions.png b/blog/images/20250308-mentions.png new file mode 100644 index 0000000000..658c7e0011 Binary files /dev/null and b/blog/images/20250308-mentions.png differ diff --git a/blog/images/20250308-reports.png b/blog/images/20250308-reports.png new file mode 100644 index 0000000000..def06bf83e Binary files /dev/null and b/blog/images/20250308-reports.png differ diff --git a/blog/images/20250703-card.jpg b/blog/images/20250703-card.jpg new file mode 100644 index 0000000000..937c70911d Binary files /dev/null and b/blog/images/20250703-card.jpg differ diff --git a/blog/images/20250703-connect1.png b/blog/images/20250703-connect1.png new file mode 100644 index 0000000000..18638a9b77 Binary files /dev/null and b/blog/images/20250703-connect1.png differ diff --git a/blog/images/20250703-connect1a.png b/blog/images/20250703-connect1a.png new file mode 100644 index 0000000000..0d7bb37ffd Binary files /dev/null and b/blog/images/20250703-connect1a.png differ diff --git a/blog/images/20250703-connect2.png b/blog/images/20250703-connect2.png new file mode 100644 index 0000000000..87ad2458bb Binary files /dev/null and b/blog/images/20250703-connect2.png differ diff --git a/blog/images/20250729-connect1.png b/blog/images/20250729-connect1.png new file mode 100644 index 0000000000..984e4e44ef Binary files /dev/null and b/blog/images/20250729-connect1.png differ diff --git a/blog/images/20250729-connect2.png b/blog/images/20250729-connect2.png new file mode 100644 index 0000000000..784ea74e84 Binary files /dev/null and b/blog/images/20250729-connect2.png differ diff --git a/blog/images/20250729-join1.png b/blog/images/20250729-join1.png new file mode 100644 index 0000000000..6cec898eaf Binary files /dev/null and b/blog/images/20250729-join1.png differ diff --git a/blog/images/20250729-join2.png b/blog/images/20250729-join2.png new file mode 100644 index 0000000000..43fa412112 Binary files /dev/null and b/blog/images/20250729-join2.png differ diff --git a/bots/README.md b/bots/README.md new file mode 100644 index 0000000000..9449e9d847 --- /dev/null +++ b/bots/README.md @@ -0,0 +1,199 @@ +# SimpleX Chat bot API + +- [Why create a bot](#why-create-a-bot) +- [What is SimpleX bot](#what-is-simplex-bot) +- [How to configure bot profile](#how-to-configure-bot-profile) +- [How to create a bot](#how-to-create-a-bot) +- [Sending commands](#sending-commands) +- [Processing events](#processing-events) +- [Security considerations](#security-considerations) +- [Useful bots](#useful-bots) +- [API types reference](./api/README.md) (another page) + + +## Why create a bot + +You can implement SimpleX Chat for these and many other scenarios: +- customer support - both as a single- and a multi-agent support chat (using SimpleX Chat [business address](https://simplex.chat/docs/business.html) feature), +- information search and retrieval bots, with or without LLM integration, +- moderation bots, to moderate your group and communities. +- broadcast bot, when messages from your trusted users are forwarded to all connected contacts - e.g., see our SimpleX Status bot in the app ([source code](../apps/simplex-broadcast-bot/)), +- feedback bot, when messages from connected contacts are forwarded to a preset list of your trusted users, +- P2P trading bots, connecting buyers and sellers, +- etc. + +We will share all useful bots you create in the bottom of this page - please submit a PR to add it. + + +## What is SimpleX bot + +SimpleX bot is a participant of SimpleX network. Theoretically, bot can do everything that a usual SimpleX Chat user can do – send and receive messages and files, connect to addresses and join groups, etc. But to be useful, a bot should distinguish itself as a bot, and to provide an interface for the users to interact with it. + +## How to configure bot profile + +Starting from v6.4.3, SimpleX Chat apps support bot configuration to distinguish bots, to highlight commands in messages, and to show command menus. + +### Set up bot profile + +To distinguish SimpleX user profile as a bot, set its `peerType` property to `"bot"`. It can be done in one of these ways: +- using CLI options `--create-bot-display-name` and `create-bot-allow-files` when first starting CLI to create bot profile, +- using command `/create bot [files=on] [ ]` (if name contains spaces, it must be in single quotes), when creating additional bot profiles in the same database, +- by configuring bot commands that the users will see in the UI when they type `/` character or tap `//` button with `/set bot commands ...` CLI command (see syntax below), +- by using [APIUpdateProfile](./api/COMMANDS.md#apiupdateprofile) bot command to set `peerType` and configure bot commands at the same time. + +### Configure bot commands + +Bot commands are messages that start from `/` character. Normally, they would consist of lowercase latin letters, but commands can use any letters, digits and underscores. Commands can have parameters. + +All commands in messages will be highlighted in the chats with the bot, and when users tap them, they will be instantly sent. If the message has a single line and starts from `/` character, the whole message will be highlighted. Otherwise, if command is included as part of the message, it will be highlighted until the first space after `/` character: e.g., `/list` command in Directory service shows user's groups. + +*Please note*: commands in messages will be highlighted based purely on `/` character, regardless of whether they are supported by the bot or included in bot configuration. It allows bots to have "hidden" commands that bot would support, but that won't be shown in the menu. But it may also lead to mistakes if bot sends incorrect commands in the instructions to the users. + +Bots can also send highlighted commands with parameters. To do that, bots should surround both command and its parameters in single quotes: e.g., `/'role 2'`. Quotes won't show in the apps UI, and if the user taps this command, it will be sent as `/role 2`. + +Configured bot commands will be be offered to the users as a menu, and for quick lookup as the user types. + +Bot commands configuration is a property in `preferences` object in bot profile received by the user. These preferences can be configured both on the bot user profile level, to offer the same commands to all connected users, and as overrides for specific contacts, to offer different commands to different bot contacts. + +Configuring commands in bot user can be done either with [APIUpdateProfile](./api/COMMANDS.md#apiupdateprofile) or with `/set bot commands` CLI command: + +``` +/set bot commands +``` + +where: + +``` +commands = [,...] +commandOrMenu = command | menu +command = '