diff --git a/.github/actions/prepare-build/action.yml b/.github/actions/prepare-build/action.yml new file mode 100644 index 0000000000..ce75b7a57c --- /dev/null +++ b/.github/actions/prepare-build/action.yml @@ -0,0 +1,47 @@ +name: "Prebuilt steps for build" +description: "Reusable steps for multiple jobs" +inputs: + java_ver: + required: true + description: "Java version to install" + ghc_ver: + required: true + description: "GHC version to install" + github_ref: + required: true + description: "Git reference" + os: + required: true + description: "Target OS" + cache_path: + required: false + default: "~/.cabal/store" + description: "Cache path" + cabal_ver: + required: false + default: 3.10.1.0 + description: "GHC version to install" +runs: + using: "composite" + steps: + - name: Setup Haskell + uses: simplex-chat/setup-haskell-action@v2 + with: + ghc-version: ${{ inputs.ghc_ver }} + cabal-version: ${{ inputs.cabal_ver }} + + - name: Setup Java + if: startsWith(inputs.github_ref, 'refs/tags/v') + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: ${{ inputs.java_ver }} + cache: 'gradle' + + - name: Restore cached build + uses: actions/cache@v4 + with: + path: | + ${{ inputs.cache_path }} + dist-newstyle + key: ${{ inputs.os }}-ghc${{ inputs.ghc_ver }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} diff --git a/.github/actions/prepare-release/action.yml b/.github/actions/prepare-release/action.yml new file mode 100644 index 0000000000..e0d32bd596 --- /dev/null +++ b/.github/actions/prepare-release/action.yml @@ -0,0 +1,39 @@ +name: "Upload binary and update hash" +description: "Reusable steps for multiple jobs" +inputs: + bin_path: + required: true + description: "Path to binary to upload" + bin_name: + required: true + description: "Name of uploaded binary" + bin_hash: + required: true + description: "Message with SHA to include in release" + github_ref: + required: true + description: "Github reference" + github_token: + required: true + description: "Github token" +runs: + using: "composite" + steps: + - name: Upload file with specific name + if: startsWith(inputs.github_ref, 'refs/tags/v') + uses: simplex-chat/upload-release-action@v2 + with: + repo_token: ${{ inputs.github_token }} + file: ${{ inputs.bin_path }} + asset_name: ${{ inputs.bin_name }} + tag: ${{ inputs.github_ref }} + + - name: Add hash to release notes + if: startsWith(inputs.github_ref, 'refs/tags/v') + uses: simplex-chat/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + with: + append_body: true + body: | + ${{ inputs.bin_hash }} diff --git a/.github/actions/swap/action.yml b/.github/actions/swap/action.yml new file mode 100644 index 0000000000..87d670b147 --- /dev/null +++ b/.github/actions/swap/action.yml @@ -0,0 +1,44 @@ +name: 'Set Swap Space' +description: 'Add moar swap' +branding: + icon: 'crop' + color: 'orange' +inputs: + swap-size-gb: + description: 'Swap space to create, in Gigabytes.' + required: false + default: '10' +runs: + using: "composite" + steps: + - name: Swap space report before modification + shell: bash + run: | + echo "Memory and swap:" + free -h + echo + swapon --show + echo + - name: Set Swap + shell: bash + run: | + export SWAP_FILE=$(swapon --show=NAME | tail -n 1) + echo "Swap file: $SWAP_FILE" + if [ -z "$SWAP_FILE" ]; then + SWAP_FILE=/opt/swapfile + else + sudo swapoff $SWAP_FILE + sudo rm $SWAP_FILE + fi + sudo fallocate -l ${{ inputs.swap-size-gb }}G $SWAP_FILE + sudo chmod 600 $SWAP_FILE + sudo mkswap $SWAP_FILE + sudo swapon $SWAP_FILE + - name: Swap space report after modification + shell: bash + run: | + echo "Memory and swap:" + free -h + echo + swapon --show + echo diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2338258d82..ca1bc79510 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,183 +94,295 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build: - name: build-${{ matrix.os }}-${{ matrix.ghc }} - if: always() - needs: prepare-release - runs-on: ${{ matrix.os }} +# ========================= +# Linux Build +# ========================= + + build-linux: + name: "ubuntu-${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" + needs: [maybe-release, variables] + runs-on: ubuntu-${{ matrix.os }} strategy: fail-fast: false matrix: include: - - os: ubuntu-20.04 + - os: 22.04 ghc: "8.10.7" - cache_path: ~/.cabal/store - - os: ubuntu-20.04 - ghc: "9.6.3" - cache_path: ~/.cabal/store - asset_name: simplex-chat-ubuntu-20_04-x86-64 - desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb - - os: ubuntu-22.04 - ghc: "9.6.3" - cache_path: ~/.cabal/store - asset_name: simplex-chat-ubuntu-22_04-x86-64 + should_run: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} + - os: 22.04 + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-ubuntu-22_04-x86-64 desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb - - os: macos-latest - ghc: "9.6.3" - cache_path: ~/.cabal/store - asset_name: simplex-chat-macos-aarch64 - desktop_asset_name: simplex-desktop-macos-aarch64.dmg - - os: macos-13 - ghc: "9.6.3" - cache_path: ~/.cabal/store - asset_name: simplex-chat-macos-x86-64 - desktop_asset_name: simplex-desktop-macos-x86_64.dmg - - os: windows-latest - ghc: "9.6.3" - cache_path: C:/cabal - asset_name: simplex-chat-windows-x86-64 - desktop_asset_name: simplex-desktop-windows-x86_64.msi - + should_run: true + - os: 24.04 + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-ubuntu-24_04-x86-64 + desktop_asset_name: simplex-desktop-ubuntu-24_04-x86_64.deb + should_run: true steps: - - name: 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 + + # Otherwise we run out of disk space with Docker build + - name: Free disk space + if: matrix.should_run == true + shell: bash + run: ./scripts/ci/linux_util_free_space.sh - name: Restore cached build - id: restore_cache - uses: actions/cache/restore@v3 + if: matrix.should_run == true + uses: actions/cache@v4 with: path: | - ${{ matrix.cache_path }} + ~/.cabal/store dist-newstyle - key: ${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} + key: ubuntu-${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} - # / Unix + - name: Set up Docker Buildx + if: matrix.should_run == true + uses: simplex-chat/docker-setup-buildx-action@v3 - - name: Unix prepare cabal.project.local for Mac - if: matrix.os == 'macos-latest' + - name: Build and cache Docker image + if: matrix.should_run == true + uses: simplex-chat/docker-build-push-action@v6 + with: + context: . + load: true + file: Dockerfile.build + tags: build/${{ matrix.os }}:latest + build-args: | + TAG=${{ matrix.os }} + GHC=${{ matrix.ghc }} + + # Docker needs these flags for AppImage build: + # --device /dev/fuse + # --cap-add SYS_ADMIN + # --security-opt apparmor:unconfined + - name: Start container + if: matrix.should_run == true shell: bash run: | - echo "ignore-project: False" >> cabal.project.local - echo "package simplexmq" >> cabal.project.local - echo " extra-include-dirs: /opt/homebrew/opt/openssl@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 + 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 - - - 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 openssl for Mac - if: matrix.os == 'macos-latest' || matrix.os == 'macos-13' - run: brew install openssl@3.0 - - - name: Unix prepare cabal.project.local for Ubuntu - if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04' + - name: Prepare cabal.project.local + if: matrix.should_run == true shell: bash run: | echo "ignore-project: False" >> cabal.project.local echo "package direct-sqlcipher" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local - - name: Unix build CLI - id: unix_cli_build - if: matrix.os != 'windows-latest' + # chmod/git commands are used to workaround permission issues when cache is restored + - name: Build CLI + if: matrix.should_run == true + shell: docker exec -t builder sh -eu {0} + run: | + chmod -R 777 dist-newstyle ~/.cabal && git config --global --add safe.directory '*' + cabal clean + cabal update + cabal build -j --enable-tests + mkdir -p /out + for i in simplex-chat simplex-chat-test; do + bin=$(find /project/dist-newstyle -name "$i" -type f -executable) + chmod +x "$bin" + mv "$bin" /out/ + done + strip /out/simplex-chat + + - name: Copy tests from container + if: matrix.should_run == true shell: bash run: | - cabal build --enable-tests - path=$(cabal list-bin simplex-chat) - echo "bin_path=$path" >> $GITHUB_OUTPUT - echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + docker cp builder:/out/simplex-chat-test . - - name: Unix upload CLI binary to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os != 'windows-latest' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.unix_cli_build.outputs.bin_path }} - asset_name: ${{ matrix.asset_name }} - tag: ${{ github.ref }} - - - name: Unix update CLI binary hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os != 'windows-latest' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.unix_cli_build.outputs.bin_hash }} - - - name: Setup Java - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name - uses: actions/setup-java@v3 - with: - distribution: 'corretto' - java-version: '17' - cache: 'gradle' - - - name: Linux build desktop - id: linux_desktop_build - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') + - name: Copy CLI from container and prepare it + id: linux_cli_prepare + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: bash + run: | + docker cp builder:/out/simplex-chat ./${{ matrix.cli_asset_name }} + path="${{ github.workspace }}/${{ matrix.cli_asset_name }}" + echo "bin_path=$path" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-256\(${{ matrix.cli_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Upload CLI + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + uses: ./.github/actions/prepare-release + with: + bin_path: ${{ steps.linux_cli_prepare.outputs.bin_path }} + bin_name: ${{ matrix.cli_asset_name }} + bin_hash: ${{ steps.linux_cli_prepare.outputs.bin_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Desktop + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + shell: docker exec -t builder sh -eu {0} run: | scripts/desktop/build-lib-linux.sh cd apps/multiplatform ./gradlew packageDeb - path=$(echo $PWD/release/main/deb/simplex_*_amd64.deb) - echo "package_path=$path" >> $GITHUB_OUTPUT - echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Linux make AppImage - id: linux_appimage_build - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04' + - name: Prepare Desktop + id: linux_desktop_build + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: bash run: | - scripts/desktop/make-appimage-linux.sh - path=$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage) - echo "appimage_path=$path" >> $GITHUB_OUTPUT - echo "appimage_hash=$(echo SHA2-512\(simplex-desktop-x86_64.AppImage\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/deb/simplex_*_amd64.deb ) + echo "package_path=$path" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Mac build desktop + - name: Upload Desktop + uses: ./.github/actions/prepare-release + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + with: + bin_path: ${{ steps.linux_desktop_build.outputs.package_path }} + bin_name: ${{ matrix.desktop_asset_name }} + bin_hash: ${{ steps.linux_desktop_build.outputs.package_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build AppImage + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true + shell: docker exec -t builder sh -eu {0} + run: | + scripts/desktop/make-appimage-linux.sh + + - name: Prepare AppImage + id: linux_appimage_build + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true + shell: bash + run: | + path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/*imple*.AppImage) + echo "appimage_path=$path" >> $GITHUB_OUTPUT + echo "appimage_hash=$(echo SHA2-256\(simplex-desktop-x86_64.AppImage\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Upload AppImage + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true + uses: ./.github/actions/prepare-release + with: + bin_path: ${{ steps.linux_appimage_build.outputs.appimage_path }} + bin_name: "simplex-desktop-x86_64.AppImage" + bin_hash: ${{ steps.linux_appimage_build.outputs.appimage_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fix permissions for cache + if: matrix.should_run == true + shell: bash + run: | + sudo chmod -R 777 dist-newstyle ~/.cabal + sudo chown -R $(id -u):$(id -g) dist-newstyle ~/.cabal + + - name: Run tests + if: matrix.should_run == true + timeout-minutes: 120 + shell: bash + run: | + i=1 + attempts=1 + ${{ (github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} && attempts=3 + while [ "$i" -le "$attempts" ]; do + if ./simplex-chat-test; then + break + else + echo "Attempt $i failed, retrying..." + i=$((i + 1)) + sleep 1 + fi + done + if [ "$i" -gt "$attempts" ]; then + echo "All "$attempts" attempts failed." + exit 1 + fi + +# ========================= +# MacOS Build +# ========================= + + build-macos: + name: "${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" + needs: [maybe-release, variables] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-macos-aarch64 + desktop_asset_name: simplex-desktop-macos-aarch64.dmg + openssl_dir: "/opt/homebrew/opt" + - os: macos-13 + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-macos-x86-64 + desktop_asset_name: simplex-desktop-macos-x86_64.dmg + openssl_dir: "/usr/local/opt" + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Prepare build + uses: ./.github/actions/prepare-build + with: + java_ver: ${{ needs.variables.outputs.JAVA_VER }} + ghc_ver: ${{ matrix.ghc }} + os: ${{ matrix.os }} + github_ref: ${{ github.ref }} + + - name: Install OpenSSL + run: brew install openssl@3.0 + + - name: Prepare cabal.project.local + shell: bash + run: | + echo "ignore-project: False" >> cabal.project.local + echo "package simplexmq" >> cabal.project.local + echo " extra-include-dirs: ${{ matrix.opnessl_dir }}/openssl@3.0/include" >> cabal.project.local + echo " extra-lib-dirs: ${{ matrix.openssl_dir}}/openssl@3.0/lib" >> cabal.project.local + echo "" >> cabal.project.local + echo "package direct-sqlcipher" >> cabal.project.local + echo " extra-include-dirs: ${{ matrix.openssl_dir }}/openssl@3.0/include" >> cabal.project.local + echo " extra-lib-dirs: ${{ matrix.openssl_dir }}/openssl@3.0/lib" >> cabal.project.local + echo " flags: +openssl" >> cabal.project.local + + - name: Build CLI + id: mac_cli_build + shell: bash + run: | + cabal build -j --enable-tests + path=$(cabal list-bin simplex-chat) + echo "bin_path=$path" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-256\(${{ matrix.cli_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Upload CLI + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release + with: + bin_path: ${{ steps.mac_cli_build.outputs.bin_path }} + bin_name: ${{ matrix.cli_asset_name }} + bin_hash: ${{ steps.mac_cli_build.outputs.bin_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Desktop id: mac_desktop_build - if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13') + if: startsWith(github.ref, 'refs/tags/v') shell: bash env: APPLE_SIMPLEX_SIGNING_KEYCHAIN: ${{ secrets.APPLE_SIMPLEX_SIGNING_KEYCHAIN }} @@ -238,88 +392,77 @@ jobs: scripts/ci/build-desktop-mac.sh path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg) echo "package_path=$path" >> $GITHUB_OUTPUT - echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Linux upload desktop package to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') - uses: svenstaro/upload-release-action@v2 + - name: Upload Desktop + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.linux_desktop_build.outputs.package_path }} - asset_name: ${{ matrix.desktop_asset_name }} - tag: ${{ github.ref }} + bin_path: ${{ steps.mac_desktop_build.outputs.package_path }} + bin_name: ${{ matrix.desktop_asset_name }} + bin_hash: ${{ steps.mac_desktop_build.outputs.package_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Linux update desktop package hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.linux_desktop_build.outputs.package_hash }} - - - name: Linux upload AppImage to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.linux_appimage_build.outputs.appimage_path }} - asset_name: simplex-desktop-x86_64.AppImage - tag: ${{ github.ref }} - - - name: Linux update AppImage hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.linux_appimage_build.outputs.appimage_hash }} - - - name: Mac upload desktop package to release - if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13') - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.mac_desktop_build.outputs.package_path }} - asset_name: ${{ matrix.desktop_asset_name }} - tag: ${{ github.ref }} - - - name: Mac update desktop package hash - if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13') - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.mac_desktop_build.outputs.package_hash }} - - - name: Cache unix build - uses: actions/cache/save@v3 - if: matrix.os != 'windows-latest' - with: - path: | - ${{ matrix.cache_path }} - dist-newstyle - key: ${{ steps.restore_cache.outputs.cache-primary-key }} - - - name: Unix test - if: matrix.os != 'windows-latest' - timeout-minutes: 40 + - name: Run tests + timeout-minutes: 120 shell: bash - run: cabal test --test-show-details=direct + run: | + i=1 + attempts=1 + ${{ (github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} && attempts=3 + while [ "$i" -le "$attempts" ]; do + if cabal test --test-show-details=direct; then + break + else + echo "Attempt $i failed, retrying..." + i=$((i + 1)) + sleep 1 + fi + done + if [ "$i" -gt "$attempts" ]; then + echo "All "$attempts" attempts failed." + exit 1 + fi - # Unix / +# ========================= +# Windows Build +# ========================= - # / Windows - # rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing + build-windows: + name: "${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" + needs: [maybe-release, variables] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-windows-x86-64 + desktop_asset_name: simplex-desktop-windows-x86_64.msi + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Prepare build + uses: ./.github/actions/prepare-build + with: + java_ver: ${{ needs.variables.outputs.JAVA_VER }} + ghc_ver: ${{ matrix.ghc }} + os: ${{ matrix.os }} + cache_path: "C:/cabal" + github_ref: ${{ github.ref }} + + - name: Configure pagefile (Windows) + uses: simplex-chat/configure-pagefile-action@v1.4 + with: + minimum-size: 16GB + maximum-size: 16GB + disk-root: "C:" + - name: 'Setup MSYS2' - if: matrix.os == 'windows-latest' - uses: msys2/setup-msys2@v2 + uses: simplex-chat/setup-msys2@v2 with: msystem: ucrt64 update: true @@ -331,10 +474,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) @@ -349,70 +491,42 @@ jobs: rm -rf dist-newstyle/src/direct-sq* sed -i "s/, unix /--, unix /" simplex-chat.cabal - cabal build --enable-tests + cabal build -j --enable-tests rm -rf dist-newstyle/src/direct-sq* path=$(cabal list-bin simplex-chat | tail -n 1) echo "bin_path=$path" >> $GITHUB_OUTPUT - echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-256\(${{ matrix.cli_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Windows upload CLI binary to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' - uses: svenstaro/upload-release-action@v2 + - name: Upload CLI + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.windows_build.outputs.bin_path }} - asset_name: ${{ matrix.asset_name }} - tag: ${{ github.ref }} + bin_path: ${{ steps.windows_cli_build.outputs.bin_path }} + bin_name: ${{ matrix.cli_asset_name }} + bin_hash: ${{ steps.windows_cli_build.outputs.bin_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Windows update CLI binary hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.windows_build.outputs.bin_hash }} - - - name: Windows build desktop + - name: Build Desktop id: windows_desktop_build - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' + if: startsWith(github.ref, 'refs/tags/v') shell: msys2 {0} run: | export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo) scripts/desktop/build-lib-windows.sh cd apps/multiplatform ./gradlew packageMsi + rm -rf dist-newstyle/src/direct-sq* path=$(echo $PWD/release/main/msi/*imple*.msi | sed 's#/\([a-z]\)#\1:#' | sed 's#/#\\#g') echo "package_path=$path" >> $GITHUB_OUTPUT - echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Windows upload desktop package to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' - uses: svenstaro/upload-release-action@v2 + - name: Upload Desktop + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.windows_desktop_build.outputs.package_path }} - asset_name: ${{ matrix.desktop_asset_name }} - tag: ${{ github.ref }} - - - name: Windows update desktop package hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.windows_desktop_build.outputs.package_hash }} - - - name: Cache windows build - uses: actions/cache/save@v3 - if: matrix.os == 'windows-latest' - with: - path: | - ${{ matrix.cache_path }} - dist-newstyle - key: ${{ steps.restore_cache.outputs.cache-primary-key }} - - # Windows / + bin_path: ${{ steps.windows_desktop_build.outputs.package_path }} + bin_name: ${{ matrix.desktop_asset_name }} + bin_hash: ${{ steps.windows_desktop_build.outputs.package_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reproduce-schedule.yml b/.github/workflows/reproduce-schedule.yml new file mode 100644 index 0000000000..7de44addc7 --- /dev/null +++ b/.github/workflows/reproduce-schedule.yml @@ -0,0 +1,45 @@ +name: Reproduce latest release + +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * *' # every day at 02:00 night + +jobs: + reproduce: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Get latest release + shell: bash + run: | + curl --proto '=https' \ + --tlsv1.2 \ + -sSf -L \ + 'https://api.github.com/repos/simplex-chat/simplex-chat/releases/latest' \ + 2>/dev/null | \ + grep -i "tag_name" | \ + awk -F \" '{print "TAG="$4}' >> $GITHUB_ENV + + - name: Execute reproduce script + run: | + ${GITHUB_WORKSPACE}/scripts/reproduce-builds.sh "$TAG" + + - name: Check if build has been reproduced + env: + url: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_URL }} + user: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_USER }} + pass: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_PASS }} + run: | + if [ -f "${GITHUB_WORKSPACE}/$TAG/_sha256sums" ]; then + exit 0 + else + curl --proto '=https' --tlsv1.2 -sSf \ + -u "${user}:${pass}" \ + -H 'Content-Type: application/json' \ + -d '{"title": "👾 GitHub: Runner", "description": "⛔️ '"$TAG"' did not reproduce."}' \ + "$url" + exit 1 + fi diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 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/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..76bb1127f2 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,92 @@ +# syntax=docker/dockerfile:1.7.0-labs +ARG TAG=24.04 +FROM ubuntu:${TAG} AS build + +### Build stage + +ARG GHC=9.6.3 +ARG CABAL=3.10.1.0 +ARG JAVA=17 + +ENV TZ=Etc/UTC \ + DEBIAN_FRONTEND=noninteractive + +# Install curl, git and and simplex-chat dependencies +RUN apt-get update && \ + apt-get install -y curl \ + libpq-dev \ + git \ + sqlite3 \ + libsqlite3-dev \ + build-essential \ + libgmp3-dev \ + zlib1g-dev \ + llvm \ + cmake \ + llvm-dev \ + libnuma-dev \ + libssl-dev \ + desktop-file-utils \ + patchelf \ + ca-certificates \ + zip \ + wget \ + fuse3 \ + file \ + appstream \ + gpg \ + unzip &&\ + ln -s /bin/fusermount /bin/fusermount3 || : + +# Install Java Coretto +# Required, because official Java in Ubuntu +# depends on libjpeg.so.8 and liblcms2.so.2 which are NOT copied into final +# /usr/lib/runtime/lib directory and I do not have time to figure out gradle.kotlin +# to fix this :( +RUN curl --proto '=https' --tlsv1.2 -sSf 'https://apt.corretto.aws/corretto.key' | gpg --dearmor -o /usr/share/keyrings/corretto-keyring.gpg &&\ + echo "deb [signed-by=/usr/share/keyrings/corretto-keyring.gpg] https://apt.corretto.aws stable main" > /etc/apt/sources.list.d/corretto.list &&\ + apt update &&\ + apt install -y java-${JAVA}-amazon-corretto-jdk + +# Specify bootstrap Haskell versions +ENV BOOTSTRAP_HASKELL_GHC_VERSION=${GHC} +ENV BOOTSTRAP_HASKELL_CABAL_VERSION=${CABAL} + +# Do not install Stack +ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK=true +ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK_HOOK=true + +# Install ghcup +RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh + +# Adjust PATH +ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH" + +# Set both as default +RUN ghcup set ghc "${GHC}" && \ + ghcup set cabal "${CABAL}" + +#===================== +# Install Android SDK +#===================== +ARG SDK_VERSION=13114758 + +ENV SDK_VERSION=$SDK_VERSION \ + ANDROID_HOME=/root + +RUN curl -L -o tools.zip "https://dl.google.com/android/repository/commandlinetools-linux-${SDK_VERSION}_latest.zip" && \ + unzip tools.zip && rm tools.zip && \ + mv cmdline-tools tools && mkdir "$ANDROID_HOME/cmdline-tools" && mv tools "$ANDROID_HOME/cmdline-tools/" && \ + ln -s "$ANDROID_HOME/cmdline-tools/tools" "$ANDROID_HOME/cmdline-tools/latest" + +ENV PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin" + +# https://askubuntu.com/questions/885658/android-sdk-repositories-cfg-could-not-be-loaded +RUN mkdir -p ~/.android ~/.gradle && \ + touch ~/.android/repositories.cfg && \ + echo 'org.gradle.console=plain' > ~/.gradle/gradle.properties &&\ + yes | sdkmanager --licenses >/dev/null + +ENV PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools + +WORKDIR /project diff --git a/README.md b/README.md index 936667da8c..554c6068d9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design! -[](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html)     [](https://www.privacyguides.org/en/real-time-communication/#simplex-chat)     [](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) +[](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html)     [](https://www.privacyguides.org/en/real-time-communication/#simplex-chat)     [](https://www.whonix.org/wiki/Chat#Recommendation)     [](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) ## Welcome to SimpleX Chat! @@ -72,7 +72,7 @@ You must: Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment. -You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=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) +You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FiBkJE72asZX1NUZaYFIeKRVk6oVjb-iv%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAinqu3j74AMjODLoIRR487ZW6ysip_dlpD6Zxk18SPFY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22groupLinkId%22%3A%223wAFGCLygQHR5AwynZOHlQ%3D%3D%22%7D) There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform: @@ -110,6 +110,15 @@ After you connect, you can [verify connection security code](./blog/20230103-sim Read about the app features and settings in the new [User guide](./docs/guide/README.md). +## Contribute + +We would love to have you join the development! You can help us with: + +- [share the color theme](./docs/THEMES.md) you use in Android app! +- writing a tutorial or recipes about hosting servers, chat bot automations, etc. +- contributing to SimpleX Chat knowledge-base. +- developing features - please connect to us via chat so we can help you get started. + ## Help translating SimpleX Chat Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages. @@ -141,15 +150,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! @@ -169,6 +169,7 @@ It is possible to donate via: - ETH: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692 - USDT (Ethereum): 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692 - ZEC: t1fwjQW5gpFhDqXNhxqDWyF9j9WeKvVS5Jg +- ZEC shielded: u16rnvkflumf5uw9frngc2lymvmzgdr2mmc9unyu0l44unwfmdcpfm0axujd2w34ct3ye709azxsqge45705lpvvqu264ltzvfay55ygyq - DOGE: D99pV4n9TrPxBPCkQGx4w4SMSa6QjRBxPf - SOL: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu - please ask if you want to donate any other coins. @@ -234,6 +235,10 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Mar 8, 2025. SimpleX Chat v6.3: new user experience and safety in public groups](./blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md) + +[Jan 14, 2025. SimpleX network: large groups and privacy-preserving content moderation](./blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md) + [Dec 10, 2024. SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) [Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) @@ -305,12 +310,13 @@ 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 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 5418f4bcdd..2ad8d546f2 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" } } } @@ -76,7 +74,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. @@ -211,7 +209,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,10 +276,8 @@ 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 } } } @@ -300,14 +296,6 @@ struct ContentView: View { .if(updatedConditions) { v in v.task { await setConditionsNotified_() } } - case .updatedConditions: - UsageConditionsView( - currUserServers: Binding.constant([]), - userServers: Binding.constant([]), - updated: true - ) - .modifier(ThemedBackground(grouped: true)) - .task { await setConditionsNotified_() } } } if chatModel.setDeliveryReceipts { @@ -455,12 +443,12 @@ struct ContentView: View { } func connectViaUrl() { - dismissAllSheets() { - let m = ChatModel.shared - if let url = m.appOpenUrl { - m.appOpenUrl = nil + let m = ChatModel.shared + if let url = m.appOpenUrl { + m.appOpenUrl = nil + dismissAllSheets() { var path = url.path - if (path == "/contact" || path == "/invitation") { + if (path == "/contact" || path == "/invitation" || path == "/a" || path == "/c" || path == "/g" || path == "/i") { path.removeFirst() let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") planAndConnect( diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift new file mode 100644 index 0000000000..3bf4cb7b56 --- /dev/null +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -0,0 +1,2281 @@ +// +// APITypes.swift +// SimpleX +// +// Created by EP on 01/05/2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SimpleXChat +import SwiftUI + +// some constructors are used in SEChatCommand or NSEChatCommand types as well - they must be syncronised +enum ChatCommand: ChatCmdProtocol { + case showActiveUser + case createActiveUser(profile: Profile?, pastTimestamp: Bool) + case listUsers + case apiSetActiveUser(userId: Int64, viewPwd: String?) + case setAllContactReceipts(enable: Bool) + case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) + case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) + case apiHideUser(userId: Int64, viewPwd: String) + case apiUnhideUser(userId: Int64, viewPwd: String) + case apiMuteUser(userId: Int64) + case apiUnmuteUser(userId: Int64) + case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?) + case startChat(mainApp: Bool, enableSndFiles: Bool) + case checkChatRunning + case apiStopChat + case apiActivateChat(restoreChat: Bool) + case apiSuspendChat(timeoutMicroseconds: Int) + case apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) + case apiSetEncryptLocalFiles(enable: Bool) + case apiExportArchive(config: ArchiveConfig) + case apiImportArchive(config: ArchiveConfig) + case apiDeleteStorage + case apiStorageEncryption(config: DBEncryptionConfig) + case testStorageEncryption(key: String) + case apiSaveSettings(settings: AppSettings) + case apiGetSettings(settings: AppSettings) + case apiGetChatTags(userId: Int64) + case apiGetChats(userId: Int64) + case apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String) + case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) + case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + case apiCreateChatTag(tag: ChatTagData) + case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64]) + case apiDeleteChatTag(tagId: Int64) + case apiUpdateChatTag(tagId: Int64, tagData: ChatTagData) + case apiReorderChatTags(tagIds: [Int64]) + case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) + case apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String) + case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool) + case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) + case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64]) + case apiArchiveReceivedReports(groupId: Int64) + case apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode) + case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) + case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction) + case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64]) + case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) + case apiGetNtfToken + case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) + case apiVerifyToken(token: DeviceToken, nonce: String, code: String) + case apiCheckToken(token: DeviceToken) + case apiDeleteToken(token: DeviceToken) + case apiGetNtfConns(nonce: String, encNtfInfo: String) + case apiGetConnNtfMessages(connMsgReqs: [ConnMsgReq]) + case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) + case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) + case apiJoinGroup(groupId: Int64) + case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole) + case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool) + case apiRemoveMembers(groupId: Int64, memberIds: [Int64], withMessages: Bool) + case apiLeaveGroup(groupId: Int64) + case apiListMembers(groupId: Int64) + case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) + case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole, short: Bool) + case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) + case apiDeleteGroupLink(groupId: Int64) + case apiGetGroupLink(groupId: Int64) + case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) + case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) + case apiTestProtoServer(userId: Int64, server: String) + case apiGetServerOperators + case apiSetServerOperators(operators: [ServerOperator]) + case apiGetUserServers(userId: Int64) + case apiSetUserServers(userId: Int64, userServers: [UserOperatorServers]) + case apiValidateServers(userId: Int64, userServers: [UserOperatorServers]) + case apiGetUsageConditions + case apiSetConditionsNotified(conditionsId: Int64) + case apiAcceptConditions(conditionsId: Int64, operatorIds: [Int64]) + case apiSetChatItemTTL(userId: Int64, seconds: Int64) + case apiGetChatItemTTL(userId: Int64) + case apiSetChatTTL(userId: Int64, type: ChatType, id: Int64, seconds: Int64?) + case apiSetNetworkConfig(networkConfig: NetCfg) + case apiGetNetworkConfig + case apiSetNetworkInfo(networkInfo: UserNetworkInfo) + case reconnectAllServers + case reconnectServer(userId: Int64, smpServer: String) + case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) + case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings) + case apiContactInfo(contactId: Int64) + case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) + case apiContactQueueInfo(contactId: Int64) + case apiGroupMemberQueueInfo(groupId: Int64, groupMemberId: Int64) + case apiSwitchContact(contactId: Int64) + case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64) + case apiAbortSwitchContact(contactId: Int64) + case apiAbortSwitchGroupMember(groupId: Int64, groupMemberId: Int64) + case apiSyncContactRatchet(contactId: Int64, force: Bool) + case apiSyncGroupMemberRatchet(groupId: Int64, groupMemberId: Int64, force: Bool) + case apiGetContactCode(contactId: Int64) + case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64) + case apiVerifyContact(contactId: Int64, connectionCode: String?) + case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?) + case apiAddContact(userId: Int64, short: Bool, incognito: Bool) + case apiSetConnectionIncognito(connId: Int64, incognito: Bool) + case apiChangeConnectionUser(connId: Int64, userId: Int64) + case apiConnectPlan(userId: Int64, connLink: String) + case apiConnect(userId: Int64, incognito: Bool, connLink: CreatedConnLink) + case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64) + case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode) + case apiClearChat(type: ChatType, id: Int64) + case apiListContacts(userId: Int64) + case apiUpdateProfile(userId: Int64, profile: Profile) + case apiSetContactPrefs(contactId: Int64, preferences: Preferences) + case apiSetContactAlias(contactId: Int64, localAlias: String) + case apiSetGroupAlias(groupId: Int64, localAlias: String) + case apiSetConnectionAlias(connId: Int64, localAlias: String) + case apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) + case apiSetChatUIThemes(chatId: String, themes: ThemeModeOverrides?) + case apiCreateMyAddress(userId: Int64, short: Bool) + case apiDeleteMyAddress(userId: Int64) + case apiShowMyAddress(userId: Int64) + case apiSetProfileAddress(userId: Int64, on: Bool) + case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?) + case apiAcceptContact(incognito: Bool, contactReqId: Int64) + case apiRejectContact(contactReqId: Int64) + // WebRTC calls + case apiSendCallInvitation(contact: Contact, callType: CallType) + case apiRejectCall(contact: Contact) + case apiSendCallOffer(contact: Contact, callOffer: WebRTCCallOffer) + case apiSendCallAnswer(contact: Contact, answer: WebRTCSession) + case apiSendCallExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo) + case apiEndCall(contact: Contact) + case apiGetCallInvitations + case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) + // WebRTC calls / + case apiGetNetworkStatuses + case apiChatRead(type: ChatType, id: Int64) + case apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) + case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) + case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?) + case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?) + case cancelFile(fileId: Int64) + // remote desktop commands + case setLocalDeviceName(displayName: String) + case connectRemoteCtrl(xrcpInvitation: String) + case findKnownRemoteCtrl + case confirmRemoteCtrl(remoteCtrlId: Int64) + case verifyRemoteCtrlSession(sessionCode: String) + case listRemoteCtrls + case stopRemoteCtrl + case deleteRemoteCtrl(remoteCtrlId: Int64) + case apiUploadStandaloneFile(userId: Int64, file: CryptoFile) + case apiDownloadStandaloneFile(userId: Int64, url: String, file: CryptoFile) + case apiStandaloneFileInfo(url: String) + // misc + case showVersion + case getAgentSubsTotal(userId: Int64) + case getAgentServersSummary(userId: Int64) + case resetAgentServersStats + case string(String) + + var cmdString: String { + get { + switch self { + case .showActiveUser: return "/u" + case let .createActiveUser(profile, pastTimestamp): + let user = NewUser(profile: profile, pastTimestamp: pastTimestamp) + return "/_create user \(encodeJSON(user))" + case .listUsers: return "/users" + case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))" + case let .setAllContactReceipts(enable): return "/set receipts all \(onOff(enable))" + case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings): + let umrs = userMsgReceiptSettings + return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" + case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings): + let umrs = userMsgReceiptSettings + return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" + case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" + case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))" + case let .apiMuteUser(userId): return "/_mute user \(userId)" + case let .apiUnmuteUser(userId): return "/_unmute user \(userId)" + case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))" + case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))" + case .checkChatRunning: return "/_check running" + case .apiStopChat: return "/_stop" + case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" + case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" + case let .apiSetAppFilePaths(filesFolder, tempFolder, assetsFolder): return "/set file paths \(encodeJSON(AppFilePaths(appFilesFolder: filesFolder, appTempFolder: tempFolder, appAssetsFolder: assetsFolder)))" + case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" + case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" + case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" + case .apiDeleteStorage: return "/_db delete" + case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))" + case let .testStorageEncryption(key): return "/db test key \(key)" + case let .apiSaveSettings(settings): return "/_save app settings \(encodeJSON(settings))" + case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))" + case let .apiGetChatTags(userId): return "/_get tags \(userId)" + case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" + case let .apiGetChat(chatId, pagination, search): return "/_get chat \(chatId) \(pagination.cmdString)" + + (search == "" ? "" : " search=\(search)") + case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)" + case let .apiSendMessages(type, id, live, ttl, composedMessages): + let msgs = encodeJSON(composedMessages) + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))" + case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id)) \(tagIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)" + case let .apiUpdateChatTag(tagId, tagData): return "/_update tag \(tagId) \(encodeJSON(tagData))" + case let .apiReorderChatTags(tagIds): return "/_reorder tags \(tagIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiCreateChatItems(noteFolderId, composedMessages): + let msgs = encodeJSON(composedMessages) + return "/_create *\(noteFolderId) json \(msgs)" + case let .apiReportMessage(groupId, chatItemId, reportReason, reportText): + return "/_report #\(groupId) \(chatItemId) reason=\(reportReason) \(reportText)" + case let .apiUpdateChatItem(type, id, itemId, um, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(um.cmdString)" + case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" + case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiArchiveReceivedReports(groupId): return "/_archive reports #\(groupId)" + case let .apiDeleteReceivedReports(groupId, itemIds, mode): return "/_delete reports #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" + case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" + case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))" + case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl): + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)" + case .apiGetNtfToken: return "/_ntf get " + case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" + case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" + case let .apiCheckToken(token): return "/_ntf check \(token.cmdString)" + case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" + case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)" + case let .apiGetConnNtfMessages(connMsgReqs): return "/_ntf conn messages \(connMsgReqs.map { $0.cmdString }.joined(separator: ","))" + case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))" + case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" + case let .apiJoinGroup(groupId): return "/_join #\(groupId)" + case let .apiMembersRole(groupId, memberIds, memberRole): return "/_member role #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) \(memberRole.rawValue)" + case let .apiBlockMembersForAll(groupId, memberIds, blocked): return "/_block #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) blocked=\(onOff(blocked))" + case let .apiRemoveMembers(groupId, memberIds, withMessages): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) messages=\(onOff(withMessages))" + case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)" + case let .apiListMembers(groupId): return "/_members #\(groupId)" + case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))" + case let .apiCreateGroupLink(groupId, memberRole, short): return "/_create link #\(groupId) \(memberRole) short=\(onOff(short))" + case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)" + case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)" + case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" + case let .apiCreateMemberContact(groupId, groupMemberId): return "/_create member contact #\(groupId) \(groupMemberId)" + case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)" + case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)" + case .apiGetServerOperators: return "/_operators" + case let .apiSetServerOperators(operators): return "/_operators \(encodeJSON(operators))" + case let .apiGetUserServers(userId): return "/_servers \(userId)" + case let .apiSetUserServers(userId, userServers): return "/_servers \(userId) \(encodeJSON(userServers))" + case let .apiValidateServers(userId, userServers): return "/_validate_servers \(userId) \(encodeJSON(userServers))" + case .apiGetUsageConditions: return "/_conditions" + case let .apiSetConditionsNotified(conditionsId): return "/_conditions_notified \(conditionsId)" + case let .apiAcceptConditions(conditionsId, operatorIds): return "/_accept_conditions \(conditionsId) \(joinedIds(operatorIds))" + case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))" + case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)" + case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id)) \(chatItemTTLStr(seconds: seconds))" + case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" + case .apiGetNetworkConfig: return "/network" + case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))" + case .reconnectAllServers: return "/reconnect" + case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)" + case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" + case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" + case let .apiContactInfo(contactId): return "/_info @\(contactId)" + case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" + case let .apiContactQueueInfo(contactId): return "/_queue info @\(contactId)" + case let .apiGroupMemberQueueInfo(groupId, groupMemberId): return "/_queue info #\(groupId) \(groupMemberId)" + case let .apiSwitchContact(contactId): return "/_switch @\(contactId)" + case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)" + case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)" + case let .apiAbortSwitchGroupMember(groupId, groupMemberId): return "/_abort switch #\(groupId) \(groupMemberId)" + case let .apiSyncContactRatchet(contactId, force): if force { + return "/_sync @\(contactId) force=on" + } else { + return "/_sync @\(contactId)" + } + case let .apiSyncGroupMemberRatchet(groupId, groupMemberId, force): if force { + return "/_sync #\(groupId) \(groupMemberId) force=on" + } else { + return "/_sync #\(groupId) \(groupMemberId)" + } + case let .apiGetContactCode(contactId): return "/_get code @\(contactId)" + case let .apiGetGroupMemberCode(groupId, groupMemberId): return "/_get code #\(groupId) \(groupMemberId)" + case let .apiVerifyContact(contactId, .some(connectionCode)): return "/_verify code @\(contactId) \(connectionCode)" + case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)" + case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)" + case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)" + case let .apiAddContact(userId, short, incognito): return "/_connect \(userId) short=\(onOff(short)) incognito=\(onOff(incognito))" + case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" + case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" + case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)" + case let .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")" + case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)" + case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)" + case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" + case let .apiListContacts(userId): return "/_contacts \(userId)" + case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))" + case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))" + case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))" + case let .apiSetGroupAlias(groupId, localAlias): return "/_set alias #\(groupId) \(localAlias.trimmingCharacters(in: .whitespaces))" + case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))" + case let .apiSetUserUIThemes(userId, themes): return "/_set theme user \(userId) \(themes != nil ? encodeJSON(themes) : "")" + case let .apiSetChatUIThemes(chatId, themes): return "/_set theme \(chatId) \(themes != nil ? encodeJSON(themes) : "")" + case let .apiCreateMyAddress(userId, short): return "/_address \(userId) short=\(onOff(short))" + case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" + case let .apiShowMyAddress(userId): return "/_show_address \(userId)" + case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" + case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))" + case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)" + case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" + case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))" + case let .apiRejectCall(contact): return "/_call reject @\(contact.apiId)" + case let .apiSendCallOffer(contact, callOffer): return "/_call offer @\(contact.apiId) \(encodeJSON(callOffer))" + case let .apiSendCallAnswer(contact, answer): return "/_call answer @\(contact.apiId) \(encodeJSON(answer))" + case let .apiSendCallExtraInfo(contact, extraInfo): return "/_call extra @\(contact.apiId) \(encodeJSON(extraInfo))" + case let .apiEndCall(contact): return "/_call end @\(contact.apiId)" + case .apiGetCallInvitations: return "/_call get" + case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" + case .apiGetNetworkStatuses: return "/_network_statuses" + case let .apiChatRead(type, id): return "/_read chat \(ref(type, id))" + case let .apiChatItemsRead(type, id, itemIds): return "/_read chat items \(ref(type, id)) \(joinedIds(itemIds))" + case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" + case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" + case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))" + case let .cancelFile(fileId): return "/fcancel \(fileId)" + case let .setLocalDeviceName(displayName): return "/set device name \(displayName)" + case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)" + case .findKnownRemoteCtrl: return "/find remote ctrl" + case let .confirmRemoteCtrl(rcId): return "/confirm remote ctrl \(rcId)" + case let .verifyRemoteCtrlSession(sessCode): return "/verify remote ctrl \(sessCode)" + case .listRemoteCtrls: return "/list remote ctrls" + case .stopRemoteCtrl: return "/stop remote ctrl" + case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)" + case let .apiUploadStandaloneFile(userId, file): return "/_upload \(userId) \(file.filePath)" + case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)" + case let .apiStandaloneFileInfo(link): return "/_download info \(link)" + case .showVersion: return "/version" + case let .getAgentSubsTotal(userId): return "/get subs total \(userId)" + case let .getAgentServersSummary(userId): return "/get servers summary \(userId)" + case .resetAgentServersStats: return "/reset servers stats" + case let .string(str): return str + } + } + } + + var cmdType: String { + get { + switch self { + case .showActiveUser: return "showActiveUser" + case .createActiveUser: return "createActiveUser" + case .listUsers: return "listUsers" + case .apiSetActiveUser: return "apiSetActiveUser" + case .setAllContactReceipts: return "setAllContactReceipts" + case .apiSetUserContactReceipts: return "apiSetUserContactReceipts" + case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts" + case .apiHideUser: return "apiHideUser" + case .apiUnhideUser: return "apiUnhideUser" + case .apiMuteUser: return "apiMuteUser" + case .apiUnmuteUser: return "apiUnmuteUser" + case .apiDeleteUser: return "apiDeleteUser" + case .startChat: return "startChat" + case .checkChatRunning: return "checkChatRunning" + case .apiStopChat: return "apiStopChat" + case .apiActivateChat: return "apiActivateChat" + case .apiSuspendChat: return "apiSuspendChat" + case .apiSetAppFilePaths: return "apiSetAppFilePaths" + case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles" + case .apiExportArchive: return "apiExportArchive" + case .apiImportArchive: return "apiImportArchive" + case .apiDeleteStorage: return "apiDeleteStorage" + case .apiStorageEncryption: return "apiStorageEncryption" + case .testStorageEncryption: return "testStorageEncryption" + case .apiSaveSettings: return "apiSaveSettings" + case .apiGetSettings: return "apiGetSettings" + case .apiGetChatTags: return "apiGetChatTags" + case .apiGetChats: return "apiGetChats" + case .apiGetChat: return "apiGetChat" + case .apiGetChatItemInfo: return "apiGetChatItemInfo" + case .apiSendMessages: return "apiSendMessages" + case .apiCreateChatTag: return "apiCreateChatTag" + case .apiSetChatTags: return "apiSetChatTags" + case .apiDeleteChatTag: return "apiDeleteChatTag" + case .apiUpdateChatTag: return "apiUpdateChatTag" + case .apiReorderChatTags: return "apiReorderChatTags" + case .apiCreateChatItems: return "apiCreateChatItems" + case .apiReportMessage: return "apiReportMessage" + case .apiUpdateChatItem: return "apiUpdateChatItem" + case .apiDeleteChatItem: return "apiDeleteChatItem" + case .apiConnectContactViaAddress: return "apiConnectContactViaAddress" + case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem" + case .apiArchiveReceivedReports: return "apiArchiveReceivedReports" + case .apiDeleteReceivedReports: return "apiDeleteReceivedReports" + case .apiChatItemReaction: return "apiChatItemReaction" + case .apiGetReactionMembers: return "apiGetReactionMembers" + case .apiPlanForwardChatItems: return "apiPlanForwardChatItems" + case .apiForwardChatItems: return "apiForwardChatItems" + case .apiGetNtfToken: return "apiGetNtfToken" + case .apiRegisterToken: return "apiRegisterToken" + case .apiVerifyToken: return "apiVerifyToken" + case .apiCheckToken: return "apiCheckToken" + case .apiDeleteToken: return "apiDeleteToken" + case .apiGetNtfConns: return "apiGetNtfConns" + case .apiGetConnNtfMessages: return "apiGetConnNtfMessages" + case .apiNewGroup: return "apiNewGroup" + case .apiAddMember: return "apiAddMember" + case .apiJoinGroup: return "apiJoinGroup" + case .apiMembersRole: return "apiMembersRole" + case .apiBlockMembersForAll: return "apiBlockMembersForAll" + case .apiRemoveMembers: return "apiRemoveMembers" + case .apiLeaveGroup: return "apiLeaveGroup" + case .apiListMembers: return "apiListMembers" + case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" + case .apiCreateGroupLink: return "apiCreateGroupLink" + case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" + case .apiDeleteGroupLink: return "apiDeleteGroupLink" + case .apiGetGroupLink: return "apiGetGroupLink" + case .apiCreateMemberContact: return "apiCreateMemberContact" + case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" + case .apiTestProtoServer: return "apiTestProtoServer" + case .apiGetServerOperators: return "apiGetServerOperators" + case .apiSetServerOperators: return "apiSetServerOperators" + case .apiGetUserServers: return "apiGetUserServers" + case .apiSetUserServers: return "apiSetUserServers" + case .apiValidateServers: return "apiValidateServers" + case .apiGetUsageConditions: return "apiGetUsageConditions" + case .apiSetConditionsNotified: return "apiSetConditionsNotified" + case .apiAcceptConditions: return "apiAcceptConditions" + case .apiSetChatItemTTL: return "apiSetChatItemTTL" + case .apiGetChatItemTTL: return "apiGetChatItemTTL" + case .apiSetChatTTL: return "apiSetChatTTL" + case .apiSetNetworkConfig: return "apiSetNetworkConfig" + case .apiGetNetworkConfig: return "apiGetNetworkConfig" + case .apiSetNetworkInfo: return "apiSetNetworkInfo" + case .reconnectAllServers: return "reconnectAllServers" + case .reconnectServer: return "reconnectServer" + case .apiSetChatSettings: return "apiSetChatSettings" + case .apiSetMemberSettings: return "apiSetMemberSettings" + case .apiContactInfo: return "apiContactInfo" + case .apiGroupMemberInfo: return "apiGroupMemberInfo" + case .apiContactQueueInfo: return "apiContactQueueInfo" + case .apiGroupMemberQueueInfo: return "apiGroupMemberQueueInfo" + case .apiSwitchContact: return "apiSwitchContact" + case .apiSwitchGroupMember: return "apiSwitchGroupMember" + case .apiAbortSwitchContact: return "apiAbortSwitchContact" + case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember" + case .apiSyncContactRatchet: return "apiSyncContactRatchet" + case .apiSyncGroupMemberRatchet: return "apiSyncGroupMemberRatchet" + case .apiGetContactCode: return "apiGetContactCode" + case .apiGetGroupMemberCode: return "apiGetGroupMemberCode" + case .apiVerifyContact: return "apiVerifyContact" + case .apiVerifyGroupMember: return "apiVerifyGroupMember" + case .apiAddContact: return "apiAddContact" + case .apiSetConnectionIncognito: return "apiSetConnectionIncognito" + case .apiChangeConnectionUser: return "apiChangeConnectionUser" + case .apiConnectPlan: return "apiConnectPlan" + case .apiConnect: return "apiConnect" + case .apiDeleteChat: return "apiDeleteChat" + case .apiClearChat: return "apiClearChat" + case .apiListContacts: return "apiListContacts" + case .apiUpdateProfile: return "apiUpdateProfile" + case .apiSetContactPrefs: return "apiSetContactPrefs" + case .apiSetContactAlias: return "apiSetContactAlias" + case .apiSetGroupAlias: return "apiSetGroupAlias" + case .apiSetConnectionAlias: return "apiSetConnectionAlias" + case .apiSetUserUIThemes: return "apiSetUserUIThemes" + case .apiSetChatUIThemes: return "apiSetChatUIThemes" + case .apiCreateMyAddress: return "apiCreateMyAddress" + case .apiDeleteMyAddress: return "apiDeleteMyAddress" + case .apiShowMyAddress: return "apiShowMyAddress" + case .apiSetProfileAddress: return "apiSetProfileAddress" + case .apiAddressAutoAccept: return "apiAddressAutoAccept" + case .apiAcceptContact: return "apiAcceptContact" + case .apiRejectContact: return "apiRejectContact" + case .apiSendCallInvitation: return "apiSendCallInvitation" + case .apiRejectCall: return "apiRejectCall" + case .apiSendCallOffer: return "apiSendCallOffer" + case .apiSendCallAnswer: return "apiSendCallAnswer" + case .apiSendCallExtraInfo: return "apiSendCallExtraInfo" + case .apiEndCall: return "apiEndCall" + case .apiGetCallInvitations: return "apiGetCallInvitations" + case .apiCallStatus: return "apiCallStatus" + case .apiGetNetworkStatuses: return "apiGetNetworkStatuses" + case .apiChatRead: return "apiChatRead" + case .apiChatItemsRead: return "apiChatItemsRead" + case .apiChatUnread: return "apiChatUnread" + case .receiveFile: return "receiveFile" + case .setFileToReceive: return "setFileToReceive" + case .cancelFile: return "cancelFile" + case .setLocalDeviceName: return "setLocalDeviceName" + case .connectRemoteCtrl: return "connectRemoteCtrl" + case .findKnownRemoteCtrl: return "findKnownRemoteCtrl" + case .confirmRemoteCtrl: return "confirmRemoteCtrl" + case .verifyRemoteCtrlSession: return "verifyRemoteCtrlSession" + case .listRemoteCtrls: return "listRemoteCtrls" + case .stopRemoteCtrl: return "stopRemoteCtrl" + case .deleteRemoteCtrl: return "deleteRemoteCtrl" + case .apiUploadStandaloneFile: return "apiUploadStandaloneFile" + case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" + case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" + case .showVersion: return "showVersion" + case .getAgentSubsTotal: return "getAgentSubsTotal" + case .getAgentServersSummary: return "getAgentServersSummary" + case .resetAgentServersStats: return "resetAgentServersStats" + case .string: return "console command" + } + } + } + + func ref(_ type: ChatType, _ id: Int64) -> String { + "\(type.rawValue)\(id)" + } + + func joinedIds(_ ids: [Int64]) -> String { + ids.map { "\($0)" }.joined(separator: ",") + } + + func chatItemTTLStr(seconds: Int64?) -> String { + if let seconds = seconds { + return String(seconds) + } else { + return "default" + } + } + + var obfuscated: ChatCommand { + switch self { + case let .apiStorageEncryption(cfg): + return .apiStorageEncryption(config: DBEncryptionConfig(currentKey: obfuscate(cfg.currentKey), newKey: obfuscate(cfg.newKey))) + case let .apiSetActiveUser(userId, viewPwd): + return .apiSetActiveUser(userId: userId, viewPwd: obfuscate(viewPwd)) + case let .apiHideUser(userId, viewPwd): + return .apiHideUser(userId: userId, viewPwd: obfuscate(viewPwd)) + case let .apiUnhideUser(userId, viewPwd): + return .apiUnhideUser(userId: userId, viewPwd: obfuscate(viewPwd)) + case let .apiDeleteUser(userId, delSMPQueues, viewPwd): + return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd)) + case let .testStorageEncryption(key): + return .testStorageEncryption(key: obfuscate(key)) + default: return self + } + } + + private func obfuscate(_ s: String) -> String { + s == "" ? "" : "***" + } + + private func obfuscate(_ s: String?) -> String? { + if let s = s { + return obfuscate(s) + } + return nil + } + + private func onOffParam(_ param: String, _ b: Bool?) -> String { + if let b = b { + return " \(param)=\(onOff(b))" + } + return "" + } + + private func maybePwd(_ pwd: String?) -> String { + pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd) + } +} + +// ChatResponse is split to three enums to reduce stack size used when parsing it, parsing large enums is very inefficient. +enum ChatResponse0: Decodable, ChatAPIResult { + case activeUser(user: User) + case usersList(users: [UserInfo]) + case chatStarted + case chatRunning + case chatStopped + case apiChats(user: UserRef, chats: [ChatData]) + case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo?) + case chatTags(user: UserRef, userTags: [ChatTag]) + case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) + case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) + case serverOperatorConditions(conditions: ServerOperatorConditions) + case userServers(user: UserRef, userServers: [UserOperatorServers]) + case userServersValidation(user: UserRef, serverErrors: [UserServersError]) + case usageConditions(usageConditions: UsageConditions, conditionsText: String, acceptedConditions: UsageConditions?) + case chatItemTTL(user: UserRef, chatItemTTL: Int64?) + case networkConfig(networkConfig: NetCfg) + case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?) + case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) + case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: ServerQueueInfo) + case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberSwitchAborted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactRatchetSyncStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberRatchetSyncStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactCode(user: UserRef, contact: Contact, connectionCode: String) + case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String) + case connectionVerified(user: UserRef, verified: Bool, expectedCode: String) + case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64]) + + var responseType: String { + switch self { + case .activeUser: "activeUser" + case .usersList: "usersList" + case .chatStarted: "chatStarted" + case .chatRunning: "chatRunning" + case .chatStopped: "chatStopped" + case .apiChats: "apiChats" + case .apiChat: "apiChat" + case .chatTags: "chatTags" + case .chatItemInfo: "chatItemInfo" + case .serverTestResult: "serverTestResult" + case .serverOperatorConditions: "serverOperators" + case .userServers: "userServers" + case .userServersValidation: "userServersValidation" + case .usageConditions: "usageConditions" + case .chatItemTTL: "chatItemTTL" + case .networkConfig: "networkConfig" + case .contactInfo: "contactInfo" + case .groupMemberInfo: "groupMemberInfo" + case .queueInfo: "queueInfo" + case .contactSwitchStarted: "contactSwitchStarted" + case .groupMemberSwitchStarted: "groupMemberSwitchStarted" + case .contactSwitchAborted: "contactSwitchAborted" + case .groupMemberSwitchAborted: "groupMemberSwitchAborted" + case .contactRatchetSyncStarted: "contactRatchetSyncStarted" + case .groupMemberRatchetSyncStarted: "groupMemberRatchetSyncStarted" + case .contactCode: "contactCode" + case .groupMemberCode: "groupMemberCode" + case .connectionVerified: "connectionVerified" + case .tagsUpdated: "tagsUpdated" + } + } + + var details: String { + switch self { + case let .activeUser(user): return String(describing: user) + case let .usersList(users): return String(describing: users) + case .chatStarted: return noDetails + case .chatRunning: return noDetails + case .chatStopped: return noDetails + case let .apiChats(u, chats): return withUser(u, String(describing: chats)) + case let .apiChat(u, chat, navInfo): return withUser(u, "chat: \(String(describing: chat))\nnavInfo: \(String(describing: navInfo))") + case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))") + case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))") + case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") + case let .serverOperatorConditions(conditions): return "conditions: \(String(describing: conditions))" + case let .userServers(u, userServers): return withUser(u, "userServers: \(String(describing: userServers))") + case let .userServersValidation(u, serverErrors): return withUser(u, "serverErrors: \(String(describing: serverErrors))") + case let .usageConditions(usageConditions, _, acceptedConditions): return "usageConditions: \(String(describing: usageConditions))\nacceptedConditions: \(String(describing: acceptedConditions))" + case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL)) + case let .networkConfig(networkConfig): return String(describing: networkConfig) + case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))") + case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))") + case let .queueInfo(u, rcvMsgInfo, queueInfo): + let msgInfo = if let info = rcvMsgInfo { encodeJSON(info) } else { "none" } + return withUser(u, "rcvMsgInfo: \(msgInfo)\nqueueInfo: \(encodeJSON(queueInfo))") + case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") + case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") + case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") + case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") + case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") + case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") + case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)") + case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)") + case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)") + case let .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))") + } + } + + static func fallbackResult(_ type: String, _ json: NSDictionary) -> ChatResponse0? { + if type == "apiChats" { + if let r = parseApiChats(json) { + return .apiChats(user: r.user, chats: r.chats) + } + } else if type == "apiChat" { + if let jApiChat = json["apiChat"] as? NSDictionary, + let user: UserRef = try? decodeObject(jApiChat["user"] as Any), + let jChat = jApiChat["chat"] as? NSDictionary, + let (chat, navInfo) = try? parseChatData(jChat, jApiChat["navInfo"] as? NSDictionary) { + return .apiChat(user: user, chat: chat, navInfo: navInfo) + } + } + return nil + } +} + +enum ChatResponse1: Decodable, ChatAPIResult { + case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection) + case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) + case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) + case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan) + case sentConfirmation(user: UserRef, connection: PendingContactConnection) + case sentInvitation(user: UserRef, connection: PendingContactConnection) + case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?) + case contactAlreadyExists(user: UserRef, contact: Contact) + case contactDeleted(user: UserRef, contact: Contact) + case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) + case groupDeletedUser(user: UserRef, groupInfo: GroupInfo) + case chatCleared(user: UserRef, chatInfo: ChatInfo) + case userProfileNoChange(user: User) + case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary) + case userPrivacy(user: User, updatedUser: User) + case contactAliasUpdated(user: UserRef, toContact: Contact) + case groupAliasUpdated(user: UserRef, toGroup: GroupInfo) + case connectionAliasUpdated(user: UserRef, toConnection: PendingContactConnection) + case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact) + case userContactLink(user: User, contactLink: UserContactLink) + case userContactLinkUpdated(user: User, contactLink: UserContactLink) + case userContactLinkCreated(user: User, connLinkContact: CreatedConnLink) + case userContactLinkDeleted(user: User) + case acceptingContactRequest(user: UserRef, contact: Contact) + case contactRequestRejected(user: UserRef) + case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) + case newChatItems(user: UserRef, chatItems: [AChatItem]) + case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set, byUser: Bool, member_: GroupMember?) + case forwardPlan(user: UserRef, chatItemIds: [Int64], forwardConfirmation: ForwardConfirmation?) + case chatItemUpdated(user: UserRef, chatItem: AChatItem) + case chatItemNotChanged(user: UserRef, chatItem: AChatItem) + case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) + case reactionMembers(user: UserRef, memberReactions: [MemberReaction]) + case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool) + case contactsList(user: UserRef, contacts: [Contact]) + + var responseType: String { + switch self { + case .invitation: "invitation" + case .connectionIncognitoUpdated: "connectionIncognitoUpdated" + case .connectionUserChanged: "connectionUserChanged" + case .connectionPlan: "connectionPlan" + case .sentConfirmation: "sentConfirmation" + case .sentInvitation: "sentInvitation" + case .sentInvitationToContact: "sentInvitationToContact" + case .contactAlreadyExists: "contactAlreadyExists" + case .contactDeleted: "contactDeleted" + case .contactConnectionDeleted: "contactConnectionDeleted" + case .groupDeletedUser: "groupDeletedUser" + case .chatCleared: "chatCleared" + case .userProfileNoChange: "userProfileNoChange" + case .userProfileUpdated: "userProfileUpdated" + case .userPrivacy: "userPrivacy" + case .contactAliasUpdated: "contactAliasUpdated" + case .groupAliasUpdated: "groupAliasUpdated" + case .connectionAliasUpdated: "connectionAliasUpdated" + case .contactPrefsUpdated: "contactPrefsUpdated" + case .userContactLink: "userContactLink" + case .userContactLinkUpdated: "userContactLinkUpdated" + case .userContactLinkCreated: "userContactLinkCreated" + case .userContactLinkDeleted: "userContactLinkDeleted" + case .acceptingContactRequest: "acceptingContactRequest" + case .contactRequestRejected: "contactRequestRejected" + case .networkStatuses: "networkStatuses" + case .newChatItems: "newChatItems" + case .groupChatItemsDeleted: "groupChatItemsDeleted" + case .forwardPlan: "forwardPlan" + case .chatItemUpdated: "chatItemUpdated" + case .chatItemNotChanged: "chatItemNotChanged" + case .chatItemReaction: "chatItemReaction" + case .reactionMembers: "reactionMembers" + case .chatItemsDeleted: "chatItemsDeleted" + case .contactsList: "contactsList" + } + } + + var details: String { + switch self { + case let .contactDeleted(u, contact): return withUser(u, String(describing: contact)) + case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) + case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo)) + case .userProfileNoChange: return noDetails + case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile)) + case let .userPrivacy(u, updatedUser): return withUser(u, String(describing: updatedUser)) + case let .contactAliasUpdated(u, toContact): return withUser(u, String(describing: toContact)) + case let .groupAliasUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) + case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) + case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))") + case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails) + case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails) + case let .userContactLinkCreated(u, connLink): return withUser(u, String(describing: connLink)) + case .userContactLinkDeleted: return noDetails + case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact)) + case .contactRequestRejected: return noDetails + case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) + case let .newChatItems(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case let .groupChatItemsDeleted(u, gInfo, chatItemIDs, byUser, member_): + return withUser(u, "chatItemIDs: \(String(describing: chatItemIDs))\ngroupInfo: \(String(describing: gInfo))\nbyUser: \(byUser)\nmember_: \(String(describing: member_))") + case let .forwardPlan(u, chatItemIds, forwardConfirmation): return withUser(u, "items: \(chatItemIds) forwardConfirmation: \(String(describing: forwardConfirmation))") + case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") + case let .reactionMembers(u, reaction): return withUser(u, "memberReactions: \(String(describing: reaction))") + case let .chatItemsDeleted(u, items, byUser): + let itemsString = items.map { item in + "deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n") + return withUser(u, itemsString + "\nbyUser: \(byUser)") + case let .contactsList(u, contacts): return withUser(u, String(describing: contacts)) + case let .invitation(u, connLinkInvitation, connection): return withUser(u, "connLinkInvitation: \(connLinkInvitation)\nconnection: \(connection)") + case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) + case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\nnewUserId: \(String(describing: newUser.userId))") + case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))") + case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection)) + case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) + case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact)) + case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact)) + } + } +} + +enum ChatResponse2: Decodable, ChatAPIResult { + // group responses + case groupCreated(user: UserRef, groupInfo: GroupInfo) + case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember) + case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) + case userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], withMessages: Bool) + case leftMemberUser(user: UserRef, groupInfo: GroupInfo) + case groupMembers(user: UserRef, group: SimpleXChat.Group) + case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole) + case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool) + case groupUpdated(user: UserRef, toGroup: GroupInfo) + case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole) + case groupLink(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole) + case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo) + case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + // receiving file responses + case rcvFileAccepted(user: UserRef, chatItem: AChatItem) + case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer) + case standaloneFileInfo(fileMeta: MigrationFileLinkData?) + case rcvStandaloneFileCreated(user: UserRef, rcvFileTransfer: RcvFileTransfer) + case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer) + // sending file responses + case sndFileCancelled(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer]) + case sndStandaloneFileCreated(user: UserRef, fileTransferMeta: FileTransferMeta) // returned by _upload + case sndFileStartXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) // not used + case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) + // call invitations + case callInvitations(callInvitations: [RcvCallInvitation]) + // notifications + case ntfTokenStatus(status: NtfTknStatus) + case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String) + case ntfConns(ntfConns: [NtfConn]) + case connNtfMessages(receivedMsgs: [RcvNtfMsgInfo]) + // remote desktop responses + case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo]) + case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String) + case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) + // misc + case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) + case cmdOk(user_: UserRef?) + case agentSubsTotal(user: UserRef, subsTotal: SMPServerSubs, hasSession: Bool) + case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary) + case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs) + case archiveExported(archiveErrors: [ArchiveError]) + case archiveImported(archiveErrors: [ArchiveError]) + case appSettings(appSettings: AppSettings) + + var responseType: String { + switch self { + case .groupCreated: "groupCreated" + case .sentGroupInvitation: "sentGroupInvitation" + case .userAcceptedGroupSent: "userAcceptedGroupSent" + case .userDeletedMembers: "userDeletedMembers" + case .leftMemberUser: "leftMemberUser" + case .groupMembers: "groupMembers" + case .membersRoleUser: "membersRoleUser" + case .membersBlockedForAllUser: "membersBlockedForAllUser" + case .groupUpdated: "groupUpdated" + case .groupLinkCreated: "groupLinkCreated" + case .groupLink: "groupLink" + case .groupLinkDeleted: "groupLinkDeleted" + case .newMemberContact: "newMemberContact" + case .newMemberContactSentInv: "newMemberContactSentInv" + case .rcvFileAccepted: "rcvFileAccepted" + case .rcvFileAcceptedSndCancelled: "rcvFileAcceptedSndCancelled" + case .standaloneFileInfo: "standaloneFileInfo" + case .rcvStandaloneFileCreated: "rcvStandaloneFileCreated" + case .rcvFileCancelled: "rcvFileCancelled" + case .sndFileCancelled: "sndFileCancelled" + case .sndStandaloneFileCreated: "sndStandaloneFileCreated" + case .sndFileStartXFTP: "sndFileStartXFTP" + case .sndFileCancelledXFTP: "sndFileCancelledXFTP" + case .callInvitations: "callInvitations" + case .ntfTokenStatus: "ntfTokenStatus" + case .ntfToken: "ntfToken" + case .ntfConns: "ntfConns" + case .connNtfMessages: "connNtfMessages" + case .remoteCtrlList: "remoteCtrlList" + case .remoteCtrlConnecting: "remoteCtrlConnecting" + case .remoteCtrlConnected: "remoteCtrlConnected" + case .versionInfo: "versionInfo" + case .cmdOk: "cmdOk" + case .agentSubsTotal: "agentSubsTotal" + case .agentServersSummary: "agentServersSummary" + case .agentSubsSummary: "agentSubsSummary" + case .archiveExported: "archiveExported" + case .archiveImported: "archiveImported" + case .appSettings: "appSettings" + } + } + + var details: String { + switch self { + case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)") + case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))") + case let .userDeletedMembers(u, groupInfo, members, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\nwithMessages: \(withMessages)") + case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .groupMembers(u, group): return withUser(u, String(describing: group)) + case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)") + case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)") + case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) + case let .groupLinkCreated(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)") + case let .groupLink(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)") + case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem)) + case .rcvFileAcceptedSndCancelled: return noDetails + case let .standaloneFileInfo(fileMeta): return String(describing: fileMeta) + case .rcvStandaloneFileCreated: return noDetails + case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem)) + case .sndStandaloneFileCreated: return noDetails + case let .sndFileStartXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .callInvitations(invs): return String(describing: invs) + case let .ntfTokenStatus(status): return String(describing: status) + case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)" + case let .ntfConns(ntfConns): return String(describing: ntfConns) + case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))" + case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls) + case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)" + case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) + case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" + case .cmdOk: return noDetails + case let .agentSubsTotal(u, subsTotal, hasSession): return withUser(u, "subsTotal: \(String(describing: subsTotal))\nhasSession: \(hasSession)") + case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary)) + case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary)) + case let .archiveExported(archiveErrors): return String(describing: archiveErrors) + case let .archiveImported(archiveErrors): return String(describing: archiveErrors) + case let .appSettings(appSettings): return String(describing: appSettings) + } + } +} + +enum ChatEvent: Decodable, ChatAPIResult { + case chatSuspended + case contactSwitch(user: UserRef, contact: Contact, switchProgress: SwitchProgress) + case groupMemberSwitch(user: UserRef, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress) + case contactRatchetSync(user: UserRef, contact: Contact, ratchetSyncProgress: RatchetSyncProgress) + case groupMemberRatchetSync(user: UserRef, groupInfo: GroupInfo, member: GroupMember, ratchetSyncProgress: RatchetSyncProgress) + case contactDeletedByContact(user: UserRef, contact: Contact) + case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) + case contactConnecting(user: UserRef, contact: Contact) + case contactSndReady(user: UserRef, contact: Contact) + case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest) + case contactUpdated(user: UserRef, toContact: Contact) + case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember) + case contactsMerged(user: UserRef, intoContact: Contact, mergedContact: Contact) + case networkStatus(networkStatus: NetworkStatus, connections: [String]) + case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) + case newChatItems(user: UserRef, chatItems: [AChatItem]) + case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) + case chatItemUpdated(user: UserRef, chatItem: AChatItem) + case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) + case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool) + // group events + case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set, byUser: Bool, member_: GroupMember?) + case receivedGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole) + case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) + case groupLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember) + case businessLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, fromContact: Contact) + case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) + case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) + case memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool) + case deletedMemberUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, withMessages: Bool) + case deletedMember(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember, withMessages: Bool) + case leftMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case groupDeleted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case userJoinedGroup(user: UserRef, groupInfo: GroupInfo) + case joinedGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case connectedToGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?) + case groupUpdated(user: UserRef, toGroup: GroupInfo) + case newMemberContactReceivedInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + // receiving file events + case rcvFileAccepted(user: UserRef, chatItem: AChatItem) + case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer) + case rcvFileStart(user: UserRef, chatItem: AChatItem) // send by chats + case rcvFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, receivedSize: Int64, totalSize: Int64, rcvFileTransfer: RcvFileTransfer) + case rcvFileComplete(user: UserRef, chatItem: AChatItem) + case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer) + case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) + case rcvFileError(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer) + case rcvFileWarning(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer) + // sending file events + case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) + case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) + case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer) + case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) + case sndFileRedirectStartXFTP(user: UserRef, fileTransferMeta: FileTransferMeta, redirectMeta: FileTransferMeta) + case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) + case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String]) + case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + // call events + case callInvitation(callInvitation: RcvCallInvitation) + case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) + case callAnswer(user: UserRef, contact: Contact, answer: WebRTCSession) + case callExtraInfo(user: UserRef, contact: Contact, extraInfo: WebRTCExtraInfo) + case callEnded(user: UserRef, contact: Contact) + case contactDisabled(user: UserRef, contact: Contact) + // notification marker + case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) + // remote desktop responses + case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool) + case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String) + case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) + case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason) + // pq + case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool) + + var responseType: String { + switch self { + case .chatSuspended: "chatSuspended" + case .contactSwitch: "contactSwitch" + case .groupMemberSwitch: "groupMemberSwitch" + case .contactRatchetSync: "contactRatchetSync" + case .groupMemberRatchetSync: "groupMemberRatchetSync" + case .contactDeletedByContact: "contactDeletedByContact" + case .contactConnected: "contactConnected" + case .contactConnecting: "contactConnecting" + case .contactSndReady: "contactSndReady" + case .receivedContactRequest: "receivedContactRequest" + case .contactUpdated: "contactUpdated" + case .groupMemberUpdated: "groupMemberUpdated" + case .contactsMerged: "contactsMerged" + case .networkStatus: "networkStatus" + case .networkStatuses: "networkStatuses" + case .newChatItems: "newChatItems" + case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated" + case .chatItemUpdated: "chatItemUpdated" + case .chatItemReaction: "chatItemReaction" + case .chatItemsDeleted: "chatItemsDeleted" + case .groupChatItemsDeleted: "groupChatItemsDeleted" + case .receivedGroupInvitation: "receivedGroupInvitation" + case .userAcceptedGroupSent: "userAcceptedGroupSent" + case .groupLinkConnecting: "groupLinkConnecting" + case .businessLinkConnecting: "businessLinkConnecting" + case .joinedGroupMemberConnecting: "joinedGroupMemberConnecting" + case .memberRole: "memberRole" + case .memberBlockedForAll: "memberBlockedForAll" + case .deletedMemberUser: "deletedMemberUser" + case .deletedMember: "deletedMember" + case .leftMember: "leftMember" + case .groupDeleted: "groupDeleted" + case .userJoinedGroup: "userJoinedGroup" + case .joinedGroupMember: "joinedGroupMember" + case .connectedToGroupMember: "connectedToGroupMember" + case .groupUpdated: "groupUpdated" + case .newMemberContactReceivedInv: "newMemberContactReceivedInv" + case .rcvFileAccepted: "rcvFileAccepted" + case .rcvFileAcceptedSndCancelled: "rcvFileAcceptedSndCancelled" + case .rcvFileStart: "rcvFileStart" + case .rcvFileProgressXFTP: "rcvFileProgressXFTP" + case .rcvFileComplete: "rcvFileComplete" + case .rcvStandaloneFileComplete: "rcvStandaloneFileComplete" + case .rcvFileSndCancelled: "rcvFileSndCancelled" + case .rcvFileError: "rcvFileError" + case .rcvFileWarning: "rcvFileWarning" + case .sndFileStart: "sndFileStart" + case .sndFileComplete: "sndFileComplete" + case .sndFileRcvCancelled: "sndFileRcvCancelled" + case .sndFileProgressXFTP: "sndFileProgressXFTP" + case .sndFileRedirectStartXFTP: "sndFileRedirectStartXFTP" + case .sndFileCompleteXFTP: "sndFileCompleteXFTP" + case .sndStandaloneFileComplete: "sndStandaloneFileComplete" + case .sndFileError: "sndFileError" + case .sndFileWarning: "sndFileWarning" + case .callInvitation: "callInvitation" + case .callOffer: "callOffer" + case .callAnswer: "callAnswer" + case .callExtraInfo: "callExtraInfo" + case .callEnded: "callEnded" + case .contactDisabled: "contactDisabled" + case .ntfMessage: "ntfMessage" + case .remoteCtrlFound: "remoteCtrlFound" + case .remoteCtrlSessionCode: "remoteCtrlSessionCode" + case .remoteCtrlConnected: "remoteCtrlConnected" + case .remoteCtrlStopped: "remoteCtrlStopped" + case .contactPQEnabled: "contactPQEnabled" + } + } + + var details: String { + switch self { + case .chatSuspended: return noDetails + case let .contactSwitch(u, contact, switchProgress): return withUser(u, "contact: \(String(describing: contact))\nswitchProgress: \(String(describing: switchProgress))") + case let .groupMemberSwitch(u, groupInfo, member, switchProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nswitchProgress: \(String(describing: switchProgress))") + case let .contactRatchetSync(u, contact, ratchetSyncProgress): return withUser(u, "contact: \(String(describing: contact))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))") + case let .groupMemberRatchetSync(u, groupInfo, member, ratchetSyncProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))") + case let .contactDeletedByContact(u, contact): return withUser(u, String(describing: contact)) + case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact)) + case let .contactConnecting(u, contact): return withUser(u, String(describing: contact)) + case let .contactSndReady(u, contact): return withUser(u, String(describing: contact)) + case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest)) + case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact)) + case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)") + case let .contactsMerged(u, intoContact, mergedContact): return withUser(u, "intoContact: \(intoContact)\nmergedContact: \(mergedContact)") + case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))" + case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) + case let .newChatItems(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case let .chatItemsStatusesUpdated(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") + case let .chatItemsDeleted(u, items, byUser): + let itemsString = items.map { item in + "deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n") + return withUser(u, itemsString + "\nbyUser: \(byUser)") + case let .groupChatItemsDeleted(u, gInfo, chatItemIDs, byUser, member_): + return withUser(u, "chatItemIDs: \(String(describing: chatItemIDs))\ngroupInfo: \(String(describing: gInfo))\nbyUser: \(byUser)\nmember_: \(String(describing: member_))") + case let .receivedGroupInvitation(u, groupInfo, contact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)") + case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))") + case let .groupLinkConnecting(u, groupInfo, hostMember): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))") + case let .businessLinkConnecting(u, groupInfo, hostMember, fromContact): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))\nfromContact: \(String(describing: fromContact))") + case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)") + case let .memberRole(u, groupInfo, byMember, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)") + case let .memberBlockedForAll(u, groupInfo, byMember, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nblocked: \(blocked)") + case let .deletedMemberUser(u, groupInfo, member, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nwithMessages: \(withMessages)") + case let .deletedMember(u, groupInfo, byMember, deletedMember, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)\nwithMessages: \(withMessages)") + case let .leftMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .groupDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .userJoinedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .joinedGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .connectedToGroupMember(u, groupInfo, member, memberContact): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nmemberContact: \(String(describing: memberContact))") + case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) + case let .newMemberContactReceivedInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem)) + case .rcvFileAcceptedSndCancelled: return noDetails + case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize, _): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)") + case let .rcvStandaloneFileComplete(u, targetPath, _): return withUser(u, targetPath) + case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .rcvFileError(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))") + case let .rcvFileWarning(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))") + case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)") + case let .sndFileRedirectStartXFTP(u, _, redirectMeta): return withUser(u, String(describing: redirectMeta)) + case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count)) + case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + case let .callInvitation(inv): return String(describing: inv) + case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))") + case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))") + case let .callExtraInfo(u, contact, extraInfo): return withUser(u, "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))") + case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)") + case let .contactDisabled(u, contact): return withUser(u, String(describing: contact)) + case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") + case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)" + case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)" + case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) + case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))" + case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)") + } + } +} + +struct NewUser: Encodable { + var profile: Profile? + var pastTimestamp: Bool +} + +enum ChatPagination { + static let INITIAL_COUNT = 75 + static let PRELOAD_COUNT = 100 + static let UNTIL_PRELOAD_COUNT = 50 + + case last(count: Int) + case after(chatItemId: Int64, count: Int) + case before(chatItemId: Int64, count: Int) + case around(chatItemId: Int64, count: Int) + case initial(count: Int) + + var cmdString: String { + switch self { + case let .last(count): return "count=\(count)" + case let .after(chatItemId, count): return "after=\(chatItemId) count=\(count)" + case let .before(chatItemId, count): return "before=\(chatItemId) count=\(count)" + case let .around(chatItemId, count): return "around=\(chatItemId) count=\(count)" + case let .initial(count): return "initial=\(count)" + } + } +} + +enum ConnectionPlan: Decodable, Hashable { + case invitationLink(invitationLinkPlan: InvitationLinkPlan) + case contactAddress(contactAddressPlan: ContactAddressPlan) + case groupLink(groupLinkPlan: GroupLinkPlan) + case error(chatError: ChatError) +} + +enum InvitationLinkPlan: Decodable, Hashable { + case ok + case ownLink + case connecting(contact_: Contact?) + case known(contact: Contact) +} + +enum ContactAddressPlan: Decodable, Hashable { + case ok + case ownLink + case connectingConfirmReconnect + case connectingProhibit(contact: Contact) + case known(contact: Contact) + case contactViaAddress(contact: Contact) +} + +enum GroupLinkPlan: Decodable, Hashable { + case ok + case ownLink(groupInfo: GroupInfo) + case connectingConfirmReconnect + case connectingProhibit(groupInfo_: GroupInfo?) + case known(groupInfo: GroupInfo) +} + +struct ChatTagData: Encodable { + var emoji: String? + var text: String +} + +struct UpdatedMessage: Encodable { + var msgContent: MsgContent + var mentions: [String: Int64] + + var cmdString: String { + "json \(encodeJSON(self))" + } +} + +enum ChatDeleteMode: Codable { + case full(notify: Bool) + case entity(notify: Bool) + case messages + + var cmdString: String { + switch self { + case let .full(notify): "full notify=\(onOff(notify))" + case let .entity(notify): "entity notify=\(onOff(notify))" + case .messages: "messages" + } + } + + var isEntity: Bool { + switch self { + case .entity: return true + default: return false + } + } +} + +enum NetworkStatus: Decodable, Equatable { + case unknown + case connected + case disconnected + case error(connectionError: String) + + var statusString: LocalizedStringKey { + switch self { + case .connected: "connected" + case .error: "error" + default: "connecting" + } + } + + var statusExplanation: LocalizedStringKey { + switch self { + case .connected: "You are connected to the server used to receive messages from this contact." + case let .error(err): "Trying to connect to the server used to receive messages from this contact (error: \(err))." + default: "Trying to connect to the server used to receive messages from this contact." + } + } + + var imageName: String { + switch self { + case .unknown: "circle.dotted" + case .connected: "circle.fill" + case .disconnected: "ellipsis.circle.fill" + case .error: "exclamationmark.circle.fill" + } + } +} + +enum ForwardConfirmation: Decodable, Hashable { + case filesNotAccepted(fileIds: [Int64]) + case filesInProgress(filesCount: Int) + case filesMissing(filesCount: Int) + case filesFailed(filesCount: Int) +} + +struct ConnNetworkStatus: Decodable { + var agentConnId: String + var networkStatus: NetworkStatus +} + +struct UserMsgReceiptSettings: Codable { + var enable: Bool + var clearOverrides: Bool +} + + +struct UserContactLink: Decodable, Hashable { + var connLinkContact: CreatedConnLink + var autoAccept: AutoAccept? + + var responseDetails: String { + "connLinkContact: \(connLinkContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))" + } +} + +struct AutoAccept: Codable, Hashable { + var businessAddress: Bool + var acceptIncognito: Bool + var autoReply: MsgContent? + + static func cmdString(_ autoAccept: AutoAccept?) -> String { + guard let autoAccept = autoAccept else { return "off" } + var s = "on" + if autoAccept.acceptIncognito { + s += " incognito=on" + } else if autoAccept.businessAddress { + s += " business" + } + guard let msg = autoAccept.autoReply else { return s } + return s + " " + msg.cmdString + } +} + +struct DeviceToken: Decodable { + var pushProvider: PushProvider + var token: String + + var cmdString: String { + "\(pushProvider) \(token)" + } +} + +enum PushEnvironment: String { + case development + case production +} + +enum PushProvider: String, Decodable { + case apns_dev + case apns_prod + + init(env: PushEnvironment) { + switch env { + case .development: self = .apns_dev + case .production: self = .apns_prod + } + } +} + +// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable, +// and .local for periodic background checks +enum NotificationsMode: String, Decodable, SelectableItem { + case off = "OFF" + case periodic = "PERIODIC" + case instant = "INSTANT" + + var label: LocalizedStringKey { + switch self { + case .off: "No push server" + case .periodic: "Periodic" + case .instant: "Instant" + } + } + + var icon: String { + switch self { + case .off: return "arrow.clockwise" + case .periodic: return "timer" + case .instant: return "bolt" + } + } + + var id: String { self.rawValue } + + static var values: [NotificationsMode] = [.instant, .periodic, .off] +} + +struct RemoteCtrlInfo: Decodable { + var remoteCtrlId: Int64 + var ctrlDeviceName: String + var sessionState: RemoteCtrlSessionState? + + var deviceViewName: String { + ctrlDeviceName == "" ? "\(remoteCtrlId)" : ctrlDeviceName + } +} + +enum RemoteCtrlSessionState: Decodable { + case starting + case searching + case connecting + case pendingConfirmation(sessionCode: String) + case connected(sessionCode: String) +} + +enum RemoteCtrlStopReason: Decodable { + case discoveryFailed(chatError: ChatError) + case connectionFailed(chatError: ChatError) + case setupFailed(chatError: ChatError) + case disconnected +} + +struct CtrlAppInfo: Decodable { + var appVersionRange: AppVersionRange + var deviceName: String +} + +struct AppVersionRange: Decodable { + var minVersion: String + var maxVersion: String +} + +struct CoreVersionInfo: Decodable { + var version: String + var simplexmqVersion: String + var simplexmqCommit: String +} + +struct ArchiveConfig: Encodable { + var archivePath: String + var disableCompression: Bool? +} + +struct DBEncryptionConfig: Codable { + var currentKey: String + var newKey: String +} + +enum OperatorTag: String, Codable { + case simplex = "simplex" + case flux = "flux" +} + +struct ServerOperatorInfo { + var description: [String] + var website: URL + var selfhost: (text: String, link: URL)? = nil + var logo: String + var largeLogo: String + var logoDarkMode: String + var largeLogoDarkMode: String +} + +let operatorsInfo: Dictionary = [ + .simplex: ServerOperatorInfo( + description: [ + "SimpleX Chat is the first communication network that has no user profile IDs of any kind, not even random numbers or identity keys.", + "SimpleX Chat Ltd develops the communication software for SimpleX network." + ], + website: URL(string: "https://simplex.chat")!, + logo: "decentralized", + largeLogo: "logo", + logoDarkMode: "decentralized-light", + largeLogoDarkMode: "logo-light" + ), + .flux: ServerOperatorInfo( + description: [ + "Flux is the largest decentralized cloud, based on a global network of user-operated nodes.", + "Flux offers a powerful, scalable, and affordable cutting edge technology platform for all.", + "Flux operates servers in SimpleX network to improve its privacy and decentralization." + ], + website: URL(string: "https://runonflux.com")!, + selfhost: (text: "Self-host SimpleX servers on Flux", link: URL(string: "https://home.runonflux.io/apps/marketplace?q=simplex")!), + logo: "flux_logo_symbol", + largeLogo: "flux_logo", + logoDarkMode: "flux_logo_symbol", + largeLogoDarkMode: "flux_logo-light" + ), +] + +struct UsageConditions: Decodable { + var conditionsId: Int64 + var conditionsCommit: String + var notifiedAt: Date? + var createdAt: Date + + static var sampleData = UsageConditions( + conditionsId: 1, + conditionsCommit: "11a44dc1fd461a93079f897048b46998db55da5c", + notifiedAt: nil, + createdAt: Date.now + ) +} + +enum UsageConditionsAction: Decodable { + case review(operators: [ServerOperator], deadline: Date?, showNotice: Bool) + case accepted(operators: [ServerOperator]) + + var showNotice: Bool { + switch self { + case let .review(_, _, showNotice): showNotice + case .accepted: false + } + } +} + +struct ServerOperatorConditions: Decodable { + var serverOperators: [ServerOperator] + var currentConditions: UsageConditions + var conditionsAction: UsageConditionsAction? + + static var empty = ServerOperatorConditions( + serverOperators: [], + currentConditions: UsageConditions(conditionsId: 0, conditionsCommit: "empty", notifiedAt: nil, createdAt: .now), + conditionsAction: nil + ) +} + +enum ConditionsAcceptance: Equatable, Codable, Hashable { + case accepted(acceptedAt: Date?, autoAccepted: Bool) + // If deadline is present, it means there's a grace period to review and accept conditions during which user can continue to use the operator. + // No deadline indicates it's required to accept conditions for the operator to start using it. + case required(deadline: Date?) + + var conditionsAccepted: Bool { + switch self { + case .accepted: true + case .required: false + } + } + + var usageAllowed: Bool { + switch self { + case .accepted: true + case let .required(deadline): deadline != nil + } + } +} + +struct ServerOperator: Identifiable, Equatable, Codable { + var operatorId: Int64 + var operatorTag: OperatorTag? + var tradeName: String + var legalName: String? + var serverDomains: [String] + var conditionsAcceptance: ConditionsAcceptance + var enabled: Bool + var smpRoles: ServerRoles + var xftpRoles: ServerRoles + + var id: Int64 { operatorId } + + static func == (l: ServerOperator, r: ServerOperator) -> Bool { + l.operatorId == r.operatorId && l.operatorTag == r.operatorTag && l.tradeName == r.tradeName && l.legalName == r.legalName && + l.serverDomains == r.serverDomains && l.conditionsAcceptance == r.conditionsAcceptance && l.enabled == r.enabled && + l.smpRoles == r.smpRoles && l.xftpRoles == r.xftpRoles + } + + var legalName_: String { + legalName ?? tradeName + } + + var info: ServerOperatorInfo { + return if let operatorTag = operatorTag { + operatorsInfo[operatorTag] ?? ServerOperator.dummyOperatorInfo + } else { + ServerOperator.dummyOperatorInfo + } + } + + static let dummyOperatorInfo = ServerOperatorInfo( + description: ["Default"], + website: URL(string: "https://simplex.chat")!, + logo: "decentralized", + largeLogo: "logo", + logoDarkMode: "decentralized-light", + largeLogoDarkMode: "logo-light" + ) + + func logo(_ colorScheme: ColorScheme) -> String { + colorScheme == .light ? info.logo : info.logoDarkMode + } + + func largeLogo(_ colorScheme: ColorScheme) -> String { + colorScheme == .light ? info.largeLogo : info.largeLogoDarkMode + } + + static var sampleData1 = ServerOperator( + operatorId: 1, + operatorTag: .simplex, + tradeName: "SimpleX Chat", + legalName: "SimpleX Chat Ltd", + serverDomains: ["simplex.im"], + conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false), + enabled: true, + smpRoles: ServerRoles(storage: true, proxy: true), + xftpRoles: ServerRoles(storage: true, proxy: true) + ) +} + +struct ServerRoles: Equatable, Codable { + var storage: Bool + var proxy: Bool +} + +struct UserOperatorServers: Identifiable, Equatable, Codable { + var `operator`: ServerOperator? + var smpServers: [UserServer] + var xftpServers: [UserServer] + + var id: String { + if let op = self.operator { + "\(op.operatorId)" + } else { + "nil operator" + } + } + + var operator_: ServerOperator { + get { + self.operator ?? ServerOperator( + operatorId: 0, + operatorTag: nil, + tradeName: "", + legalName: "", + serverDomains: [], + conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false), + enabled: false, + smpRoles: ServerRoles(storage: true, proxy: true), + xftpRoles: ServerRoles(storage: true, proxy: true) + ) + } + set { `operator` = newValue } + } + + static var sampleData1 = UserOperatorServers( + operator: ServerOperator.sampleData1, + smpServers: [UserServer.sampleData.preset], + xftpServers: [UserServer.sampleData.xftpPreset] + ) + + static var sampleDataNilOperator = UserOperatorServers( + operator: nil, + smpServers: [UserServer.sampleData.preset], + xftpServers: [UserServer.sampleData.xftpPreset] + ) +} + +enum UserServersError: Decodable { + case noServers(protocol: ServerProtocol, user: UserRef?) + case storageMissing(protocol: ServerProtocol, user: UserRef?) + case proxyMissing(protocol: ServerProtocol, user: UserRef?) + case duplicateServer(protocol: ServerProtocol, duplicateServer: String, duplicateHost: String) + + var globalError: String? { + switch self { + case let .noServers(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + case let .storageMissing(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + case let .proxyMissing(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + default: return nil + } + } + + var globalSMPError: String? { + switch self { + case let .noServers(.smp, user): + let text = NSLocalizedString("No message servers.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .storageMissing(.smp, user): + let text = NSLocalizedString("No servers to receive messages.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .proxyMissing(.smp, user): + let text = NSLocalizedString("No servers for private message routing.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + default: + return nil + } + } + + var globalXFTPError: String? { + switch self { + case let .noServers(.xftp, user): + let text = NSLocalizedString("No media & file servers.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .storageMissing(.xftp, user): + let text = NSLocalizedString("No servers to send files.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .proxyMissing(.xftp, user): + let text = NSLocalizedString("No servers to receive files.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + default: + return nil + } + } + + private func userStr(_ user: UserRef) -> String { + String.localizedStringWithFormat(NSLocalizedString("For chat profile %@:", comment: "servers error"), user.localDisplayName) + } +} + +struct UserServer: Identifiable, Equatable, Codable, Hashable { + var serverId: Int64? + var server: String + var preset: Bool + var tested: Bool? + var enabled: Bool + var deleted: Bool + var createdAt = Date() + + static func == (l: UserServer, r: UserServer) -> Bool { + l.serverId == r.serverId && l.server == r.server && l.preset == r.preset && l.tested == r.tested && + l.enabled == r.enabled && l.deleted == r.deleted + } + + var id: String { "\(server) \(createdAt)" } + + static var empty = UserServer(serverId: nil, server: "", preset: false, tested: nil, enabled: false, deleted: false) + + var isEmpty: Bool { + server.trimmingCharacters(in: .whitespaces) == "" + } + + struct SampleData { + var preset: UserServer + var custom: UserServer + var untested: UserServer + var xftpPreset: UserServer + } + + static var sampleData = SampleData( + preset: UserServer( + serverId: 1, + server: "smp://abcd@smp8.simplex.im", + preset: true, + tested: true, + enabled: true, + deleted: false + ), + custom: UserServer( + serverId: 2, + server: "smp://abcd@smp9.simplex.im", + preset: false, + tested: false, + enabled: false, + deleted: false + ), + untested: UserServer( + serverId: 3, + server: "smp://abcd@smp10.simplex.im", + preset: false, + tested: nil, + enabled: true, + deleted: false + ), + xftpPreset: UserServer( + serverId: 4, + server: "xftp://abcd@xftp8.simplex.im", + preset: true, + tested: true, + enabled: true, + deleted: false + ) + ) + + enum CodingKeys: CodingKey { + case serverId + case server + case preset + case tested + case enabled + case deleted + } +} + +enum ProtocolTestStep: String, Decodable, Equatable { + case connect + case disconnect + case createQueue + case secureQueue + case deleteQueue + case createFile + case uploadFile + case downloadFile + case compareFile + case deleteFile + + var text: String { + switch self { + case .connect: return NSLocalizedString("Connect", comment: "server test step") + case .disconnect: return NSLocalizedString("Disconnect", comment: "server test step") + case .createQueue: return NSLocalizedString("Create queue", comment: "server test step") + case .secureQueue: return NSLocalizedString("Secure queue", comment: "server test step") + case .deleteQueue: return NSLocalizedString("Delete queue", comment: "server test step") + case .createFile: return NSLocalizedString("Create file", comment: "server test step") + case .uploadFile: return NSLocalizedString("Upload file", comment: "server test step") + case .downloadFile: return NSLocalizedString("Download file", comment: "server test step") + case .compareFile: return NSLocalizedString("Compare file", comment: "server test step") + case .deleteFile: return NSLocalizedString("Delete file", comment: "server test step") + } + } +} + +struct ProtocolTestFailure: Decodable, Error, Equatable { + var testStep: ProtocolTestStep + var testError: AgentErrorType + + static func == (l: ProtocolTestFailure, r: ProtocolTestFailure) -> Bool { + l.testStep == r.testStep + } + + var localizedDescription: String { + let err = String.localizedStringWithFormat(NSLocalizedString("Test failed at step %@.", comment: "server test failure"), testStep.text) + switch testError { + case .SMP(_, .AUTH): + return err + " " + NSLocalizedString("Server requires authorization to create queues, check password", comment: "server test error") + case .XFTP(.AUTH): + return err + " " + NSLocalizedString("Server requires authorization to upload, check password", comment: "server test error") + case .BROKER(_, .NETWORK): + return err + " " + NSLocalizedString("Possibly, certificate fingerprint in server address is incorrect", comment: "server test error") + default: + return err + } + } +} + +struct MigrationFileLinkData: Codable { + let networkConfig: NetworkConfig? + + struct NetworkConfig: Codable { + let socksProxy: String? + let networkProxy: NetworkProxy? + let hostMode: HostMode? + let requiredHostMode: Bool? + + func transformToPlatformSupported() -> NetworkConfig { + return if let hostMode, let requiredHostMode { + NetworkConfig( + socksProxy: nil, + networkProxy: nil, + hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode, + requiredHostMode: requiredHostMode + ) + } else { self } + } + } + + func addToLink(link: String) -> String { + "\(link)&data=\(encodeJSON(self).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)" + } + + static func readFromLink(link: String) -> MigrationFileLinkData? { +// standaloneFileInfo(link) + nil + } +} + +struct AppSettings: Codable, Equatable { + var networkConfig: NetCfg? = nil + var networkProxy: NetworkProxy? = nil + var privacyEncryptLocalFiles: Bool? = nil + var privacyAskToApproveRelays: Bool? = nil + var privacyAcceptImages: Bool? = nil + var privacyLinkPreviews: Bool? = nil + var privacyShowChatPreviews: Bool? = nil + var privacySaveLastDraft: Bool? = nil + var privacyProtectScreen: Bool? = nil + var privacyMediaBlurRadius: Int? = nil + var notificationMode: AppSettingsNotificationMode? = nil + var notificationPreviewMode: NotificationPreviewMode? = nil + var webrtcPolicyRelay: Bool? = nil + var webrtcICEServers: [String]? = nil + var confirmRemoteSessions: Bool? = nil + var connectRemoteViaMulticast: Bool? = nil + var connectRemoteViaMulticastAuto: Bool? = nil + var developerTools: Bool? = nil + var confirmDBUpgrades: Bool? = nil + var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil + var iosCallKitEnabled: Bool? = nil + var iosCallKitCallsInRecents: Bool? = nil + var uiProfileImageCornerRadius: Double? = nil + var uiChatItemRoundness: Double? = nil + var uiChatItemTail: Bool? = nil + var uiColorScheme: String? = nil + var uiDarkColorScheme: String? = nil + var uiCurrentThemeIds: [String: String]? = nil + var uiThemes: [ThemeOverrides]? = nil + var oneHandUI: Bool? = nil + var chatBottomBar: Bool? = nil + + func prepareForExport() -> AppSettings { + var empty = AppSettings() + let def = AppSettings.defaults + if networkConfig != def.networkConfig { empty.networkConfig = networkConfig } + if networkProxy != def.networkProxy { empty.networkProxy = networkProxy } + if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } + if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } + if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages } + if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews } + if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } + if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft } + if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen } + if privacyMediaBlurRadius != def.privacyMediaBlurRadius { empty.privacyMediaBlurRadius = privacyMediaBlurRadius } + if notificationMode != def.notificationMode { empty.notificationMode = notificationMode } + if notificationPreviewMode != def.notificationPreviewMode { empty.notificationPreviewMode = notificationPreviewMode } + if webrtcPolicyRelay != def.webrtcPolicyRelay { empty.webrtcPolicyRelay = webrtcPolicyRelay } + if webrtcICEServers != def.webrtcICEServers { empty.webrtcICEServers = webrtcICEServers } + if confirmRemoteSessions != def.confirmRemoteSessions { empty.confirmRemoteSessions = confirmRemoteSessions } + if connectRemoteViaMulticast != def.connectRemoteViaMulticast {empty.connectRemoteViaMulticast = connectRemoteViaMulticast } + if connectRemoteViaMulticastAuto != def.connectRemoteViaMulticastAuto { empty.connectRemoteViaMulticastAuto = connectRemoteViaMulticastAuto } + if developerTools != def.developerTools { empty.developerTools = developerTools } + if confirmDBUpgrades != def.confirmDBUpgrades { empty.confirmDBUpgrades = confirmDBUpgrades } + if androidCallOnLockScreen != def.androidCallOnLockScreen { empty.androidCallOnLockScreen = androidCallOnLockScreen } + if iosCallKitEnabled != def.iosCallKitEnabled { empty.iosCallKitEnabled = iosCallKitEnabled } + if iosCallKitCallsInRecents != def.iosCallKitCallsInRecents { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } + if uiProfileImageCornerRadius != def.uiProfileImageCornerRadius { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius } + if uiChatItemRoundness != def.uiChatItemRoundness { empty.uiChatItemRoundness = uiChatItemRoundness } + if uiChatItemTail != def.uiChatItemTail { empty.uiChatItemTail = uiChatItemTail } + if uiColorScheme != def.uiColorScheme { empty.uiColorScheme = uiColorScheme } + if uiDarkColorScheme != def.uiDarkColorScheme { empty.uiDarkColorScheme = uiDarkColorScheme } + if uiCurrentThemeIds != def.uiCurrentThemeIds { empty.uiCurrentThemeIds = uiCurrentThemeIds } + if uiThemes != def.uiThemes { empty.uiThemes = uiThemes } + if oneHandUI != def.oneHandUI { empty.oneHandUI = oneHandUI } + if chatBottomBar != def.chatBottomBar { empty.chatBottomBar = chatBottomBar } + return empty + } + + static var defaults: AppSettings { + AppSettings ( + networkConfig: NetCfg.defaults, + networkProxy: NetworkProxy.def, + privacyEncryptLocalFiles: true, + privacyAskToApproveRelays: true, + privacyAcceptImages: true, + privacyLinkPreviews: true, + privacyShowChatPreviews: true, + privacySaveLastDraft: true, + privacyProtectScreen: false, + privacyMediaBlurRadius: 0, + notificationMode: AppSettingsNotificationMode.instant, + notificationPreviewMode: NotificationPreviewMode.message, + webrtcPolicyRelay: true, + webrtcICEServers: [], + confirmRemoteSessions: false, + connectRemoteViaMulticast: true, + connectRemoteViaMulticastAuto: true, + developerTools: false, + confirmDBUpgrades: false, + androidCallOnLockScreen: AppSettingsLockScreenCalls.show, + iosCallKitEnabled: true, + iosCallKitCallsInRecents: false, + uiProfileImageCornerRadius: 22.5, + uiChatItemRoundness: 0.75, + uiChatItemTail: true, + uiColorScheme: DefaultTheme.SYSTEM_THEME_NAME, + uiDarkColorScheme: DefaultTheme.SIMPLEX.themeName, + uiCurrentThemeIds: nil as [String: String]?, + uiThemes: nil as [ThemeOverrides]?, + oneHandUI: true, + chatBottomBar: true + ) + } +} + +enum AppSettingsNotificationMode: String, Codable { + case off + case periodic + case instant + + func toNotificationsMode() -> NotificationsMode { + switch self { + case .instant: .instant + case .periodic: .periodic + case .off: .off + } + } + + static func from(_ mode: NotificationsMode) -> AppSettingsNotificationMode { + switch mode { + case .instant: .instant + case .periodic: .periodic + case .off: .off + } + } +} + +//enum NotificationPreviewMode: Codable { +// case hidden +// case contact +// case message +//} + +enum AppSettingsLockScreenCalls: String, Codable { + case disable + case show + case accept +} + +struct UserNetworkInfo: Codable, Equatable { + let networkType: UserNetworkType + let online: Bool +} + +enum UserNetworkType: String, Codable { + case none + case cellular + case wifi + case ethernet + case other + + var text: LocalizedStringKey { + switch self { + case .none: "No network connection" + case .cellular: "Cellular" + case .wifi: "WiFi" + case .ethernet: "Wired ethernet" + case .other: "Other" + } + } +} + +struct RcvMsgInfo: Codable { + var msgId: Int64 + var msgDeliveryId: Int64 + var msgDeliveryStatus: String + var agentMsgId: Int64 + var agentMsgMeta: String +} + +struct ServerQueueInfo: Codable { + var server: String + var rcvId: String + var sndId: String + var ntfId: String? + var status: String + var info: QueueInfo +} + +struct QueueInfo: Codable { + var qiSnd: Bool + var qiNtf: Bool + var qiSub: QSub? + var qiSize: Int + var qiMsg: MsgInfo? +} + +struct QSub: Codable { + var qSubThread: QSubThread + var qDelivered: String? +} + +enum QSubThread: String, Codable { + case noSub + case subPending + case subThread + case prohibitSub +} + +struct MsgInfo: Codable { + var msgId: String + var msgTs: Date + var msgType: MsgType +} + +enum MsgType: String, Codable { + case message + case quota +} + +struct PresentedServersSummary: Codable { + var statsStartedAt: Date + var allUsersSMP: SMPServersSummary + var allUsersXFTP: XFTPServersSummary + var currentUserSMP: SMPServersSummary + var currentUserXFTP: XFTPServersSummary +} + +struct SMPServersSummary: Codable { + var smpTotals: SMPTotals + var currentlyUsedSMPServers: [SMPServerSummary] + var previouslyUsedSMPServers: [SMPServerSummary] + var onlyProxiedSMPServers: [SMPServerSummary] +} + +struct SMPTotals: Codable { + var sessions: ServerSessions + var subs: SMPServerSubs + var stats: AgentSMPServerStatsData +} + +struct SMPServerSummary: Codable, Identifiable { + var smpServer: String + var known: Bool? + var sessions: ServerSessions? + var subs: SMPServerSubs? + var stats: AgentSMPServerStatsData? + + var id: String { smpServer } + + var hasSubs: Bool { subs != nil } + + var sessionsOrNew: ServerSessions { sessions ?? ServerSessions.newServerSessions } + + var subsOrNew: SMPServerSubs { subs ?? SMPServerSubs.newSMPServerSubs } +} + +struct ServerSessions: Codable { + var ssConnected: Int + var ssErrors: Int + var ssConnecting: Int + + static var newServerSessions = ServerSessions( + ssConnected: 0, + ssErrors: 0, + ssConnecting: 0 + ) + + var hasSess: Bool { ssConnected > 0 } +} + +struct SMPServerSubs: Codable { + var ssActive: Int + var ssPending: Int + + static var newSMPServerSubs = SMPServerSubs( + ssActive: 0, + ssPending: 0 + ) + + var total: Int { ssActive + ssPending } + + var shareOfActive: Double { + guard total != 0 else { return 0.0 } + return Double(ssActive) / Double(total) + } +} + +struct AgentSMPServerStatsData: Codable { + var _sentDirect: Int + var _sentViaProxy: Int + var _sentProxied: Int + var _sentDirectAttempts: Int + var _sentViaProxyAttempts: Int + var _sentProxiedAttempts: Int + var _sentAuthErrs: Int + var _sentQuotaErrs: Int + var _sentExpiredErrs: Int + var _sentOtherErrs: Int + var _recvMsgs: Int + var _recvDuplicates: Int + var _recvCryptoErrs: Int + var _recvErrs: Int + var _ackMsgs: Int + var _ackAttempts: Int + var _ackNoMsgErrs: Int + var _ackOtherErrs: Int + var _connCreated: Int + var _connSecured: Int + var _connCompleted: Int + var _connDeleted: Int + var _connDelAttempts: Int + var _connDelErrs: Int + var _connSubscribed: Int + var _connSubAttempts: Int + var _connSubIgnored: Int + var _connSubErrs: Int + var _ntfKey: Int + var _ntfKeyAttempts: Int + var _ntfKeyDeleted: Int + var _ntfKeyDeleteAttempts: Int +} + +struct XFTPServersSummary: Codable { + var xftpTotals: XFTPTotals + var currentlyUsedXFTPServers: [XFTPServerSummary] + var previouslyUsedXFTPServers: [XFTPServerSummary] +} + +struct XFTPTotals: Codable { + var sessions: ServerSessions + var stats: AgentXFTPServerStatsData +} + +struct XFTPServerSummary: Codable, Identifiable { + var xftpServer: String + var known: Bool? + var sessions: ServerSessions? + var stats: AgentXFTPServerStatsData? + var rcvInProgress: Bool + var sndInProgress: Bool + var delInProgress: Bool + + var id: String { xftpServer } +} + +struct AgentXFTPServerStatsData: Codable { + var _uploads: Int + var _uploadsSize: Int64 + var _uploadAttempts: Int + var _uploadErrs: Int + var _downloads: Int + var _downloadsSize: Int64 + var _downloadAttempts: Int + var _downloadAuthErrs: Int + var _downloadErrs: Int + var _deletions: Int + var _deleteAttempts: Int + var _deleteErrs: Int +} + +struct AgentNtfServerStatsData: Codable { + var _ntfCreated: Int + var _ntfCreateAttempts: Int + var _ntfChecked: Int + var _ntfCheckAttempts: Int + var _ntfDeleted: Int + var _ntfDelAttempts: Int +} diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index a1c5a55c3b..9b9fda0397 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -30,9 +30,18 @@ actor TerminalItems { } } - func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async { + func addCommand(_ start: Date, _ cmd: ChatCommand, _ res: APIResult) async { await add(.cmd(start, cmd)) - await add(.resp(.now, resp)) + await addResult(res) + } + + func addResult(_ res: APIResult) async { + let item: TerminalItem = switch res { + case let .result(r): .res(.now, r) + case let .error(e): .err(.now, e) + case let .invalid(type, json): .bad(.now, type, json) + } + await add(item) } } @@ -54,9 +63,7 @@ class ItemsModel: ObservableObject { willSet { publisher.send() } } - // set listener here that will be notified on every add/delete of a chat item let chatState = ActiveChatState() - var chatItemsChangesListener: RecalculatePositions = RecalculatePositions() // Publishes directly to `objectWillChange` publisher, // this will cause reversedChatItems to be rendered without throttling @@ -66,6 +73,10 @@ class ItemsModel: ObservableObject { private var navigationTimeoutTask: Task? = nil private var loadChatTask: Task? = nil + var lastItemsLoaded: Bool { + chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id + } + init() { publisher .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) @@ -569,7 +580,7 @@ final class ChatModel: ObservableObject { ci.meta.itemStatus = status } im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) - im.chatItemsChangesListener.added((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0) + im.chatState.itemAdded((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0) im.itemAdded = true ChatItemDummyModel.shared.sendUpdate() return true @@ -617,13 +628,52 @@ final class ChatModel: ObservableObject { if let i = getChatItemIndex(cItem) { withAnimation { let item = im.reversedChatItems.remove(at: i) - im.chatItemsChangesListener.removed([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed()) + im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed()) } } } VoiceItemState.stopVoiceInChatView(cInfo, cItem) } + func removeMemberItems(_ removedMember: GroupMember, byMember: GroupMember, _ groupInfo: GroupInfo) { + // this should not happen, only another member can "remove" user, user can only "leave" (another event). + if byMember.groupMemberId == groupInfo.membership.groupMemberId { + logger.debug("exiting removeMemberItems") + return + } + if chatId == groupInfo.id { + for i in 0.. 0, + let updatedItem = removedUpdatedItem(chat.chatItems[0]) { + chat.chatItems = [updatedItem] + } + + func removedUpdatedItem(_ item: ChatItem) -> ChatItem? { + let newContent: CIContent + if case .groupSnd = item.chatDir, removedMember.groupMemberId == groupInfo.membership.groupMemberId { + newContent = .sndModerated + } else if case let .groupRcv(groupMember) = item.chatDir, groupMember.groupMemberId == removedMember.groupMemberId { + newContent = .rcvModerated + } else { + return nil + } + var updatedItem = item + updatedItem.meta.itemDeleted = .moderated(deletedTs: Date.now, byGroupMember: byMember) + if groupInfo.fullGroupPreferences.fullDelete.on { + updatedItem.content = newContent + } + if item.isActiveReport { + decreaseGroupReportsCounter(groupInfo.id) + } + return updatedItem + } + } + func nextChatItemData(_ chatItemId: Int64, previous: Bool, map: @escaping (ChatItem) -> T?) -> T? { guard var i = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil } if previous { @@ -666,7 +716,7 @@ final class ChatModel: ObservableObject { let cItem = ChatItem.liveDummy(chatInfo.chatType) withAnimation { im.reversedChatItems.insert(cItem, at: 0) - im.chatItemsChangesListener.added((cItem.id, cItem.isRcvNew), 0) + im.chatState.itemAdded((cItem.id, cItem.isRcvNew), 0) im.itemAdded = true } return cItem @@ -700,7 +750,7 @@ final class ChatModel: ObservableObject { markChatItemRead_(i) i += 1 } - im.chatItemsChangesListener.read(nil, im.reversedChatItems.reversed()) + im.chatState.itemsRead(nil, im.reversedChatItems.reversed()) } } func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) { @@ -724,7 +774,7 @@ final class ChatModel: ObservableObject { if chatId == cInfo.id { chatItemStatuses = [:] im.reversedChatItems = [] - im.chatItemsChangesListener.cleared() + im.chatState.clear() } } @@ -742,7 +792,7 @@ final class ChatModel: ObservableObject { } i += 1 } - im.chatItemsChangesListener.read(unreadItemIds, im.reversedChatItems.reversed()) + im.chatState.itemsRead(unreadItemIds, im.reversedChatItems.reversed()) } self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead) } @@ -1102,27 +1152,6 @@ 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 { switch chatInfo.chatSettings?.enableNtfs { case .all: chatStats.unreadChat || chatStats.unreadCount > 0 diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 545e90763f..d92411decd 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -11,7 +11,7 @@ import UIKit import Dispatch import BackgroundTasks import SwiftUI -import SimpleXChat +@preconcurrency import SimpleXChat private var chatController: chat_ctrl? @@ -19,32 +19,34 @@ private let networkStatusesLock = DispatchQueue(label: "chat.simplex.app.network 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 +88,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, log: Bool = true) -> APIResult { if log { logger.debug("chatSendCmd \(cmd.cmdType)") } let start = Date.now - let resp = bgTask + let resp: APIResult = bgTask ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) } : sendSimpleXCmd(cmd, ctrl) 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 +114,51 @@ 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 { +@inline(__always) +func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async throws -> R { + let res: APIResult = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log) + return try apiResult(res) +} + +@inline(__always) +func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async -> APIResult { await withCheckedContinuation { cont in - cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl, log: log)) + cont.resume(returning: chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log)) } } -func chatRecvMsg(_ ctrl: chat_ctrl? = nil) async -> ChatResponse? { +@inline(__always) +func apiResult(_ res: APIResult) throws -> R { + switch res { + case let .result(r): return r + case let .error(e): throw e + case let .invalid(type, _): throw ChatError.unexpectedResult(type: type) + } +} + +func chatRecvMsg(_ ctrl: chat_ctrl? = nil) async -> APIResult? { await withCheckedContinuation { cont in - _ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in - let resp = recvSimpleXMsg(ctrl) - cont.resume(returning: resp) - return resp + _ = withBGTask(bgDelay: msgDelay) { () -> APIResult? in + let evt: APIResult? = recvSimpleXMsg(ctrl) + cont.resume(returning: evt) + return evt } } } func apiGetActiveUser(ctrl: chat_ctrl? = nil) throws -> User? { - let r = chatSendCmdSync(.showActiveUser, ctrl) + let r: APIResult = chatApiSendCmdSync(.showActiveUser, ctrl: ctrl) switch r { - case let .activeUser(user): return user - case .chatCmdError(_, .error(.noActiveUser)): return nil - default: throw r + case let .result(.activeUser(user)): return user + case .error(.error(.noActiveUser)): return nil + default: throw r.unexpected } } func apiCreateActiveUser(_ p: Profile?, 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 +169,35 @@ func listUsersAsync() async throws -> [UserInfo] { return try listUsersResponse(await chatSendCmd(.listUsers)) } -private func listUsersResponse(_ r: ChatResponse) throws -> [UserInfo] { +private func listUsersResponse(_ r: ChatResponse0) throws -> [UserInfo] { if case let .usersList(users) = r { return users.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending } } - throw r + throw r.unexpected } func apiSetActiveUser(_ userId: Int64, viewPwd: String?) throws -> User { - let r = chatSendCmdSync(.apiSetActiveUser(userId: userId, viewPwd: viewPwd)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSetActiveUser(userId: userId, viewPwd: viewPwd)) if case let .activeUser(user) = r { return user } - throw r + throw r.unexpected } func apiSetActiveUserAsync(_ userId: Int64, viewPwd: String?) async throws -> User { - let r = await chatSendCmd(.apiSetActiveUser(userId: userId, viewPwd: viewPwd)) + let r: ChatResponse0 = try await chatSendCmd(.apiSetActiveUser(userId: userId, viewPwd: viewPwd)) if case let .activeUser(user) = r { return user } - throw r + throw r.unexpected } func apiSetAllContactReceipts(enable: Bool) async throws { - let r = await chatSendCmd(.setAllContactReceipts(enable: enable)) - if case .cmdOk = r { return } - throw r + try await sendCommandOkResp(.setAllContactReceipts(enable: enable)) } func apiSetUserContactReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws { - let r = await chatSendCmd(.apiSetUserContactReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) - if case .cmdOk = r { return } - throw r + try await sendCommandOkResp(.apiSetUserContactReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) } func apiSetUserGroupReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws { - let r = await chatSendCmd(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) - if case .cmdOk = r { return } - throw r + try await sendCommandOkResp(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) } func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User { @@ -199,90 +217,88 @@ func apiUnmuteUser(_ userId: Int64) async throws -> User { } func setUserPrivacy_(_ cmd: ChatCommand) async throws -> User { - let r = await chatSendCmd(cmd) + let r: ChatResponse1 = try await chatSendCmd(cmd) if case let .userPrivacy(_, updatedUser) = r { return updatedUser } - throw r + throw r.unexpected } func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) async throws { - let r = await chatSendCmd(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: viewPwd)) - if case .cmdOk = r { return } - throw r + try await sendCommandOkResp(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: viewPwd)) } func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool { - let r = chatSendCmdSync(.startChat(mainApp: true, 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 +309,8 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey))) } -func testStorageEncryption(key: String, _ ctrl: chat_ctrl? = nil) async throws { - try await sendCommandOkResp(.testStorageEncryption(key: key), ctrl) +func testStorageEncryption(key: String, ctrl: chat_ctrl? = nil) async throws { + try await sendCommandOkResp(.testStorageEncryption(key: key), ctrl: ctrl) } func apiGetChats() throws -> [ChatData] { @@ -307,31 +323,31 @@ 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(chatId: ChatId, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) { - let r = await chatSendCmd(.apiGetChat(chatId: chatId, pagination: pagination, search: search)) + let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, pagination: pagination, search: search)) if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) } - throw r + throw r.unexpected } func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async { @@ -341,26 +357,26 @@ func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async { func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async { let m = ChatModel.shared let im = ItemsModel.shared - m.chatItemStatuses = [:] - if clearItems { - await MainActor.run { + await MainActor.run { + m.chatItemStatuses = [:] + if clearItems { im.reversedChatItems = [] - ItemsModel.shared.chatItemsChangesListener.cleared() + ItemsModel.shared.chatState.clear() } } await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 }) } func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo { - let r = await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId)) + let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId)) if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo } - throw r + throw r.unexpected } func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) { - let r = await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds)) + let r: ChatResponse1 = try await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds)) if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) } - throw r + throw r.unexpected } func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { @@ -369,19 +385,19 @@ func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: Ch } 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 { @@ -403,7 +419,7 @@ func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = 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({ @@ -413,8 +429,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 @@ -425,40 +441,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", @@ -466,7 +482,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", @@ -475,53 +491,56 @@ private func createChatItemsErrorAlert(_ r: ChatResponse) { } func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem { - let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay) - if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem } - throw r + let r: ChatResponse1 = try await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay) + switch r { + case let .chatItemUpdated(_, aChatItem): return aChatItem.chatItem + case let .chatItemNotChanged(_, aChatItem): return aChatItem.chatItem + default: throw r.unexpected + } } func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem { - let r = await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay) + let r: ChatResponse1 = try await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay) if case let .chatItemReaction(_, _, reaction) = r { return reaction.chatReaction.chatItem } - throw r + throw r.unexpected } func 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) + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay) if case let .chatItemsDeleted(_, items, _) = r { return items } - throw r + 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 -> ChatResponse { - let r = await chatSendCmd(.apiArchiveReceivedReports(groupId: groupId), bgDelay: msgDelay) +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 + throw r.unexpected } func apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { - let r = await chatSendCmd(.apiDeleteReceivedReports(groupId: groupId, itemIds: itemIds, mode: mode), bgDelay: msgDelay) + 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 + 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) @@ -529,9 +548,9 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String } func apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) async throws -> NtfTknStatus { - let r = await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode)) + let r: ChatResponse2 = try await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode)) if case let .ntfTokenStatus(status) = r { return status } - throw r + throw r.unexpected } func registerToken(token: DeviceToken) { @@ -594,9 +613,9 @@ func apiVerifyToken(token: DeviceToken, nonce: String, code: String) async throw } func apiCheckToken(token: DeviceToken) async throws -> NtfTknStatus { - let r = await chatSendCmd(.apiCheckToken(token: token)) + let r: ChatResponse2 = try await chatSendCmd(.apiCheckToken(token: token)) if case let .ntfTokenStatus(status) = r { return status } - throw r + throw r.unexpected } func apiDeleteToken(token: DeviceToken) async throws { @@ -605,80 +624,80 @@ func apiDeleteToken(token: DeviceToken) async throws { 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 { @@ -691,7 +710,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) @@ -699,7 +718,7 @@ private func chatItemTTLResponse(_ r: ChatResponse) throws -> ChatItemTTL { throw RuntimeError("chatItemTTLResponse: invalid ttl") } } - throw r + throw r.unexpected } func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws { @@ -713,21 +732,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 { @@ -748,131 +767,135 @@ func apiSetMemberSettings(_ groupId: Int64, _ groupMemberId: Int64, _ memberSett } func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) { - let r = await chatSendCmd(.apiContactInfo(contactId: contactId)) + let r: ChatResponse0 = try await chatSendCmd(.apiContactInfo(contactId: contactId)) if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) } - throw r + throw r.unexpected } func 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)) + let r: ChatResponse0 = try await chatSendCmd(.apiContactQueueInfo(contactId: contactId)) if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } - throw r + throw r.unexpected } func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { - let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) + let r: ChatResponse0 = try await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } - throw r + throw r.unexpected } func apiSwitchContact(contactId: Int64) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiSwitchContact(contactId: contactId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSwitchContact(contactId: contactId)) if case let .contactSwitchStarted(_, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) if case let .groupMemberSwitchStarted(_, _, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiAbortSwitchContact(_ contactId: Int64) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiAbortSwitchContact(contactId: contactId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiAbortSwitchContact(contactId: contactId)) if case let .contactSwitchAborted(_, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiAbortSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiAbortSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiAbortSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) if case let .groupMemberSwitchAborted(_, _, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiSyncContactRatchet(_ contactId: Int64, _ force: Bool) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiSyncContactRatchet(contactId: contactId, force: force)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSyncContactRatchet(contactId: contactId, force: force)) if case let .contactRatchetSyncStarted(_, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiSyncGroupMemberRatchet(_ groupId: Int64, _ groupMemberId: Int64, _ force: Bool) throws -> (GroupMember, ConnectionStats) { - let r = chatSendCmdSync(.apiSyncGroupMemberRatchet(groupId: groupId, groupMemberId: groupMemberId, force: force)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSyncGroupMemberRatchet(groupId: groupId, groupMemberId: groupMemberId, force: force)) if case let .groupMemberRatchetSyncStarted(_, _, member, connectionStats) = r { return (member, connectionStats) } - throw r + throw r.unexpected } func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) { - let r = await chatSendCmd(.apiGetContactCode(contactId: contactId)) + let r: ChatResponse0 = try await chatSendCmd(.apiGetContactCode(contactId: contactId)) if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) } - throw r + throw r.unexpected } func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) 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 short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS) + let r: APIResult = await chatApiSendCmd(.apiAddContact(userId: userId, short: short, incognito: incognito), bgTask: false) + if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) } let alert = connectionErrorAlert(r) return (nil, alert) } func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? { - let r = await chatSendCmd(.apiSetConnectionIncognito(connId: connId, incognito: incognito)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetConnectionIncognito(connId: connId, incognito: incognito)) if case let .connectionIncognitoUpdated(_, toConnection) = r { return toConnection } - throw r + throw r.unexpected } -func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? { - let r = await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId)) +func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection { + let r: ChatResponse1 = try await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId)) if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection} - throw r + throw r.unexpected } -func apiConnectPlan(connReq: String) async throws -> ConnectionPlan { - let userId = try currentUserId("apiConnectPlan") - let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq)) - if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan } - logger.error("apiConnectPlan error: \(responseError(r))") - throw r +func apiConnectPlan(connLink: String) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { + guard let userId = ChatModel.shared.currentUser?.userId else { + logger.error("apiConnectPlan: no current user") + return (nil, nil) + } + let r: APIResult = await chatApiSendCmd(.apiConnectPlan(userId: userId, connLink: connLink)) + if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } + let alert = apiConnectResponseAlert(r.unexpected) ?? connectionErrorAlert(r) + return (nil, alert) } -func apiConnect(incognito: Bool, 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 @@ -881,38 +904,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 chatApiSendCmd(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) let m = ChatModel.shared switch r { - case let .sentConfirmation(_, connection): + case let .result(.sentConfirmation(_, connection)): return ((.invitation, connection), nil) - case let .sentInvitation(_, connection): + case let .result(.sentInvitation(_, connection)): return ((.contact, connection), nil) - case let .contactAlreadyExists(_, contact): + case let .result(.contactAlreadyExists(_, contact)): if let c = m.getContactChat(contact.contactId) { ItemsModel.shared.loadOpenChat(c.id) } let alert = contactAlreadyExistsAlert(contact) return (nil, alert) - case .chatCmdError(_, .error(.invalidConnReq)): - let alert = mkAlert( + default: () + } + let alert = apiConnectResponseAlert(r.unexpected) ?? connectionErrorAlert(r) + return (nil, alert) +} + +private func apiConnectResponseAlert(_ r: ChatError) -> Alert? { + switch r { + case .error(.invalidConnReq): + mkAlert( title: "Invalid connection link", message: "Please check that you used the correct link or ask your contact to send you another one." ) - return (nil, alert) - case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))): - let alert = mkAlert( + case .error(.unsupportedConnReq): + mkAlert( + title: "Unsupported connection link", + message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." + ) + case .errorAgent(.SMP(_, .AUTH)): + mkAlert( title: "Connection error (AUTH)", message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." ) - return (nil, alert) - case let .chatCmdError(_, .errorAgent(.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")), @@ -922,25 +956,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)))." ) - return (nil, alert) + } else { + nil } - default: () + default: nil } - let alert = connectionErrorAlert(r) - return (nil, alert) } func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { @@ -950,13 +981,13 @@ func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { ) } -private func connectionErrorAlert(_ r: ChatResponse) -> Alert { +private func connectionErrorAlert(_ r: APIResult) -> Alert { if let networkErrorAlert = networkErrorAlert(r) { return networkErrorAlert } else { return mkAlert( title: "Connection error", - message: "Error: \(responseError(r))" + message: "Error: \(responseError(r.unexpected))" ) } } @@ -966,9 +997,9 @@ func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Co logger.error("apiConnectContactViaAddress: no current user") return (nil, nil) } - let r = await chatSendCmd(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) - if case let .sentInvitationToContact(_, contact, _) = r { return (contact, nil) } - logger.error("apiConnectContactViaAddress error: \(responseError(r))") + let r: APIResult = await chatApiSendCmd(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) + if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) } + logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") let alert = connectionErrorAlert(r) return (nil, alert) } @@ -977,11 +1008,11 @@ func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = . 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 { @@ -995,9 +1026,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 { @@ -1048,9 +1079,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 { @@ -1065,119 +1096,125 @@ func clearChat(_ chat: Chat) async { func apiListContacts() throws -> [Contact] { let userId = try currentUserId("apiListContacts") - let r = chatSendCmdSync(.apiListContacts(userId: userId)) + let r: ChatResponse1 = try chatSendCmdSync(.apiListContacts(userId: userId)) if case let .contactsList(_, contacts) = r { return contacts } - throw r + throw r.unexpected } func apiUpdateProfile(profile: Profile) async throws -> (Profile, [Contact])? { let userId = try currentUserId("apiUpdateProfile") - let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile)) + let r: APIResult = await chatApiSendCmd(.apiUpdateProfile(userId: userId, profile: profile)) switch r { - case .userProfileNoChange: return (profile, []) - case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts) - case .chatCmdError(_, .errorStore(.duplicateName)): return nil; - default: throw r + case .result(.userProfileNoChange): return (profile, []) + case let .result(.userProfileUpdated(_, _, toProfile, updateSummary)): return (toProfile, updateSummary.changedContacts) + case .error(.errorStore(.duplicateName)): return nil; + default: throw r.unexpected } } func apiSetProfileAddress(on: Bool) async throws -> User? { let userId = try currentUserId("apiSetProfileAddress") - let r = await chatSendCmd(.apiSetProfileAddress(userId: userId, on: on)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetProfileAddress(userId: userId, on: on)) switch r { case .userProfileNoChange: return nil case let .userProfileUpdated(user, _, _, _): return user - default: throw r + default: throw r.unexpected } } func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? { - let r = await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences)) if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact } - throw r + throw r.unexpected } func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Contact? { - let r = await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias)) if case let .contactAliasUpdated(_, toContact) = r { return toContact } - throw r + throw r.unexpected } func apiSetGroupAlias(groupId: Int64, localAlias: String) async throws -> GroupInfo? { - let r = 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(short: Bool) async throws -> CreatedConnLink { let userId = try currentUserId("apiCreateUserAddress") - let r = await chatSendCmd(.apiCreateMyAddress(userId: userId)) - if case let .userContactLinkCreated(_, connReq) = r { return connReq } - throw r + let r: ChatResponse1 = try await chatSendCmd(.apiCreateMyAddress(userId: userId, short: short)) + if case let .userContactLinkCreated(_, connLink) = r { return connLink } + throw r.unexpected } func apiDeleteUserAddress() async throws -> User? { let userId = try currentUserId("apiDeleteUserAddress") - let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId)) + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteMyAddress(userId: userId)) if case let .userContactLinkDeleted(user) = r { return user } - throw r + throw r.unexpected } func apiGetUserAddress() throws -> UserContactLink? { let userId = try currentUserId("apiGetUserAddress") - return try userAddressResponse(chatSendCmdSync(.apiShowMyAddress(userId: userId))) + return try userAddressResponse(chatApiSendCmdSync(.apiShowMyAddress(userId: userId))) } func apiGetUserAddressAsync() async throws -> UserContactLink? { let userId = try currentUserId("apiGetUserAddressAsync") - return try userAddressResponse(await chatSendCmd(.apiShowMyAddress(userId: userId))) + return try userAddressResponse(await chatApiSendCmd(.apiShowMyAddress(userId: userId))) } -private func userAddressResponse(_ r: ChatResponse) throws -> UserContactLink? { +private func userAddressResponse(_ r: APIResult) throws -> UserContactLink? { switch r { - case let .userContactLink(_, contactLink): return contactLink - case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil - default: throw r + case let .result(.userContactLink(_, contactLink)): return contactLink + case .error(.errorStore(storeError: .userContactLinkNotFound)): return nil + default: throw r.unexpected } } func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? { let userId = try currentUserId("userAddressAutoAccept") - let r = await chatSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept)) + let r: APIResult = await chatApiSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept)) switch r { - case let .userContactLinkUpdated(_, contactLink): return contactLink - case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil - default: throw r + case let .result(.userContactLinkUpdated(_, contactLink)): return contactLink + case .error(.errorStore(storeError: .userContactLinkNotFound)): return nil + default: throw r.unexpected } } func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { - let r = await chatSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) + let r: APIResult = await chatApiSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) let am = AlertManager.shared - if case let .acceptingContactRequest(_, contact) = r { return contact } - if case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))) = r { + if case let .result(.acceptingContactRequest(_, contact)) = r { return contact } + if case .error(.errorAgent(.SMP(_, .AUTH))) = r { am.showAlertMsg( title: "Connection error (AUTH)", message: "Sender may have deleted the connection request." @@ -1188,16 +1225,16 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont logger.error("apiAcceptContactRequest error: \(String(describing: r))") am.showAlertMsg( title: "Error accepting contact request", - message: "Error: \(responseError(r))" + message: "Error: \(responseError(r.unexpected))" ) } return nil } func apiRejectContactRequest(contactReqId: Int64) async throws { - let r = await chatSendCmd(.apiRejectContact(contactReqId: contactReqId)) + let r: ChatResponse1 = try await chatSendCmd(.apiRejectContact(contactReqId: contactReqId)) if case .contactRequestRejected = r { return } - throw r + throw r.unexpected } func apiChatRead(type: ChatType, id: Int64) async throws { @@ -1213,31 +1250,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 } } @@ -1252,12 +1291,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(), @@ -1266,32 +1305,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 @@ -1327,7 +1356,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( @@ -1335,19 +1364,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 @@ -1359,8 +1383,22 @@ 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 { if let chatItem = await apiCancelFile(fileId: fileId) { await chatItemSimpleUpdate(user, chatItem) @@ -1369,12 +1407,12 @@ func cancelFile(user: User, fileId: Int64) async { } func apiCancelFile(fileId: Int64, ctrl: chat_ctrl? = nil) async -> AChatItem? { - let r = await chatSendCmd(.cancelFile(fileId: fileId), ctrl) + let r: APIResult = await chatApiSendCmd(.cancelFile(fileId: fileId), ctrl: ctrl) switch r { - case let .sndFileCancelled(_, chatItem, _, _) : return chatItem - case let .rcvFileCancelled(_, chatItem, _) : return chatItem + case let .result(.sndFileCancelled(_, chatItem, _, _)) : return chatItem + case let .result(.rcvFileCancelled(_, chatItem, _)) : return chatItem default: - logger.error("apiCancelFile error: \(String(describing: r))") + logger.error("apiCancelFile error: \(responseError(r.unexpected))") return nil } } @@ -1384,9 +1422,9 @@ func setLocalDeviceName(_ displayName: String) throws { } func connectRemoteCtrl(desktopAddress: String) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) { - let r = await chatSendCmd(.connectRemoteCtrl(xrcpInvitation: desktopAddress)) + let r: ChatResponse2 = try await chatSendCmd(.connectRemoteCtrl(xrcpInvitation: desktopAddress)) if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) } - throw r + throw r.unexpected } func findKnownRemoteCtrl() async throws { @@ -1394,21 +1432,21 @@ func findKnownRemoteCtrl() async throws { } func confirmRemoteCtrl(_ rcId: Int64) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) { - let r = await chatSendCmd(.confirmRemoteCtrl(remoteCtrlId: rcId)) + let r: ChatResponse2 = try await chatSendCmd(.confirmRemoteCtrl(remoteCtrlId: rcId)) if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) } - throw r + throw r.unexpected } func verifyRemoteCtrlSession(_ sessCode: String) async throws -> RemoteCtrlInfo { - let r = await chatSendCmd(.verifyRemoteCtrlSession(sessionCode: sessCode)) + let r: ChatResponse2 = try await chatSendCmd(.verifyRemoteCtrlSession(sessionCode: sessCode)) if case let .remoteCtrlConnected(rc) = r { return rc } - throw r + throw r.unexpected } func listRemoteCtrls() throws -> [RemoteCtrlInfo] { - let r = chatSendCmdSync(.listRemoteCtrls) + let r: ChatResponse2 = try chatSendCmdSync(.listRemoteCtrls) if case let .remoteCtrlList(rcInfo) = r { return rcInfo } - throw r + throw r.unexpected } func stopRemoteCtrl() async throws { @@ -1419,8 +1457,8 @@ 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 @@ -1482,15 +1520,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 { @@ -1502,9 +1540,9 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws { } func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] { - let r = chatSendCmdSync(.apiGetNetworkStatuses) + let r: ChatResponse1 = try chatSendCmdSync(.apiGetNetworkStatuses) if case let .networkStatuses(_, statuses) = r { return statuses } - throw r + throw r.unexpected } func markChatRead(_ chat: Chat) async { @@ -1547,29 +1585,29 @@ func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsR } } -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 { @@ -1579,31 +1617,31 @@ enum JoinGroupResult { } func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { - let r = await chatSendCmd(.apiJoinGroup(groupId: groupId)) + let r: APIResult = await chatApiSendCmd(.apiJoinGroup(groupId: groupId)) switch r { - case let .userAcceptedGroupSent(_, groupInfo, _): return .joined(groupInfo: groupInfo) - case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))): return .invitationRemoved - case .chatCmdError(_, .errorStore(.groupNotFound)): return .groupNotFound - default: throw r + case let .result(.userAcceptedGroupSent(_, groupInfo, _)): return .joined(groupInfo: groupInfo) + case .error(.errorAgent(.SMP(_, .AUTH))): return .invitationRemoved + case .error(.errorStore(.groupNotFound)): return .groupNotFound + default: throw r.unexpected } } -func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64]) async throws -> [GroupMember] { - let r = await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds), bgTask: false) - if case let .userDeletedMembers(_, _, members) = r { return members } - throw r +func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> [GroupMember] { + let r: ChatResponse2 = try await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds, withMessages: withMessages), bgTask: false) + if case let .userDeletedMembers(_, _, members, withMessages) = r { return members } + throw r.unexpected } func apiMembersRole(_ groupId: Int64, _ memberIds: [Int64], _ memberRole: GroupMemberRole) async throws -> [GroupMember] { - let r = await chatSendCmd(.apiMembersRole(groupId: groupId, memberIds: memberIds, memberRole: memberRole), bgTask: false) + 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 + throw r.unexpected } func apiBlockMembersForAll(_ groupId: Int64, _ memberIds: [Int64], _ blocked: Bool) async throws -> [GroupMember] { - let r = await chatSendCmd(.apiBlockMembersForAll(groupId: groupId, memberIds: memberIds, blocked: blocked), bgTask: false) + 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 + throw r.unexpected } func leaveGroup(_ groupId: Int64) async { @@ -1616,15 +1654,15 @@ func leaveGroup(_ groupId: Int64) async { } func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo { - let r = await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false) + let r: ChatResponse2 = try await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false) if case let .leftMemberUser(_, groupInfo) = r { return groupInfo } - throw r + throw r.unexpected } // use ChatModel's loadGroupMembers from views func apiListMembers(_ groupId: Int64) async -> [GroupMember] { - let r = await chatSendCmd(.apiListMembers(groupId: groupId)) - if case let .groupMembers(_, group) = r { return group.members } + let r: APIResult = await chatApiSendCmd(.apiListMembers(groupId: groupId)) + if case let .result(.groupMembers(_, group)) = r { return group.members } return [] } @@ -1637,72 +1675,73 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] { } func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws -> GroupInfo { - let r = await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile)) + let r: ChatResponse2 = try await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile)) if case let .groupUpdated(_, toGroup) = r { return toGroup } - throw r + throw r.unexpected } -func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) { - let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) - if case let .groupLinkCreated(_, _, connReq, memberRole) = r { return (connReq, memberRole) } - throw r +func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) { + let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS) + let r: ChatResponse2 = try await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole, short: short)) + if case let .groupLinkCreated(_, _, connLink, memberRole) = r { return (connLink, memberRole) } + throw r.unexpected } -func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) { - let r = await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole)) - if case let .groupLink(_, _, connReq, memberRole) = r { return (connReq, memberRole) } - throw r +func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) { + let r: ChatResponse2 = try await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole)) + if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) } + throw r.unexpected } func apiDeleteGroupLink(_ groupId: Int64) async throws { - let r = await chatSendCmd(.apiDeleteGroupLink(groupId: groupId)) + let r: ChatResponse2 = try await chatSendCmd(.apiDeleteGroupLink(groupId: groupId)) if case .groupLinkDeleted = r { return } - throw r + throw r.unexpected } -func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? { - let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId)) +func apiGetGroupLink(_ groupId: Int64) throws -> (CreatedConnLink, GroupMemberRole)? { + let r: APIResult = chatApiSendCmdSync(.apiGetGroupLink(groupId: groupId)) switch r { - case let .groupLink(_, _, connReq, memberRole): - return (connReq, memberRole) - case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)): + case let .result(.groupLink(_, _, connLink, memberRole)): + return (connLink, memberRole) + case .error(.errorStore(storeError: .groupLinkNotFound)): return nil - default: throw r + default: throw r.unexpected } } func apiCreateMemberContact(_ groupId: Int64, _ groupMemberId: Int64) async throws -> Contact { - let r = await chatSendCmd(.apiCreateMemberContact(groupId: groupId, groupMemberId: groupMemberId)) + let r: ChatResponse2 = try await chatSendCmd(.apiCreateMemberContact(groupId: groupId, groupMemberId: groupMemberId)) if case let .newMemberContact(_, contact, _, _) = r { return contact } - throw r + throw r.unexpected } func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async throws -> Contact { - let r = await chatSendCmd(.apiSendMemberContactInvitation(contactId: contactId, msg: msg), bgDelay: msgDelay) + let r: ChatResponse2 = try await chatSendCmd(.apiSendMemberContactInvitation(contactId: contactId, msg: msg), bgDelay: msgDelay) if case let .newMemberContactSentInv(_, contact, _, _) = r { return contact } - throw r + throw r.unexpected } func apiGetVersion() throws -> CoreVersionInfo { - let r = chatSendCmdSync(.showVersion) + let r: ChatResponse2 = try chatSendCmdSync(.showVersion) if case let .versionInfo(info, _, _) = r { return info } - throw r + throw r.unexpected } func getAgentSubsTotal() async throws -> (SMPServerSubs, Bool) { let userId = try currentUserId("getAgentSubsTotal") - let r = 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 { @@ -1912,7 +1951,7 @@ class ChatReceiver { private var receiveMessages = true private var _lastMsgTime = Date.now - var messagesChannel: ((ChatResponse) -> Void)? = nil + var messagesChannel: ((APIResult) -> Void)? = nil static let shared = ChatReceiver() @@ -1930,7 +1969,12 @@ class ChatReceiver { while self.receiveMessages { if let msg = await chatRecvMsg() { self._lastMsgTime = .now - await processReceivedMsg(msg) + Task { await TerminalItems.shared.addResult(msg) } + switch msg { + case let .result(evt): await processReceivedMsg(evt) + case let .error(err): logger.debug("chatRecvMsg error: \(responseError(err))") + case let .invalid(type, json): logger.debug("chatRecvMsg event: * \(type) \(dataToString(json))") + } if let messagesChannel { messagesChannel(msg) } @@ -1947,10 +1991,7 @@ 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)") @@ -2187,16 +2228,22 @@ func processReceivedMsg(_ res: ChatResponse) async { _ = m.upsertGroupMember(groupInfo, member) } } - case let .deletedMemberUser(user, groupInfo, _): // TODO update user member + case let .deletedMemberUser(user, groupInfo, member, withMessages): // TODO update user member if active(user) { await MainActor.run { m.updateGroup(groupInfo) + if withMessages { + m.removeMemberItems(groupInfo.membership, byMember: member, groupInfo) + } } } - case let .deletedMember(user, groupInfo, _, deletedMember): + case let .deletedMember(user, groupInfo, byMember, deletedMember, withMessages): if active(user) { await MainActor.run { _ = m.upsertGroupMember(groupInfo, deletedMember) + if withMessages { + m.removeMemberItems(deletedMember, byMember: byMember, groupInfo) + } } } case let .leftMember(user, groupInfo, member): @@ -2262,6 +2309,10 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE await chatItemSimpleUpdate(user, aChatItem) +// TODO when aChatItem added +// case let .rcvFileAcceptedSndCancelled(user, aChatItem, _): // usually rcvFileAcceptedSndCancelled is a response, but it's also an event for XFTP files auto-accepted from NSE +// await chatItemSimpleUpdate(user, aChatItem) +// Task { cleanupFile(aChatItem) } case let .rcvFileStart(user, aChatItem): await chatItemSimpleUpdate(user, aChatItem) case let .rcvFileComplete(user, aChatItem): diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 10120db185..f8d69c5fc8 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -19,6 +19,7 @@ struct SimpleXApp: App { @Environment(\.scenePhase) var scenePhase @State private var enteredBackgroundAuthenticated: TimeInterval? = nil + @State private var appOpenUrlLater: URL? init() { DispatchQueue.global(qos: .background).sync { @@ -42,7 +43,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 { + appOpenUrlLater = url + } } .onAppear() { // Present screen for continue migration if it wasn't finished yet @@ -93,7 +98,16 @@ struct SimpleXApp: App { if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { await updateCallInvitations() } + if let url = appOpenUrlLater { + await MainActor.run { + appOpenUrlLater = nil + chatModel.appOpenUrl = url + } + } } + } else if let url = appOpenUrlLater { + appOpenUrlLater = nil + chatModel.appOpenUrl = url } } } 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/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 8fe4260a1e..8194c8fe6f 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import SimpleXChat +@preconcurrency import SimpleXChat func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View { HStack { 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/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 bc44afdd7f..b0b404d8b5 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) { 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 4c8221d887..d30369339d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -31,7 +31,9 @@ struct CIImageView: View { .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 } @@ -43,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: @@ -80,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..f9dbaede63 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -21,15 +21,15 @@ 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) .font(.caption) .lineLimit(1) @@ -37,10 +37,32 @@ struct CILinkView: View { } .padding(.horizontal, 12) .frame(maxWidth: .infinity, alignment: .leading) + .simultaneousGesture(TapGesture().onEnded { + openBrowserAlert(uri: linkPreview.uri) + }) } } } +func openBrowserAlert(uri: URL) { + showAlert( + NSLocalizedString("Open link?", comment: "alert title"), + message: uri.absoluteString, + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .default, + handler: { _ in } + ), + UIAlertAction( + title: NSLocalizedString("Open", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(uri) } + ) + ]} + ) +} + struct LargeLinkPreview_Previews: PreviewProvider { static var previews: some View { let preview = LinkPreview( diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift index 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..4e5713c263 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -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 { @@ -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 e2fec02ba1..715e606a74 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)" } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index e411befbfa..b27d266d8a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -23,8 +23,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,7 +55,7 @@ struct FramedItemView: View { if let qi = chatItem.quotedItem { ciQuoteView(qi) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) { withAnimation { scrollToItemId(ci.id) @@ -67,7 +65,7 @@ struct FramedItemView: View { } 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) } @@ -89,19 +87,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 } @@ -159,7 +157,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) @@ -203,6 +201,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): @@ -244,7 +243,8 @@ struct FramedItemView: View { // if enable this always, size of the framed voice message item will be incorrect after end of playback .overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } } .frame(minWidth: msgWidth, alignment: .leading) - .background(chatItemFrameContextColor(chatItem, theme)) + .background(backgroundColor) + .environment(\.containerBackground, UIColor(backgroundColor)) if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth { v.frame(maxWidth: mediaWidth, alignment: .leading) } else { @@ -270,14 +270,12 @@ struct FramedItemView: View { .padding(.top, 6) .padding(.horizontal, 12) } - + + @inline(__always) private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View { - toggleSecrets(qi.formattedText, $showQuoteSecrets, - MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, showSecrets: showQuoteSecrets) - .lineLimit(lines) - .font(.subheadline) - .padding(.bottom, 6) - ) + MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, textStyle: .subheadline) + .lineLimit(lines) + .padding(.bottom, 6) } private func ciQuoteIconView(_ image: String) -> some View { @@ -297,21 +295,22 @@ struct FramedItemView: View { } } - @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) @@ -351,14 +350,6 @@ struct FramedItemView: View { } } -@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 - } -} - func isRightToLeft(_ s: String) -> Bool { if let lang = CFStringTokenizerCopyBestStringLanguage(s as CFString, CFRange(location: 0, length: min(s.count, 80))) { return NSLocale.characterDirection(forLanguage: lang as String) == .rightToLeft diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift index c3aad9490d..10e5efa298 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift @@ -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/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 0bb2463d23..e04584dfff 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -11,53 +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() @@ -67,119 +88,276 @@ struct MsgContentView: View { private func stopTyping() { timer?.invalidate() timer = nil + typingIdx = 0 } - private func msgContentView() -> Text { - var v = messageText(text, formattedText, sender, mentions: mentions, userMemberId: userMemberId, 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, backgroundColor: containerBackground, prefix: prefix) + let s = r.string + let t: Text if let mt = meta { if mt.isLive { - v = v + typingIndicator(mt.recent) + s.append(typingIndicator(mt.recent)) } - v = v + reserveSpaceForMeta(mt) + t = Text(AttributedString(s)) + reserveSpaceForMeta(mt) + } else { + t = Text(AttributedString(s)) } - return v + return msgTextResultView(r, t, showSecrets: $showSecrets) } - private func typingIndicator(_ recent: Bool) -> Text { - return (recent ? typingIndicators[typingIdx] : noTyping) - .font(.body.monospaced()) - .kerning(-2) - .foregroundColor(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, mentions: [String: CIMention]?, userMemberId: String?, 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, mentions: mentions, userMemberId: userMemberId) - var i = 1 - while i < ft.count { - res = res + formatText(ft[i], preview, showSecret: showSecrets, mentions: mentions, userMemberId: userMemberId) - 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) -> some View { + t.if(r.hasSecrets, transform: hiddenSecretsView) + .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets)) } } -private func formatText(_ ft: FormattedText, _ preview: Bool, showSecret: Bool, mentions: [String: CIMention]?, userMemberId: String?) -> 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: "") - } - case let .mention(memberName): - if let m = mentions?[memberName] { - if let ref = m.memberRef { - let name: String = if let alias = ref.localAlias, alias != "" { - "\(alias) (\(ref.displayName))" - } else { - ref.displayName +@inline(__always) +private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding>? = nil) -> some View { + return GeometryReader { g in + Rectangle() + .fill(Color.clear) + .contentShape(Rectangle()) + .simultaneousGesture(DragGesture(minimumDistance: 0).onEnded { event in + let t = event.translation + if t.width * t.width + t.height * t.height > 100 { return } + let framesetter = CTFramesetterCreateWithAttributedString(s as CFAttributedString) + let path = CGPath(rect: CGRect(origin: .zero, size: g.size), transform: nil) + let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, s.length), path, nil) + let point = CGPoint(x: event.location.x, y: g.size.height - event.location.y) // Flip y for UIKit + var index: CFIndex? + if let lines = CTFrameGetLines(frame) as? [CTLine] { + var origins = [CGPoint](repeating: .zero, count: lines.count) + CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins) + for i in 0 ..< lines.count { + let bounds = CTLineGetBoundsWithOptions(lines[i], .useOpticalBounds) + if bounds.offsetBy(dx: origins[i].x, dy: origins[i].y).contains(point) { + index = CTLineGetStringIndexForPosition(lines[i], point) + break + } } - let tName = mentionText(name) - return m.memberId == userMemberId ? tName.foregroundColor(.accentColor) : tName - } else { - return mentionText(memberName) } + if let index, let (url, browser) = attributedStringLink(s, for: index) { + if browser { + openBrowserAlert(uri: url) + } else { + UIApplication.shared.open(url) + } + } + }) + } + + func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (URL, Bool)? { + var linkURL: URL? + var browser: Bool = false + s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in + if index >= range.location && index < range.location + range.length { + if let url = attrs[linkAttrKey] as? NSURL { + linkURL = url.absoluteURL + browser = attrs[webLinkAttrKey] != nil + } else if let showSecrets, let i = attrs[secretAttrKey] as? Int { + if showSecrets.wrappedValue.contains(i) { + showSecrets.wrappedValue.remove(i) + } else { + showSecrets.wrappedValue.insert(i) + } + } + stop.pointee = true } - return Text(t) - 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 mentionText(_ name: String) -> Text { - Text(name.contains(" @") ? "@'\(name)'" : "@\(name)").fontWeight(.semibold) +func hiddenSecretsView(_ v: V) -> some View { + v.overlay( + GeometryReader { g in + let size = (g.size.width + g.size.height) / 1.4142 + Image("vertical_logo") + .resizable(resizingMode: .tile) + .frame(width: size, height: size) + .rotationEffect(.degrees(45), anchor: .center) + .position(x: g.size.width / 2, y: g.size.height / 2) + .clipped() + .saturation(0.65) + .opacity(0.35) + } + .mask(v) + ) } -private func 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() +private let linkAttrKey = NSAttributedString.Key("chat.simplex.app.link") + +private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink") + +private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret") + +typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool) + +func messageText( + _ text: String, + _ formattedText: [FormattedText]?, + textStyle: UIFont.TextStyle = .body, + sender: String?, + preview: Bool = false, + mentions: [String: CIMention]?, + userMemberId: String?, + showSecrets: Set?, + backgroundColor: UIColor, + prefix: NSAttributedString? = nil +) -> MsgTextResult { + let res = NSMutableAttributedString() + let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) + let font = UIFont.preferredFont(forTextStyle: textStyle) + let plain: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: UIColor.label + ] + let secretColor = backgroundColor.withAlphaComponent(1) + var link: [NSAttributedString.Key: Any]? + var hasSecrets = false + var handleTaps = false + + if let sender { + if preview { + res.append(NSAttributedString(string: sender + ": ", attributes: plain)) + } else { + var attrs = plain + attrs[.font] = UIFont(descriptor: descr.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.medium]]), size: descr.pointSize) + res.append(NSAttributedString(string: sender, attributes: attrs)) + res.append(NSAttributedString(string: ": ", attributes: plain)) + } + } + + if let prefix { + res.append(prefix) + } + + if let fts = formattedText, fts.count > 0 { + var bold: UIFont? + var italic: UIFont? + var snippet: UIFont? + var mention: UIFont? + var secretIdx: Int = 0 + for ft in fts { + var t = ft.text + var attrs = plain + switch (ft.format) { + case .bold: + bold = bold ?? UIFont(descriptor: descr.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold]]), size: descr.pointSize) + attrs[.font] = bold + case .italic: + italic = italic ?? UIFont(descriptor: descr.withSymbolicTraits(.traitItalic) ?? descr, size: descr.pointSize) + attrs[.font] = italic + case .strikeThrough: + attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue + case .snippet: + snippet = snippet ?? UIFont.monospacedSystemFont(ofSize: descr.pointSize, weight: .regular) + attrs[.font] = snippet + case .secret: + if let showSecrets { + if !showSecrets.contains(secretIdx) { + attrs[.foregroundColor] = UIColor.clear + attrs[.backgroundColor] = secretColor + } + attrs[secretAttrKey] = secretIdx + secretIdx += 1 + handleTaps = true + } else { + attrs[.foregroundColor] = UIColor.clear + attrs[.backgroundColor] = secretColor + } + hasSecrets = true + case let .colored(color): + if let c = color.uiColor { + attrs[.foregroundColor] = UIColor(c) + } + case .uri: + attrs = linkAttrs() + if !preview { + let s = t.lowercased() + let link = s.hasPrefix("http://") || s.hasPrefix("https://") + ? t + : "https://" + t + attrs[linkAttrKey] = NSURL(string: link) + attrs[webLinkAttrKey] = true + handleTaps = true + } + case let .simplexLink(linkType, simplexUri, smpHosts): + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = NSURL(string: simplexUri) + handleTaps = true + } + if case .description = privacySimplexLinkModeDefault.get() { + t = simplexLinkText(linkType, smpHosts) + } + case let .mention(memberName): + if let m = mentions?[memberName] { + mention = mention ?? UIFont(descriptor: descr.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: descr.pointSize) + attrs[.font] = mention + if let ref = m.memberRef { + let name: String = if let alias = ref.localAlias, alias != "" { + "\(alias) (\(ref.displayName))" + } else { + ref.displayName + } + if m.memberId == userMemberId { + attrs[.foregroundColor] = UIColor.tintColor + } + t = mentionText(name) + } else { + t = mentionText(memberName) + } + } + case .email: + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = NSURL(string: "mailto:" + ft.text) + handleTaps = true + } + case .phone: + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = NSURL(string: "tel:" + t.replacingOccurrences(of: " ", with: "")) + handleTaps = true + } + case .none: () + } + res.append(NSAttributedString(string: t, attributes: attrs)) + } + } else { + res.append(NSMutableAttributedString(string: text, attributes: plain)) + } + + return (string: res, hasSecrets: hasSecrets, handleTaps: handleTaps) + + func linkAttrs() -> [NSAttributedString.Key: Any] { + link = link ?? [ + .font: font, + .foregroundColor: uiLinkColor, + .underlineStyle: NSUnderlineStyle.single.rawValue + ] + return link! + } +} + +@inline(__always) +private func mentionText(_ name: String) -> String { + name.contains(" @") ? "@'\(name)'" : "@\(name)" } func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String { @@ -193,9 +371,9 @@ struct MsgContentView_Previews: PreviewProvider { chat: Chat.sampleData, text: chatItem.text, formattedText: chatItem.formattedText, + textStyle: .body, sender: chatItem.memberDisplayName, - meta: chatItem.meta, - showSecrets: false + meta: chatItem.meta ) .environmentObject(Chat.sampleData) } diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index 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 b03169974e..cd75d1b0cd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -131,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() @@ -197,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 { @@ -227,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: UIColor(backgroundColor)) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(ci, theme)) + .background(backgroundColor) .modifier(ChatItemClipped()) .contextMenu { if itemVersion.msgContent.text != "" { @@ -257,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: UIColor) -> some View { if text != "" { - TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId) + TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId, backgroundColor: backgroundColor) } else { Text("no text") .italic() @@ -274,14 +275,16 @@ struct ChatItemInfoView: View { var sender: String? = nil var mentions: [String: CIMention]? var userMemberId: String? - @State private var showSecrets = false + var backgroundColor: UIColor + @State private var showSecrets: Set = [] var body: some View { - toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, secondaryColor: theme.colors.secondary)) + let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: backgroundColor) + return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) } } - @ViewBuilder private func quoteTab(_ qi: CIQuote) -> some View { + private func quoteTab(_ qi: CIQuote) -> some View { GeometryReader { g in let maxWidth = (g.size.width - 32) * 0.84 ScrollView { @@ -299,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: UIColor(backgroundColor)) .padding(.horizontal, 12) .padding(.vertical, 6) .background(quotedMsgFrameColor(qi, theme)) @@ -334,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() @@ -354,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: { @@ -371,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) @@ -402,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() @@ -417,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 { diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 5d09e153d5..f5558bcd93 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,6 +31,11 @@ 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 { diff --git a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift index add28cd7f9..07034cf8ec 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift @@ -60,6 +60,8 @@ func apiLoadMessages( chatState.unreadTotal = chat.chatStats.unreadCount chatState.unreadAfter = navInfo.afterUnread chatState.unreadAfterNewestLoaded = navInfo.afterUnread + + PreloadState.shared.clear() } case let .before(paginationChatItemId, _): newItems.append(contentsOf: oldItems) @@ -104,19 +106,22 @@ func apiLoadMessages( } } case .around: - let newSplits: [Int64] + var newSplits: [Int64] if openAroundItemId == nil { newItems.append(contentsOf: oldItems) newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed) } else { newSplits = [] } - // currently, items will always be added on top, which is index 0 - newItems.insert(contentsOf: chat.chatItems, at: 0) + let (itemIndex, splitIndex) = indexToInsertAround(chat.chatInfo.chatType, chat.chatItems.last, to: newItems, Set(newSplits)) + //indexToInsertAroundTest() + newItems.insert(contentsOf: chat.chatItems, at: itemIndex) + newSplits.insert(chat.chatItems.last!.id, at: splitIndex) let newReversed: [ChatItem] = newItems.reversed() + let orderedSplits = newSplits await MainActor.run { ItemsModel.shared.reversedChatItems = newReversed - chatState.splits = [chat.chatItems.last!.id] + newSplits + chatState.splits = orderedSplits chatState.unreadAfterItemId = chat.chatItems.last!.id chatState.totalAfter = navInfo.afterTotal chatState.unreadTotal = chat.chatStats.unreadCount @@ -130,14 +135,16 @@ func apiLoadMessages( // no need to set it, count will be wrong // chatState.unreadAfterNewestLoaded = navInfo.afterUnread } + PreloadState.shared.clear() } case .last: newItems.append(contentsOf: oldItems) - removeDuplicates(&newItems, chat) + let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, chatState.splits) newItems.append(contentsOf: chat.chatItems) let items = newItems await MainActor.run { ItemsModel.shared.reversedChatItems = items.reversed() + chatState.splits = newSplits chatModel.updateChatInfo(chat.chatInfo) chatState.unreadAfterNewestLoaded = 0 } @@ -234,10 +241,14 @@ private func removeDuplicatesAndModifySplitsOnAfterPagination( let indexInSplitRanges = splits.firstIndex(of: paginationChatItemId) // Currently, it should always load from split range let loadingFromSplitRange = indexInSplitRanges != nil - var splitsToMerge: [Int64] = if let indexInSplitRanges, loadingFromSplitRange && indexInSplitRanges + 1 <= splits.count { - Array(splits[indexInSplitRanges + 1 ..< splits.count]) + 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) @@ -257,8 +268,8 @@ private func removeDuplicatesAndModifySplitsOnAfterPagination( }) var newSplits: [Int64] = [] if firstItemIdBelowAllSplits != nil { - // no splits anymore, all were merged with bottom items - newSplits = [] + // no splits below anymore, all were merged with bottom items + newSplits = topSplits } else { if !splitsToRemove.isEmpty { var new = splits @@ -320,6 +331,28 @@ private func removeDuplicatesAndUpperSplits( 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 @@ -340,3 +373,139 @@ 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 index 2e9dac166a..0a55ed48cc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift @@ -321,6 +321,101 @@ class ActiveChatState { unreadAfter = 0 unreadAfterNewestLoaded = 0 } + + func itemsRead(_ itemIds: Set?, _ 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 { @@ -359,101 +454,3 @@ func visibleItemIndexesNonReversed(_ listState: EndlessScrollView.Li // visible items mapped to their underlying data structure which is ItemsModel.shared.reversedChatItems.reversed() return range } - -class RecalculatePositions { - private var chatState: ActiveChatState { get { ItemsModel.shared.chatState } } - - func read(_ itemIds: Set?, _ newItems: [ChatItem]) { - guard let itemIds else { - // special case when the whole chat became read - chatState.unreadTotal = 0 - chatState.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 = chatState.unreadTotal - var newUnreadAfter = chatState.unreadAfter - while i >= 0 { - let item = newItems[i] - if item.id == chatState.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 - } - chatState.unreadTotal = newUnreadTotal - chatState.unreadAfter = newUnreadAfter - } - func added(_ item: (Int64, Bool), _ index: Int) { - if item.1 { - chatState.unreadAfter += 1 - chatState.unreadTotal += 1 - } - } - func removed(_ itemIds: [(Int64, Int, Bool)], _ newItems: [ChatItem]) { - var newSplits: [Int64] = [] - for split in chatState.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) - } - } - chatState.splits = newSplits - - let index = itemIds.firstIndex(where: { (delId, _, _) in delId == chatState.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 { - chatState.unreadAfterItemId = newUnreadAfterItemId - chatState.totalAfter -= itemIds.filter { (_, delIndex, _) in delIndex > index }.count - chatState.unreadTotal -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex <= index && isRcvNew }.count - chatState.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 { - chatState.unreadTotal += 1 - chatState.unreadAfter -= 1 - } - } - } else { - // all items were deleted, 0 items in chatItems - chatState.unreadAfterItemId = -1 - chatState.totalAfter = 0 - chatState.unreadTotal = 0 - chatState.unreadAfter = 0 - } - } else { - chatState.totalAfter -= itemIds.count - } - } - func cleared() { chatState.clear() } -} diff --git a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift index 873f24d5c3..c1a1eec7d2 100644 --- a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift +++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift @@ -9,25 +9,23 @@ import SwiftUI import SimpleXChat -func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Binding, _ chat: Chat) { - if ItemsModel.shared.chatState.totalAfter == 0 { - return +func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Binding, _ chat: Chat) async { + await MainActor.run { + loadingMoreItems.wrappedValue = true + loadingBottomItems.wrappedValue = true } - loadingMoreItems.wrappedValue = true - loadingBottomItems.wrappedValue = true - Task { - try? await Task.sleep(nanoseconds: 500_000000) - if ChatModel.shared.chatId != chat.chatInfo.id { - await MainActor.run { - loadingMoreItems.wrappedValue = false - } - return - } - await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState) + try? await Task.sleep(nanoseconds: 500_000000) + if ChatModel.shared.chatId != chat.chatInfo.id { await MainActor.run { loadingMoreItems.wrappedValue = false loadingBottomItems.wrappedValue = false } + return + } + await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState) + await MainActor.run { + loadingMoreItems.wrappedValue = false + loadingBottomItems.wrappedValue = false } } @@ -36,6 +34,12 @@ class PreloadState { var prevFirstVisible: Int64 = Int64.min var prevItemsCount: Int = 0 var preloading: Bool = false + + func clear() { + prevFirstVisible = Int64.min + prevItemsCount = 0 + preloading = false + } } func preloadIfNeeded( @@ -43,26 +47,41 @@ func preloadIfNeeded( _ ignoreLoadingRequests: Binding, _ listState: EndlessScrollView.ListState, _ mergedItems: BoxedValue, - loadItems: @escaping (Bool, ChatPagination) async -> Bool + loadItems: @escaping (Bool, ChatPagination) async -> Bool, + loadLastItems: @escaping () async -> Void ) { let state = PreloadState.shared guard !listState.isScrolling && !listState.isAnimatedScrolling, - state.prevFirstVisible != listState.firstVisibleItemIndex || state.prevItemsCount != mergedItems.boxedValue.indexInParentItems.count, !state.preloading, listState.totalItemsCount > 0 else { return } - state.prevFirstVisible = listState.firstVisibleItemId as! Int64 - state.prevItemsCount = mergedItems.boxedValue.indexInParentItems.count - state.preloading = true - let allowLoadMore = allowLoadMoreItems.wrappedValue - Task { - defer { - state.preloading = false + if state.prevFirstVisible != listState.firstVisibleItemId as! Int64 || state.prevItemsCount != mergedItems.boxedValue.indexInParentItems.count { + state.preloading = true + let allowLoadMore = allowLoadMoreItems.wrappedValue + Task { + defer { state.preloading = false } + var triedToLoad = true + await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in + triedToLoad = await loadItems(false, pagination) + return triedToLoad + } + if triedToLoad { + state.prevFirstVisible = listState.firstVisibleItemId as! Int64 + state.prevItemsCount = mergedItems.boxedValue.indexInParentItems.count + } + // it's important to ask last items when the view is fully covered with items. Otherwise, visible items from one + // split will be merged with last items and position of scroll will change unexpectedly. + if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded { + await loadLastItems() + } } - await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in - await loadItems(false, pagination) + } else if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded { + state.preloading = true + Task { + defer { state.preloading = false } + await loadLastItems() } } } @@ -105,6 +124,7 @@ async { let triedToLoad = await loadItems(ChatPagination.before(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT)) if triedToLoad && sizeWas == ItemsModel.shared.reversedChatItems.count && firstItemIdWas == ItemsModel.shared.reversedChatItems.last?.id { ignoreLoadingRequests.wrappedValue = loadFromItemId + return false } return triedToLoad } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 90c9629352..c136ebc01b 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -45,7 +45,7 @@ struct ChatView: View { @State private var selectedMember: GMember? = nil // opening GroupLinkView on link button (incognito) @State private var showGroupLinkSheet: Bool = false - @State private var groupLink: String? + @State private var groupLink: CreatedConnLink? @State private var groupLinkMemberRole: GroupMemberRole = .member @State private var forwardedChatItems: [ChatItem] = [] @State private var selectedChatItems: Set? = nil @@ -71,10 +71,9 @@ struct ChatView: View { } } - @ViewBuilder private var viewBody: some View { let cInfo = chat.chatInfo - ZStack { + 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) @@ -91,18 +90,32 @@ struct ChatView: View { if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty { GroupMentionsView(groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible) } - FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel) + FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: { + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) + } + ) } connectingText() if selectedChatItems == nil { + let reason = chat.chatInfo.userCantSendReason ComposeView( chat: chat, composeState: $composeState, keyboardVisible: $keyboardVisible, keyboardHiddenDate: $keyboardHiddenDate, - selectedRange: $selectedRange + selectedRange: $selectedRange, + disabledText: reason?.composeLabel ) .disabled(!cInfo.sendMsgEnabled) + .if(!cInfo.sendMsgEnabled) { v in + v.disabled(true).onTapGesture { + AlertManager.shared.showAlertMsg( + title: "You can't send messages!", + message: reason?.alertMessage + ) + } + } } else { SelectedItemsBottomToolbar( chatItems: ItemsModel.shared.reversedChatItems, @@ -262,7 +275,6 @@ struct ChatView: View { // this may already being loading because of changed chat id (see .onChange(of: chat.id) if !loadingBottomItems { - loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) allowLoadMoreItems = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { allowLoadMoreItems = true @@ -278,7 +290,7 @@ struct ChatView: View { if chatModel.chatId == nil { chatModel.chatItemStatuses = [:] ItemsModel.shared.reversedChatItems = [] - ItemsModel.shared.chatItemsChangesListener.cleared() + ItemsModel.shared.chatState.clear() chatModel.groupMembers = [] chatModel.groupMembersIndexes.removeAll() chatModel.membersLoaded = false @@ -494,7 +506,7 @@ struct ChatView: View { Button ("Cancel") { closeSearch() - Task { await loadChat(chat: chat) } + searchTextChanged("") } } .padding(.horizontal) @@ -570,13 +582,20 @@ struct ChatView: View { .onChange(of: im.reversedChatItems) { items in mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState) scrollView.updateItems(mergedItems.boxedValue.items) + if im.itemAdded { + im.itemAdded = false + if scrollView.listState.firstVisibleItemIndex < 2 { + scrollView.scrollToBottomAnimated() + } else { + scrollView.scroll(by: 34) + } + } } .onChange(of: revealedItems) { revealed in mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealed, im.chatState) scrollView.updateItems(mergedItems.boxedValue.items) } .onChange(of: chat.id) { _ in - loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) allowLoadMoreItems = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { allowLoadMoreItems = true @@ -585,33 +604,8 @@ struct ChatView: View { .padding(.vertical, -100) .onTapGesture { hideKeyboard() } .onChange(of: searchText) { s in - guard showSearch else { return } - Task { - await loadChat(chat: chat, search: s) - mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) - await MainActor.run { - scrollView.updateItems(mergedItems.boxedValue.items) - } - if !s.isEmpty { - scrollView.scrollToBottom() - } else if let index = scrollView.listState.items.lastIndex(where: { $0.hasUnread() }) { - // scroll to the top unread item - scrollView.scrollToItem(index) - loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) - } else { - scrollView.scrollToBottom() - loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) - } - } - } - .onChange(of: im.itemAdded) { added in - if added { - im.itemAdded = false - if scrollView.listState.firstVisibleItemIndex < 2 { - scrollView.scrollToBottomAnimated() - } else { - scrollView.scroll(by: 34) - } + if showSearch { + searchTextChanged(s) } } } @@ -646,12 +640,29 @@ struct ChatView: View { if let unreadIndex { scrollView.scrollToItem(unreadIndex) } - loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { allowLoadMoreItems = true } } + private func searchTextChanged(_ s: String) { + Task { + await loadChat(chat: chat, search: s) + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + await MainActor.run { + scrollView.updateItems(mergedItems.boxedValue.items) + } + if !s.isEmpty { + scrollView.scrollToBottom() + } else if let index = scrollView.listState.items.lastIndex(where: { $0.hasUnread() }) { + // scroll to the top unread item + scrollView.scrollToItem(index) + } else { + scrollView.scrollToBottom() + } + } + } + class FloatingButtonModel: ObservableObject { @Published var unreadAbove: Int = 0 @Published var unreadBelow: Int = 0 @@ -728,6 +739,7 @@ struct ChatView: View { let theme: AppTheme let scrollView: EndlessScrollView let chat: Chat + @Binding var loadingMoreItems: Bool @Binding var loadingTopItems: Bool @Binding var requestedTopScroll: Bool @Binding var loadingBottomItems: Bool @@ -735,6 +747,7 @@ struct ChatView: View { @Binding var animatedScrollingInProgress: Bool let listState: EndlessScrollView.ListState @ObservedObject var model: FloatingButtonModel + let reloadItems: () -> Void var body: some View { ZStack(alignment: .top) { @@ -792,7 +805,7 @@ struct ChatView: View { } } .onTapGesture { - if loadingBottomItems { + if loadingBottomItems || !ItemsModel.shared.lastItemsLoaded { requestedTopScroll = false requestedBottomScroll = true } else { @@ -812,7 +825,7 @@ struct ChatView: View { } } .onChange(of: loadingBottomItems) { loading in - if !loading && requestedBottomScroll { + if !loading && requestedBottomScroll && ItemsModel.shared.lastItemsLoaded { requestedBottomScroll = false scrollToBottom() } @@ -821,15 +834,25 @@ struct ChatView: View { } private func scrollToTopUnread() { - if let index = listState.items.lastIndex(where: { $0.hasUnread() }) { - animatedScrollingInProgress = true - // scroll to the top unread item - Task { + Task { + if !ItemsModel.shared.chatState.splits.isEmpty { + await MainActor.run { loadingMoreItems = true } + await loadChat(chatId: chat.id, openAroundItemId: nil, clearItems: false) + await MainActor.run { reloadItems() } + if let index = listState.items.lastIndex(where: { $0.hasUnread() }) { + await MainActor.run { animatedScrollingInProgress = true } + await scrollView.scrollToItemAnimated(index) + await MainActor.run { animatedScrollingInProgress = false } + } + await MainActor.run { loadingMoreItems = false } + } else if let index = listState.items.lastIndex(where: { $0.hasUnread() }) { + await MainActor.run { animatedScrollingInProgress = true } + // scroll to the top unread item await scrollView.scrollToItemAnimated(index) await MainActor.run { animatedScrollingInProgress = false } + } else { + logger.debug("No more unread items, total: \(listState.items.count)") } - } else { - logger.debug("No more unread items, total: \(listState.items.count)") } } @@ -1144,6 +1167,11 @@ struct ChatView: View { } else { await loadChatItems(chat, pagination) } + }, + loadLastItems: { + if !loadingMoreItems { + await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) + } } ) } @@ -1244,18 +1272,11 @@ struct ChatView: View { nil } let showAvatar = shouldShowAvatar(item, listItem.nextItem) - let itemSeparation: ItemSeparation let single = switch merged { case .single: true default: false } - if single || revealed { - let prev = listItem.prevItem - itemSeparation = getItemSeparation(item, prev) - let nextForGap = (item.mergeCategory != nil && item.mergeCategory == prev?.mergeCategory) || isLastItem ? nil : listItem.nextItem - } else { - itemSeparation = getItemSeparation(item, nil) - } + let itemSeparation = getItemSeparation(item, single || revealed ? listItem.prevItem: nil) return VStack(spacing: 0) { if let last { DateSeparator(date: last.meta.itemTs).padding(8) @@ -1265,10 +1286,10 @@ struct ChatView: View { 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 { @@ -1317,6 +1338,9 @@ struct ChatView: View { 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) @@ -1454,7 +1478,7 @@ struct ChatView: View { } HStack(alignment: .top, spacing: 10) { MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background) - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { if let mem = m.getGroupMember(member.groupMemberId) { selectedMember = mem } else { @@ -1463,7 +1487,7 @@ struct ChatView: View { m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1 selectedMember = mem } - } + }) chatItemWithMenu(ci, range, maxWidth, itemSeparation) .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } } @@ -1513,9 +1537,9 @@ 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) { + return VStack(alignment: alignment.horizontal, spacing: 3) { HStack { if ci.chatDir.sent { goToItemButton(true) @@ -1607,9 +1631,9 @@ 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): @@ -2199,15 +2223,12 @@ struct ChatView: View { } func goToItemInnerButton(_ alignStart: Bool, _ image: String, touchInProgress: Bool, _ onClick: @escaping () -> Void) -> some View { - Button { - onClick() - } label: { - Image(systemName: image) - .resizable() - .frame(width: 13, height: 13) - .padding([alignStart ? .trailing : .leading], 10) - .tint(theme.colors.secondary.opacity(touchInProgress ? 1.0 : 0.4)) - } + 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 @@ -2368,7 +2389,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 diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index 6c44aeea83..e629a984df 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() diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 6ded9cae72..8993de886f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -327,6 +327,7 @@ struct ComposeView: View { @Binding var keyboardVisible: Bool @Binding var keyboardHiddenDate: Date @Binding var selectedRange: NSRange + var disabledText: LocalizedStringKey? = nil @State var linkUrl: URL? = nil @State var hasSimplexLink: Bool = false @@ -391,7 +392,7 @@ struct ComposeView: View { Image(systemName: "paperclip") .resizable() } - .disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false)) + .disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled || (chat.chatInfo.contact?.nextSendGrpInv ?? false)) .frame(width: 25, height: 25) .padding(.bottom, 16) .padding(.leading, 12) @@ -441,19 +442,13 @@ struct ComposeView: View { : theme.colors.primary ) .padding(.trailing, 12) - .disabled(!chat.userCanSend) + .disabled(!chat.chatInfo.sendMsgEnabled) - if chat.userIsObserver { - Text("you are observer") + if let disabledText { + Text(disabledText) .italic() .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) - .onTapGesture { - AlertManager.shared.showAlertMsg( - title: "You can't send messages!", - message: "Please contact group admin." - ) - } } } } @@ -479,8 +474,8 @@ struct ComposeView: View { hasSimplexLink = false } } - .onChange(of: chat.userCanSend) { canSend in - if !canSend { + .onChange(of: chat.chatInfo.sendMsgEnabled) { sendEnabled in + if !sendEnabled { cancelCurrentVoiceRecording() clearCurrentDraft() clearState() @@ -1254,11 +1249,14 @@ struct ComposeView: View { if pendingLinkUrl == url { composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) getLinkPreview(url: url) { linkPreview in - if let linkPreview = linkPreview, - pendingLinkUrl == url { + if let linkPreview, pendingLinkUrl == url { composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) - pendingLinkUrl = nil + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + composeState = composeState.copy(preview: .noPreview) + } } + pendingLinkUrl = nil } } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index d5b5e6ccf4..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, mentions: contextItem.mentions, userMemberId: nil, 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/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 30767d66ec..e7b02c9aea 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -15,6 +15,7 @@ struct SendMessageView: View { @Binding var composeState: ComposeState @Binding var selectedRange: NSRange @EnvironmentObject var theme: AppTheme + @Environment(\.isEnabled) var isEnabled var sendMessage: (Int?) -> Void var sendLiveMessage: (() async -> Void)? = nil var updateLiveMessage: (() async -> Void)? = nil @@ -255,6 +256,7 @@ struct SendMessageView: View { } private struct RecordVoiceMessageButton: View { + @Environment(\.isEnabled) var isEnabled @EnvironmentObject var theme: AppTheme var startVoiceMessageRecording: (() -> Void)? var finishVoiceMessageRecording: (() -> Void)? @@ -263,13 +265,12 @@ 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: 31, height: 31) .padding([.bottom, .trailing], 4) @@ -279,9 +280,7 @@ struct SendMessageView: View { pressed = ProcessInfo.processInfo.systemUptime startVoiceMessageRecording?() } else { - let now = ProcessInfo.processInfo.systemUptime - if let pressed = pressed, - now - pressed >= 1 { + if let pressed, ProcessInfo.processInfo.systemUptime - pressed >= 1 { finishVoiceMessageRecording?() } holdingVMR = false @@ -355,7 +354,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) diff --git a/apps/ios/Shared/Views/Chat/EndlessScrollView.swift b/apps/ios/Shared/Views/Chat/EndlessScrollView.swift index f04987e5f8..cc61754b26 100644 --- a/apps/ios/Shared/Views/Chat/EndlessScrollView.swift +++ b/apps/ios/Shared/Views/Chat/EndlessScrollView.swift @@ -171,6 +171,9 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu 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 @@ -284,7 +287,8 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu func updateItems(_ items: [ScrollItem], _ forceReloadVisible: Bool = false) { if !Thread.isMainThread { - fatalError("Use main thread to update items") + logger.error("Use main thread to update items") + return } if bounds.height == 0 { self.listState.items = items @@ -302,6 +306,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu 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 @@ -322,6 +327,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu 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 @@ -339,7 +345,11 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu 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 { @@ -389,6 +399,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu addSubview(vis.view) } newVisible.append(vis) + visibleItemsHeight += vis.view.frame.height nextOffsetY = vis.view.frame.origin.y } else { let vis: VisibleItem @@ -406,6 +417,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu addSubview(vis.view) } newVisible.append(vis) + visibleItemsHeight += vis.view.frame.height } if abs(nextOffsetY) < contentOffsetY && !allowOneMore { break @@ -435,6 +447,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu } offset += vis.view.frame.height newVisible.insert(vis, at: 0) + visibleItemsHeight += vis.view.frame.height if offset >= contentOffsetY + bounds.height { break } @@ -450,11 +463,15 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu prevProcessedOffset = contentOffsetY listState.visibleItems = newVisible - listState.items = items + // 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) @@ -517,7 +534,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu let y = if top { min(estimatedContentHeight.bottomOffsetY - bounds.height, item.view.frame.origin.y - insetTop) } else { - max(estimatedContentHeight.topOffsetY - insetTop, item.view.frame.origin.y + item.view.bounds.height - bounds.height + insetBottom) + 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() @@ -568,7 +585,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu let y = if top { min(estimatedContentHeight.bottomOffsetY - bounds.height, item.view.frame.origin.y - insetTop) } else { - max(estimatedContentHeight.topOffsetY - insetTop, item.view.frame.origin.y + item.view.bounds.height - bounds.height + insetBottom) + 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() @@ -632,7 +649,7 @@ class EndlessScrollView: UIScrollView, UIScrollViewDelegate, UIGestu } (view as? ReusableView)?.prepareForReuse() view.isHidden = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { if view.isHidden { view.removeFromSuperview() } } } diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 66fe67a29e..7cd543af10 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -145,9 +145,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 56d994b397..15749b0761 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -21,7 +21,7 @@ struct GroupChatInfoView: View { @State var localAlias: String @FocusState private var aliasTextFieldFocused: Bool @State private var alert: GroupChatInfoViewAlert? = nil - @State private var groupLink: String? + @State private var groupLink: CreatedConnLink? @State private var groupLinkMemberRole: GroupMemberRole = .member @State private var groupLinkNavLinkActive: Bool = false @State private var addMembersNavLinkActive: Bool = false @@ -144,17 +144,9 @@ struct GroupChatInfoView: View { let filteredMembers = s == "" ? members : members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } - MemberRowView(groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert) + MemberRowView(chat: chat, 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) - } + MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, alert: $alert) } } @@ -292,9 +284,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 } @@ -306,10 +298,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 } @@ -322,8 +311,8 @@ struct GroupChatInfoView: View { .frame(width: 1, height: 1) .hidden() } - .disabled(!groupInfo.ready) } + .disabled(!groupInfo.ready) } private func muteButton(width: CGFloat, nextNtfMode: MsgFilter) -> some View { @@ -361,6 +350,7 @@ struct GroupChatInfoView: View { } private struct MemberRowView: View { + var chat: Chat var groupInfo: GroupInfo @ObservedObject var groupMember: GMember @EnvironmentObject var theme: AppTheme @@ -369,7 +359,7 @@ struct GroupChatInfoView: View { 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 @@ -386,6 +376,20 @@ struct GroupChatInfoView: View { 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 { @@ -410,6 +414,11 @@ struct GroupChatInfoView: View { } } + private func memberInfoView() -> some View { + GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember) + .navigationBarHidden(false) + } + private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey { if member.activeConn?.connDisabled ?? false { return "disabled" @@ -488,11 +497,6 @@ struct GroupChatInfoView: View { .foregroundColor(theme.colors.secondary) } } - - private func memberInfoView(_ groupMember: GMember) -> some View { - GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember) - .navigationBarHidden(false) - } private func groupLinkButton() -> some View { NavigationLink { @@ -569,9 +573,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") diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 39288e2d52..a11c073a42 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: CreatedConnLink? @Binding var groupLinkMemberRole: GroupMemberRole var showTitle: Bool = false var creatingGroup: Bool = false var linkCreatedCb: (() -> Void)? = nil + @State private var showShortLink = true @State private var creatingLink = false @State private var alert: GroupLinkAlert? @State private var shouldCreate = true @@ -69,10 +71,10 @@ struct GroupLinkView: View { } } .frame(height: 36) - SimpleXLinkQRCode(uri: groupLink) - .id("simplex-qrcode-view-for-\(groupLink)") + SimpleXCreatedLinkQRCode(link: groupLink, short: $showShortLink) + .id("simplex-qrcode-view-for-\(groupLink.simplexChatUri(short: showShortLink))") Button { - showShareSheet(items: [simplexChatLink(groupLink)]) + showShareSheet(items: [groupLink.simplexChatUri(short: showShortLink)]) } label: { Label("Share link", systemImage: "square.and.arrow.up") } @@ -93,6 +95,10 @@ struct GroupLinkView: View { .frame(maxWidth: .infinity) } } + } header: { + if let groupLink, groupLink.connShortLink != nil { + ToggleShortLinkHeader(text: Text(""), link: groupLink, short: $showShortLink) + } } .alert(item: $alert) { alert in switch alert { @@ -158,8 +164,8 @@ struct GroupLinkView: View { struct GroupLinkView_Previews: PreviewProvider { static var previews: some View { - @State var groupLink: String? = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" - @State var noGroupLink: String? = nil + @State var groupLink: CreatedConnLink? = CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil) + @State var noGroupLink: CreatedConnLink? = nil return Group { GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member)) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 4a931ed23f..79ad242366 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -392,7 +392,7 @@ struct GroupMemberInfoView: View { await MainActor.run { progressIndicator = false chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) - ItemsModel.shared.loadOpenChat("@\(memberContact.id)") { + ItemsModel.shared.loadOpenChat(memberContact.id) { dismissAllSheets(animated: true) } NetworkModel.shared.setContactNetworkStatus(memberContact, .connected) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift index 168f0490c3..9bb4a0cc35 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -196,7 +196,9 @@ struct GroupMentionsView: View { newName = composeState.mentionMemberName(member.wrapped.memberProfile.displayName) } mentions[newName] = CIMention(groupMember: member.wrapped) - var msgMention = "@" + (newName.contains(" ") ? "'\(newName)'" : newName) + 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) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index 58bfe182cb..97bff70efb 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, mentions: nil, userMemberId: nil, showSecrets: false, secondaryColor: theme.colors.secondary) + let r = messageText(welcomeText, parseSimpleXMarkdown(welcomeText), sender: nil, mentions: nil, userMemberId: nil, showSecrets: showSecrets, backgroundColor: UIColor(theme.colors.background)) + return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) .frame(minHeight: 130, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index e3aad890bb..81d78fbadd 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -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 { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) - .frame(height: dynamicRowHeight) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { deleteContactDialog( @@ -121,6 +121,7 @@ struct ChatListNavLink: View { selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) } ) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() toggleFavoriteButton() @@ -145,7 +146,6 @@ struct ChatListNavLink: View { } .tint(.red) } - .frame(height: dynamicRowHeight) } } .alert(item: $alert) { $0.alert } @@ -163,7 +163,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,7 +183,7 @@ 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()) } @@ -203,7 +203,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() @@ -243,14 +243,14 @@ struct ChatListNavLink: View { } } - @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() } @@ -433,6 +433,7 @@ 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) } @@ -451,7 +452,6 @@ struct ChatListNavLink: View { } .tint(.red) } - .frame(height: dynamicRowHeight) .contentShape(Rectangle()) .onTapGesture { showContactRequestDialog = true } .confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) { @@ -463,6 +463,7 @@ struct ChatListNavLink: View { private func contactConnectionNavLink(_ contactConnection: PendingContactConnection) -> some View { ContactConnectionView(chat: chat) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection) { a in @@ -480,7 +481,6 @@ struct ChatListNavLink: View { } .tint(theme.colors.primary) } - .frame(height: dynamicRowHeight) .appSheet(isPresented: $showContactConnectionInfo) { Group { if case let .contactConnection(contactConnection) = chat.chatInfo { @@ -579,14 +579,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) } } @@ -595,8 +595,27 @@ 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)) + } + } + } + } +} + +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) } } } @@ -688,7 +707,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 863bfb1ae2..f34f930c6f 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -148,7 +148,10 @@ struct ChatListView: View { @State private var userPickerShown: Bool = false @State private var sheet: SomeSheet? = nil @StateObject private var chatTagsModel = ChatTagsModel.shared - + + // iOS 15 is required it to show/hide toolbar while chat is hidden/visible + @State private var viewOnScreen = true + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false @AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false @@ -203,7 +206,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 +271,7 @@ struct ChatListView: View { } } else { if oneHandUI { - content().toolbar { bottomToolbarGroup } + content().toolbar { bottomToolbarGroup() } } else { content().toolbar { topToolbar } } @@ -286,9 +299,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 +335,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 +367,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()) @@ -791,7 +798,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) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 7f92862f66..b8c8233e6e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -187,13 +187,14 @@ 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, mentionWidth + (hasFilePreview ? 38 : 36)) @@ -259,11 +260,13 @@ struct ChatPreviewView: View { } } - private func messageDraft(_ draft: ComposeState) -> Text { + 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, mentions: draft.mentions, userMemberId: nil, showSecrets: false, secondaryColor: theme.colors.secondary) + let r = messageText(msg, parseSimpleXMarkdown(msg), sender: nil, preview: true, mentions: draft.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(theme.colors.background)) + return (image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary) + + attachment() + + Text(AttributedString(r.string)), + r.hasSecrets) func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text { Text(Image(systemName: s)).foregroundColor(color) + textSpace @@ -279,10 +282,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, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: false, secondaryColor: theme.colors.secondary, prefix: prefix()) + let r = messageText(itemText, itemFormattedText, sender: cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, backgroundColor: UIColor(theme.colors.background), prefix: prefix()) + return (Text(AttributedString(r.string)), r.hasSecrets) // same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey; // can be refactored into a single function if functions calling these are changed to return same type @@ -309,19 +313,21 @@ struct ChatPreviewView: View { } } - 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 let cItem = cItem { - chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem), hasFilePreview) + let (t, hasSecrets) = chatItemPreview(cItem) + chatPreviewLayout(itemStatusMark(cItem) + t, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets) } else { switch (chat.chatInfo) { case let .direct(contact): @@ -348,7 +354,6 @@ struct ChatPreviewView: View { } @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): @@ -370,17 +375,7 @@ 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): @@ -410,7 +405,7 @@ struct ChatPreviewView: View { : chatPreviewInfoText("you are invited to group") } - @ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View { + private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View { Text(text) .frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) .padding([.leading, .trailing], 8) @@ -490,7 +485,7 @@ struct ChatPreviewView: View { } } -@ViewBuilder func groupReportsIcon(size: CGFloat) -> some View { +func groupReportsIcon(size: CGFloat) -> some View { Image(systemName: "flag") .resizable() .scaledToFit() diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 0f64b632dc..b9f5b984e1 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) @@ -167,26 +173,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..8b0a8af888 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) } @@ -587,7 +587,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 +703,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..2063fe15de 100644 --- a/apps/ios/Shared/Views/ChatList/TagListView.swift +++ b/apps/ios/Shared/Views/ChatList/TagListView.swift @@ -61,7 +61,7 @@ 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"), @@ -138,7 +138,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/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift index 242b492e83..456c46d318 100644 --- a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -140,9 +140,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 @@ -188,8 +188,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)) } 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..59eee1338b 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -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]) } 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 7c16e22571..16ab26eff7 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -65,7 +65,7 @@ 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.chatItemsChangesListener.cleared() + ItemsModel.shared.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 0fe0f2644d..87c0b80372 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: CreatedConnLink? @State private var groupLinkMemberRole: GroupMemberRole = .member var body: some View { diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 39656c1534..e5263813fa 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -85,7 +85,7 @@ struct NewChatSheet: View { } } - @ViewBuilder private func viewBody(_ showArchive: Bool) -> some View { + private func viewBody(_ showArchive: Bool) -> some View { List { HStack { ContactsListSearchBar( @@ -258,7 +258,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) diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 6e898f4cdf..110eda7882 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -81,7 +81,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 = "" @@ -174,11 +175,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 +192,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 +245,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 +264,7 @@ private struct InviteView: View { NavigationLink { ActiveProfilePicker( contactConnection: $contactConnection, - connReqInvitation: $connReqInvitation, + connLinkInvitation: $connLinkInvitation, incognitoEnabled: $incognitoDefault, choosingProfile: $choosingProfile, selectedProfile: selectedProfile @@ -296,7 +299,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 +313,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 +325,8 @@ private struct InviteView: View { .listRowBackground(Color.clear) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } header: { + ToggleShortLinkHeader(text: Text("Or show this code"), link: connLinkInvitation, short: $showShortLink) } } @@ -343,7 +348,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? @@ -415,12 +420,11 @@ private struct ActiveProfilePicker: View { } Task { do { - if let contactConn = contactConnection, - let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) { - + 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) } @@ -502,7 +506,7 @@ private struct ActiveProfilePicker: View { } } - @ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View { + private func profilerPickerUserOption(_ user: User) -> some View { Button { if selectedProfile == user && incognitoEnabled { incognitoEnabled = false @@ -836,23 +840,25 @@ func sharedProfileInfo(_ incognito: Bool) -> Text { } enum PlanAndConnectAlert: Identifiable { - case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case invitationLinkConnecting(connectionLink: String) - case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?) + case ownInvitationLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case invitationLinkConnecting(connectionLink: CreatedConnLink) + case ownContactAddressConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case contactAddressConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case groupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case groupLinkConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case groupLinkConnecting(connectionLink: CreatedConnLink, groupInfo: GroupInfo?) + case error(shortOrFullLink: String, alert: Alert) var id: String { switch self { - case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)" - case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)" - case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)" - case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)" - case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)" - case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)" - case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)" + case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink.connFullLink)" + case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink.connFullLink)" + case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink.connFullLink)" + case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink.connFullLink)" + case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink.connFullLink)" + case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink.connFullLink)" + case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink.connFullLink)" + case let .error(shortOrFullLink, alert): return "error \(shortOrFullLink)" } } } @@ -935,21 +941,22 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: ( dismissButton: .default(Text("OK")) { cleanup?() } ) } + case let .error(_, alert): return alert } } enum PlanAndConnectActionSheet: Identifiable { - case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey) + case askCurrentOrIncognitoProfile(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) + case askCurrentOrIncognitoProfileDestructive(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, title: LocalizedStringKey) case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact) - case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) + case ownGroupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) var id: String { switch self { - case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)" - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)" + case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink.connFullLink)" + case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink.connFullLink)" case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)" - case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)" + case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink.connFullLink)" } } } @@ -1008,7 +1015,7 @@ func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool } func planAndConnect( - _ connectionLink: String, + _ shortOrFullLink: String, showAlert: @escaping (PlanAndConnectAlert) -> Void, showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void, dismiss: Bool, @@ -1018,8 +1025,8 @@ func planAndConnect( filterKnownGroup: ((GroupInfo) -> Void)? = nil ) { Task { - do { - let connectionPlan = try await apiConnectPlan(connReq: connectionLink) + let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink) + if let (connectionLink, connectionPlan) = result { switch connectionPlan { case let .invitationLink(ilp): switch ilp { @@ -1028,32 +1035,40 @@ func planAndConnect( if let incognito = incognito { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) + await MainActor.run { + showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) + } } case .ownLink: logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) + await MainActor.run { + if let incognito = incognito { + showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) + } } case let .connecting(contact_): logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")") - if let contact = contact_ { - if let f = filterKnownContact { - f(contact) + await MainActor.run { + if let contact = contact_ { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + } } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) } - } else { - showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) } case let .known(contact): logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownContact { - f(contact) - } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + } } } case let .contactAddress(cap): @@ -1063,83 +1078,109 @@ func planAndConnect( if let incognito = incognito { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) + await MainActor.run { + showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) + } } case .ownLink: logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) + await MainActor.run { + if let incognito = incognito { + showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) + } } case .connectingConfirmReconnect: logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) + await MainActor.run { + if let incognito = incognito { + showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) + } } case let .connectingProhibit(contact): logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownContact { - f(contact) - } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + } } case let .known(contact): logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownContact { - f(contact) - } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + } } case let .contactViaAddress(contact): logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")") if let incognito = incognito { connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } else { - showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact)) + await MainActor.run { + showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact)) + } } } case let .groupLink(glp): switch glp { case .ok: - if let incognito = incognito { - showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group")) + await MainActor.run { + if let incognito = incognito { + showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group")) + } } case let .ownLink(groupInfo): logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownGroup { - f(groupInfo) + await MainActor.run { + if let f = filterKnownGroup { + f(groupInfo) + } + showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) } - showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) case .connectingConfirmReconnect: logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) + await MainActor.run { + if let incognito = incognito { + showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) + } } case let .connectingProhibit(groupInfo_): logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") - showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) + await MainActor.run { + showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) + } case let .known(groupInfo): logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownGroup { - f(groupInfo) - } else { - openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) } + await MainActor.run { + if let f = filterKnownGroup { + f(groupInfo) + } else { + openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) } + } } } + case let .error(chatError): + logger.debug("planAndConnect, .error \(chatErrorString(chatError))") + if let incognito = incognito { + connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup) + } else { + showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link")) + } } - } catch { - logger.debug("planAndConnect, plan error") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link")) + } else if let alert { + await MainActor.run { + showAlert(.error(shortOrFullLink: shortOrFullLink, alert: alert)) } } } @@ -1161,22 +1202,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 { @@ -1199,39 +1240,35 @@ private func connectViaLink( } func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { - Task { - let m = ChatModel.shared - if let c = m.getContactChat(contact.contactId) { - DispatchQueue.main.async { - if dismiss { - dismissAllSheets(animated: true) { - ItemsModel.shared.loadOpenChat(c.id) - showAlreadyExistsAlert?() - } - } else { - ItemsModel.shared.loadOpenChat(c.id) + let m = ChatModel.shared + if let c = m.getContactChat(contact.contactId) { + if dismiss { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(c.id) { showAlreadyExistsAlert?() } } + } else { + ItemsModel.shared.loadOpenChat(c.id) { + showAlreadyExistsAlert?() + } } } } func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { - Task { - let m = ChatModel.shared - if let g = m.getGroupChat(groupInfo.groupId) { - DispatchQueue.main.async { - if dismiss { - dismissAllSheets(animated: true) { - ItemsModel.shared.loadOpenChat(g.id) - showAlreadyExistsAlert?() - } - } else { - ItemsModel.shared.loadOpenChat(g.id) + let m = ChatModel.shared + if let g = m.getGroupChat(groupInfo.groupId) { + if dismiss { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(g.id) { showAlreadyExistsAlert?() } } + } else { + ItemsModel.shared.loadOpenChat(g.id) { + showAlreadyExistsAlert?() + } } } } @@ -1269,11 +1306,12 @@ enum ConnReqType: Equatable { } } -private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType { +private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType? { switch connectionPlan { - case .invitationLink: return .invitation - case .contactAddress: return .contact - case .groupLink: return .groupLink + case .invitationLink: .invitation + case .contactAddress: .contact + case .groupLink: .groupLink + case .error: nil } } diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index bc1dc4b5bc..453149198b 100644 --- a/apps/ios/Shared/Views/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/NewChat/QRCode.swift @@ -8,6 +8,7 @@ import SwiftUI import CoreImage.CIFilterBuiltins +import SimpleXChat struct MutableQRCode: View { @Binding var uri: String @@ -20,6 +21,16 @@ struct MutableQRCode: View { } } +struct SimpleXCreatedLinkQRCode: View { + let link: CreatedConnLink + @Binding var short: Bool + var onShare: (() -> Void)? = nil + + var body: some View { + QRCode(uri: link.simplexChatUri(short: short), onShare: onShare) + } +} + struct SimpleXLinkQRCode: View { let uri: String var withLogo: Bool = true @@ -31,12 +42,6 @@ struct SimpleXLinkQRCode: View { } } -func simplexChatLink(_ uri: String) -> String { - uri.starts(with: "simplex:/") - ? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") - : uri -} - struct QRCode: View { let uri: String var withLogo: Bool = true diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 24379cf9e3..656cef4a04 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -43,26 +43,23 @@ 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 @@ -70,83 +67,47 @@ struct ChooseServerOperators: View { 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,131 +119,28 @@ struct ChooseServerOperators: View { } .sheet(item: $sheetItem) { item in switch item { - case .showInfo: - ChooseServerOperatorsInfoView() case .showConditions: - UsageConditionsView( - currUserServers: Binding.constant([]), - userServers: Binding.constant([]), - updated: false - ) - .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 { @@ -305,25 +163,6 @@ 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 { @@ -357,9 +196,10 @@ struct ChooseServerOperators: View { } } } label: { - Text("Accept conditions") + Text("Accept") } - .buttonStyle(OnboardingButtonStyle()) + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) } } @@ -394,6 +234,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 { @@ -448,5 +408,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..ae72cb1be5 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -62,8 +62,7 @@ struct CreateProfile: View { .frame(height: 20) } footer: { VStack(alignment: .leading, spacing: 8) { - Text("Your profile, contacts and delivered messages are stored on your device.") - Text("The profile is only shared with your contacts.") + Text("Your profile is stored on your device and only shared with your contacts.") } .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) @@ -118,25 +117,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 +141,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 +170,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 +214,7 @@ struct CreateFirstProfile: View { } private func nextStepDestinationView() -> some View { - ChooseServerOperators(onboarding: true) + OnboardingConditionsView() .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) } @@ -235,15 +243,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..a2f5db7f03 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View { Spacer() if let userAddress = m.userAddress { - SimpleXLinkQRCode(uri: userAddress.connReqContact) + SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: Binding.constant(false)) .frame(maxHeight: g.size.width) shareQRCodeButton(userAddress) .frame(maxWidth: .infinity) @@ -77,9 +77,9 @@ struct CreateSimpleXAddress: View { progressIndicator = true Task { do { - let connReqContact = try await apiCreateUserAddress() + let connLinkContact = try await apiCreateUserAddress(short: false) DispatchQueue.main.async { - m.userAddress = UserContactLink(connReqContact: connReqContact) + m.userAddress = UserContactLink(connLinkContact: connLinkContact) } await MainActor.run { progressIndicator = false } } catch let error { @@ -121,7 +121,7 @@ struct CreateSimpleXAddress: View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { Button { - showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) + showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false))]) } label: { Label("Share", systemImage: "square.and.arrow.up") } @@ -189,7 +189,7 @@ struct SendAddressMailView: View { let messageBody = String(format: NSLocalizedString("""

Hi!

Connect to me via SimpleX Chat

- """, comment: "email text"), simplexChatLink(userAddress.connReqContact)) + """, comment: "email text"), simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false))) MailView( isShowing: self.$showMailView, result: $mailViewResult, diff --git a/apps/ios/Shared/Views/Onboarding/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 4547c6d20a..f65a21623a 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -542,7 +542,7 @@ private let versionDescriptions: [VersionDescription] = [ ), VersionDescription( version: "v6.3", - // post: URL(string: "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html"), + 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", @@ -594,8 +594,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) @@ -606,16 +604,7 @@ fileprivate struct NewOperatorsView: View { .multilineTextAlignment(.leading) .lineLimit(10) HStack { - Button("Enable Flux") { - showOperatorsSheet = true - } - Text("for better metadata privacy.") - } - } - .sheet(isPresented: $showOperatorsSheet) { - NavigationView { - ChooseServerOperators(onboarding: false) - .modifier(ThemedBackground()) + Text("Enable Flux in Network & servers settings for better metadata privacy.") } } } @@ -647,8 +636,7 @@ struct WhatsNewView: View { case .showConditions: UsageConditionsView( currUserServers: Binding.constant([]), - userServers: Binding.constant([]), - updated: true + userServers: Binding.constant([]) ) .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 2b58abef65..554219eb69 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -145,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 } } } } @@ -164,12 +164,38 @@ struct TerminalView: View { } } +func sendTerminalCmd(_ cmd: String) async { + let start: Date = .now + await withCheckedContinuation { (cont: CheckedContinuation) in + let d = sendSimpleXCmdStr(cmd) + Task { + guard let d else { + await TerminalItems.shared.addCommand(start, ChatCommand.string(cmd), APIResult.error(.invalidJSON(json: nil))) + return + } + let r0: APIResult = decodeAPIResult(d) + guard case .invalid = r0 else { + await TerminalItems.shared.addCommand(start, .string(cmd), r0) + return + } + let r1: APIResult = decodeAPIResult(d) + guard case .invalid = r1 else { + await TerminalItems.shared.addCommand(start, .string(cmd), r1) + return + } + let r2: APIResult = decodeAPIResult(d) + await TerminalItems.shared.addCommand(start, .string(cmd), r2) + } + cont.resume(returning: ()) + } +} + struct TerminalView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.terminalItems = [ - .resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")), - .resp(.now, ChatResponse.response(type: "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/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 7570b1c3e0..fa698f8b7c 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift @@ -209,11 +209,16 @@ struct AdvancedNetworkSettings: View { } Section { - Toggle("Use web port", isOn: $netCfg.smpWebPort) + Picker("Use web port", selection: $netCfg.smpWebPortServers) { + ForEach(SMPWebPortServers.allCases, id: \.self) { Text($0.text) } + } + .frame(height: 36) } header: { Text("TCP port for messaging") } footer: { - Text("Use TCP port \(netCfg.smpWebPort ? "443" : "5223") when no port is specified.") + 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") { @@ -368,8 +373,8 @@ 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.") } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 7d8424a67d..6f4710396a 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -20,11 +20,11 @@ private enum NetworkAlert: Identifiable { } private enum NetworkAndServersSheet: Identifiable { - case showConditions(updated: Bool) + case showConditions var id: String { switch self { - case let .showConditions(updated): return "showConditions \(updated)" + case .showConditions: return "showConditions" } } } @@ -169,11 +169,10 @@ struct NetworkAndServers: View { } .sheet(item: $sheetItem) { item in switch item { - case let .showConditions(updated): + case .showConditions: UsageConditionsView( currUserServers: $ss.servers.currUserServers, - userServers: $ss.servers.userServers, - updated: updated + userServers: $ss.servers.userServers ) .modifier(ThemedBackground(grouped: true)) } @@ -219,8 +218,7 @@ struct NetworkAndServers: View { private func conditionsButton(_ conditionsAction: UsageConditionsAction) -> some View { Button { - let updated = if case .review = conditionsAction { true } else { false } - sheetItem = .showConditions(updated: updated) + sheetItem = .showConditions } label: { switch conditionsAction { case .review: @@ -237,30 +235,26 @@ struct UsageConditionsView: View { @EnvironmentObject var theme: AppTheme @Binding var currUserServers: [UserOperatorServers] @Binding var userServers: [UserOperatorServers] - var updated: Bool var body: some View { VStack(alignment: .leading, spacing: 20) { - HStack { - if updated { - Text("Updated conditions").font(.largeTitle).bold() - } else { - 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) { @@ -272,10 +266,8 @@ struct UsageConditionsView: View { .multilineTextAlignment(.center) .frame(maxWidth: .infinity, alignment: .center) .padding(.horizontal, 32) - if updated { - conditionsDiffButton(.footnote) - } - } else if updated { + conditionsDiffButton(.footnote) + } else { conditionsDiffButton() .padding(.top) } @@ -285,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) @@ -340,6 +335,30 @@ struct UsageConditionsView: View { } } +private func regularConditionsHeader() -> some View { + HStack { + Text("Conditions of use").font(.largeTitle).bold() + Spacer() + conditionsLinkButton() + } +} + +struct SimpleConditionsView: View { + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + regularConditionsHeader() + .padding(.top) + .padding(.top) + ConditionsTextView() + .padding(.bottom) + .padding(.bottom) + } + .padding(.horizontal, 25) + .frame(maxHeight: .infinity) + } +} + func validateServers_(_ userServers: Binding<[UserOperatorServers]>, _ serverErrors: Binding<[UserServersError]>) { let userServersToValidate = userServers.wrappedValue Task { 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/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/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 0b9d1ef76c..eba7f8066a 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -14,12 +14,13 @@ 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(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @AppStorage(DEFAULT_PRIVACY_SHORT_LINKS) private var shortSimplexLinks = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @State private var currentLAMode = privacyLocalAuthModeDefault.get() @@ -75,17 +76,6 @@ struct PrivacySettings: View { privacyLinkPreviewsGroupDefault.set(linkPreviews) } } - 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("message", color: theme.colors.secondary) { Toggle("Show last messages", isOn: $showChatPreviews) } @@ -111,6 +101,11 @@ struct PrivacySettings: View { .onChange(of: simplexLinkMode) { mode in privacySimplexLinkModeDefault.set(mode) } + if developerTools { + settingsRow("link.badge.plus", color: theme.colors.secondary) { + Toggle("Use short links (BETA)", isOn: $shortSimplexLinks) + } + } } header: { Text("Chats") .foregroundColor(theme.colors.secondary) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 61dbb5d5d7..e06b1c4dd3 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -29,10 +29,10 @@ 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" +let DEFAULT_PRIVACY_SHORT_LINKS = "privacyShortLinks" let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen" let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet" let DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS = "privacyMediaBlurRadius" @@ -99,6 +99,7 @@ let appDefaults: [String: Any] = [ DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue, DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true, DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true, + DEFAULT_PRIVACY_SHORT_LINKS: false, DEFAULT_PRIVACY_PROTECT_SCREEN: false, DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false, DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS: 0, @@ -183,8 +184,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) @@ -281,159 +280,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 { @@ -528,7 +527,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..4813edf96c 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,6 +16,7 @@ struct UserAddressView: View { @EnvironmentObject var theme: AppTheme @State var shareViaProfile = false @State var autoCreate = false + @State private var showShortLink = true @State private var aas = AutoAcceptState() @State private var savedAAS = AutoAcceptState() @State private var showMailView = false @@ -135,8 +136,8 @@ struct UserAddressView: View { @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { Section { - SimpleXLinkQRCode(uri: userAddress.connReqContact) - .id("simplex-contact-address-qrcode-\(userAddress.connReqContact)") + SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: $showShortLink) + .id("simplex-contact-address-qrcode-\(userAddress.connLinkContact.simplexChatUri(short: showShortLink))") shareQRCodeButton(userAddress) // if MFMailComposeViewController.canSendMail() { // shareViaEmailButton(userAddress) @@ -153,8 +154,7 @@ struct UserAddressView: View { } 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 { Text("Add your team members to the conversations.") @@ -193,9 +193,10 @@ struct UserAddressView: View { progressIndicator = true Task { do { - let connReqContact = try await apiCreateUserAddress() + let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS) + let connLinkContact = try await apiCreateUserAddress(short: short) DispatchQueue.main.async { - chatModel.userAddress = UserContactLink(connReqContact: connReqContact) + chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact) alert = .shareOnCreate progressIndicator = false } @@ -231,7 +232,7 @@ struct UserAddressView: View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { Button { - showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) + showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: showShortLink))]) } label: { settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Text("Share address") @@ -294,6 +295,28 @@ struct UserAddressView: View { } } +struct ToggleShortLinkHeader: View { + @EnvironmentObject var theme: AppTheme + let text: Text + var link: CreatedConnLink + @Binding var short: Bool + + var body: some View { + if link.connShortLink == nil { + text.foregroundColor(theme.colors.secondary) + } else { + HStack { + text.foregroundColor(theme.colors.secondary) + Spacer() + Text(short ? "Full link" : "Short link") + .textCase(.none) + .foregroundColor(theme.colors.primary) + .onTapGesture { short.toggle() } + } + } + } +} + private struct AutoAcceptState: Equatable { var enable = false var incognito = false @@ -542,7 +565,7 @@ private func saveAAS(_ aas: Binding, _ savedAAS: Binding 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) 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 ca61f88520..e965e5a1a5 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -553,8 +553,9 @@ يمكنك أنت وجهة اتصالك إرسال رسائل صوتية. 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. @@ -582,8 +583,9 @@ إلغاء No comment provided by engineer. - + Cannot access keychain to save database password + لا يمكن الوصول إلى سلسلة المفاتيح لحفظ كلمة مرور قاعدة البيانات No comment provided by engineer. @@ -601,8 +603,9 @@ تغيير عبارة مرور قاعدة البيانات؟ No comment provided by engineer. - + Change member role? + تغيير دور العضو؟ No comment provided by engineer. @@ -664,56 +667,68 @@ الدردشات 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 @@ -724,8 +739,9 @@ Connect via group link? No comment provided by engineer. - + Connect via link + تواصل عبر الرابط No comment provided by engineer. @@ -740,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. @@ -968,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 @@ -1124,124 +1225,153 @@ Display name: No comment provided by engineer. - + Do NOT use SimpleX for emergency calls. + لا تستخدم SimpleX لإجراء مكالمات الطوارئ. No comment provided by engineer. - + Do it later + افعل ذلك لاحقا No comment provided by engineer. - + Duplicate display name! + اسم العرض مكرر! No comment provided by engineer. - + Edit + تحرير chat item action - + Edit group profile + حرّر ملف تعريف المجموعة No comment provided by engineer. - + Enable + تفعيل No comment provided by engineer. - + Enable SimpleX Lock + تفعيل قفل SimpleX authentication reason - + Enable TCP keep-alive + تفعيل أبقِ TCP على قيد الحياة No comment provided by engineer. - + Enable automatic message deletion? + تفعيل الحذف التلقائي للرسائل؟ No comment provided by engineer. - + Enable instant notifications? + تفعيل الإشعارات فورية؟ No comment provided by engineer. - + Enable notifications + تفعيل الإشعارات No comment provided by engineer. - + Enable periodic notifications? + تفعيل الإشعارات دورية؟ No comment provided by engineer. - + Encrypt + التشفير No comment provided by engineer. - + Encrypt database? + تشفير قاعدة البيانات؟ No comment provided by engineer. - + Encrypted database + قاعدة بيانات مشفرة No comment provided by engineer. - + Encrypted message or another event + رسالة مشفرة أو حدث آخر notification - + Encrypted message: database error + رسالة مشفرة: خطأ في قاعدة البيانات notification - + Encrypted message: keychain error + رسالة مشفرة: خطأ في سلسلة المفاتيح notification - + Encrypted message: no passphrase + الرسالة المشفرة: لا توجد عبارة مرور notification - + Encrypted message: unexpected error + رسالة مشفرة: خطأ غير متوقع notification - + Enter correct passphrase. + أدخل عبارة المرور الصحيحة. No comment provided by engineer. - + Enter passphrase… + أدخل عبارة المرور… No comment provided by engineer. - + Enter server manually + أدخل الخادم يدوياً No comment provided by engineer. - + Error + خطأ No comment provided by engineer. - + Error accepting contact request + خطأ في قبول طلب الاتصال No comment provided by engineer. Error accessing database file No comment provided by engineer. - + Error adding member(s) + خطأ في إضافة عضو (أعضاء) No comment provided by engineer. - + Error changing address + خطأ في تغيير العنوان No comment provided by engineer. - + Error changing role + خطأ في تغيير الدور المتغير No comment provided by engineer. - + Error changing setting + خطأ في تغيير الإعدادات No comment provided by engineer. @@ -2696,8 +2826,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. @@ -3597,72 +3727,87 @@ SimpleX servers cannot see your profile. secret No comment provided by engineer. - + starting… + يبدأ… No comment provided by engineer. - + strike + شطب No comment provided by engineer. this contact notification title - + unknown + غير معروف connection info - + updated group profile + حدثت ملف تعريف المجموعة rcv group event chat item v%@ (%@) No comment provided by engineer. - + via contact address link + عبر رابط عنوان الاتصال chat list item description - + via group link + عبر رابط المجموعة chat list item description - + via one-time link + عبر رابط لمرة واحدة chat list item description - + via relay + عبر المُرحل No comment provided by engineer. - + video call (not e2e encrypted) + مكالمة الفيديو ليست مُعمّاة بين الطريفين No comment provided by engineer. - + waiting for answer… + بانتظار الرد… No comment provided by engineer. - + waiting for confirmation… + في انتظار التأكيد… No comment provided by engineer. - + wants to connect to you! + يريد الاتصال بك! No comment provided by engineer. - + yes + نعم pref value - + you are invited to group + أنت مدعو إلى المجموعة No comment provided by engineer. - + you changed address + غيّرتَ العنوان chat item text @@ -3677,16 +3822,18 @@ SimpleX servers cannot see your profile. you changed role of %1$@ to %2$@ snd group event chat item - + you left + غادرت snd group event chat item you removed %@ snd group event chat item - + you shared one-time link + لقد شاركت رابط لمرة واحدة chat list item description @@ -4744,6 +4891,872 @@ SimpleX servers cannot see your profile. 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. + فشل إعادة التفاوض على التشفير. + @@ -4798,6 +5811,74 @@ 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 3e6e1ea387..776199ac1f 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (може да се копира) @@ -323,11 +307,6 @@ %u пропуснати съобщения. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (ново) @@ -338,11 +317,6 @@ (това устройство v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - **Create 1-time link**: to create and share a new invitation link. **Добави контакт**: за създаване на нов линк. @@ -407,11 +381,6 @@ \*удебелен* No comment provided by engineer. - - , - , - No comment provided by engineer. - - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -448,11 +417,6 @@ - история на редактиране. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -467,7 +431,7 @@ 1 day 1 ден delete after time - time interval +time interval 1 hour @@ -483,13 +447,13 @@ 1 month 1 месец delete after time - time interval +time interval 1 week 1 седмица delete after time - time interval +time interval 1 year @@ -519,11 +483,6 @@ 30 секунди No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -531,14 +490,6 @@ <p><a href="%@">Свържете се с мен чрез SimpleX Chat</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things Още няколко неща @@ -600,8 +551,8 @@ Accept Приеми accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -622,7 +573,7 @@ Accept incognito Приеми инкогнито accept contact request via notification - swipe action +swipe action Accepted conditions @@ -780,8 +731,8 @@ Всички чатове и съобщения ще бъдат изтрити - това не може да бъде отменено! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. alert message @@ -827,6 +778,10 @@ 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. Всички ваши контакти ще останат свързани. @@ -1108,10 +1063,6 @@ Архивиране на база данни No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach Прикачи @@ -1349,6 +1300,12 @@ Чрез чат профил (по подразбиране) или [чрез връзка](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! Разговорът вече приключи! @@ -1397,7 +1354,7 @@ Cancel Отказ alert action - alert button +alert button Cancel migration @@ -1486,7 +1443,7 @@ Change self-destruct passcode Промени кода за достъп за самоунищожение authentication reason - set passcode view +set passcode view Chat @@ -1703,14 +1660,6 @@ 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 will be accepted for the operator(s): **%@**. No comment provided by engineer. @@ -1728,6 +1677,10 @@ Конфигурирай ICE сървъри No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Потвърди @@ -2293,7 +2246,7 @@ This is your own one-time link! Delete Изтрий alert action - swipe action +swipe action Delete %lld messages of members? @@ -2760,7 +2713,7 @@ This is your own one-time link! Download Изтегли alert button - chat item action +chat item action Download errors @@ -2832,8 +2785,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. @@ -3386,7 +3339,9 @@ This is your own one-time link! Error: %@ Грешка: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3495,9 +3450,9 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5305,7 +5260,7 @@ Requires compatible VPN. Open Отвори - No comment provided by engineer. + alert action Open Settings @@ -5335,23 +5290,15 @@ Requires compatible VPN. Отвори група No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device Отвори миграцията към друго устройство authentication reason - - Open web link? - No comment provided by engineer. - Opening app… Приложението се отваря… @@ -5638,11 +5585,19 @@ 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 Поверителни имена на файлове @@ -5983,7 +5938,7 @@ Enable in *Network & servers* settings. Reject Отхвърляне reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6106,8 +6061,8 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -6202,10 +6157,6 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later - No comment provided by engineer. - Revoke Отзови @@ -6252,7 +6203,7 @@ Enable in *Network & servers* settings. Save Запази alert button - chat item action +chat item action Save (and notify contacts) @@ -6785,7 +6736,7 @@ Enable in *Network & servers* settings. Share Сподели alert action - chat item action +chat item action Share 1-time link @@ -6841,6 +6792,10 @@ Enable in *Network & servers* settings. Сподели с контактите No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code Покажи QR код @@ -6934,6 +6889,10 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX адрес за контакт @@ -7031,7 +6990,7 @@ Enable in *Network & servers* settings. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7370,19 +7329,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. @@ -7492,6 +7442,10 @@ It can happen because of some bug or when the connection is compromised.Това е вашят еднократен линк за връзка! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. @@ -7774,6 +7728,10 @@ To connect, please ask your contact to create another connection link and check Непрочетено swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. На новите членове се изпращат до последните 100 съобщения. @@ -7861,6 +7819,10 @@ To connect, please ask your contact to create another connection link and check 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 Използвай чата @@ -7921,6 +7883,10 @@ To connect, please ask your contact to create another connection link and check Use servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Използвайте приложението по време на разговора. @@ -8202,10 +8168,6 @@ To connect, please ask your contact to create another connection link and check XFTP server No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. **Не трябва** да използвате една и съща база данни на две устройства. @@ -8295,10 +8257,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. @@ -8532,11 +8490,6 @@ Repeat connection request? Вашите ICE сървъри No comment provided by engineer. - - Your SMP servers - Вашите SMP сървъри - No comment provided by engineer. - Your SimpleX address Вашият адрес в SimpleX @@ -8619,6 +8572,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 сървърите не могат да видят вашия профил. @@ -8628,11 +8586,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 Вашият автоматично генериран профил @@ -8767,7 +8720,8 @@ Repeat connection request? blocked by admin блокиран от админ - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8936,7 +8890,7 @@ Repeat connection request? default (%@) по подразбиране (%@) delete after time - pref value +pref value default (no) @@ -9062,19 +9016,10 @@ 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 препратено @@ -9280,8 +9225,8 @@ Repeat connection request? off изключено enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9637,7 +9582,7 @@ last received msg: %2$@
- +
@@ -9674,7 +9619,7 @@ last received msg: %2$@
- +
@@ -9696,13 +9641,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9715,15 +9664,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9742,7 +9687,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..bf7753675e 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -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. 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 54dc3734c3..0400839cb0 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (lze kopírovat) @@ -161,18 +145,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 @@ -321,11 +309,6 @@ %u zpráv přeskočeno. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) No comment provided by engineer. @@ -335,11 +318,6 @@ (toto zařízení 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. @@ -400,11 +378,6 @@ \*tučně* No comment provided by engineer. - - , - , - No comment provided by engineer. - - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -438,11 +411,6 @@ - historie úprav. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec time to disappear @@ -456,7 +424,7 @@ 1 day 1 den delete after time - time interval +time interval 1 hour @@ -472,13 +440,13 @@ 1 month 1 měsíc delete after time - time interval +time interval 1 week 1 týden delete after time - time interval +time interval 1 year @@ -507,11 +475,6 @@ 30 vteřin No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -519,14 +482,6 @@ <p><a href="%@"> Připojte se ke mne přes SimpleX Chat</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things Ještě pár věcí @@ -586,8 +541,8 @@ Accept Přijmout accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -607,7 +562,7 @@ Accept incognito Přijmout inkognito accept contact request via notification - swipe action +swipe action Accepted conditions @@ -749,8 +704,8 @@ 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 (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. alert message @@ -792,6 +747,10 @@ All reports will be archived for you. No comment provided by engineer. + + All servers + No comment provided by engineer. + All your contacts will remain connected. Všechny vaše kontakty zůstanou připojeny. @@ -1057,10 +1016,6 @@ Archiving database No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach Připojit @@ -1276,6 +1231,12 @@ 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! @@ -1320,7 +1281,7 @@ Cancel Zrušit alert action - alert button +alert button Cancel migration @@ -1405,7 +1366,7 @@ Change self-destruct passcode Změnit sebedestrukční heslo authentication reason - set passcode view +set passcode view Chat @@ -1618,14 +1579,6 @@ 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 will be accepted for the operator(s): **%@**. No comment provided by engineer. @@ -1643,6 +1596,10 @@ Konfigurace serverů ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Potvrdit @@ -1985,6 +1942,7 @@ This is your own one-time link! Create profile + Vytvořte si profil No comment provided by engineer. @@ -2183,7 +2141,7 @@ This is your own one-time link! Delete Smazat alert action - swipe action +swipe action Delete %lld messages of members? @@ -2640,7 +2598,7 @@ This is your own one-time link! Download alert button - chat item action +chat item action Download errors @@ -2709,8 +2667,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. @@ -3246,7 +3204,9 @@ This is your own one-time link! Error: %@ Chyba: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3352,9 +3312,9 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5105,7 +5065,7 @@ Vyžaduje povolení sítě VPN. Open Otevřít - No comment provided by engineer. + alert action Open Settings @@ -5134,22 +5094,14 @@ Vyžaduje povolení sítě VPN. Open group No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device authentication reason - - Open web link? - No comment provided by engineer. - Opening app… No comment provided by engineer. @@ -5422,11 +5374,19 @@ 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ů @@ -5759,7 +5719,7 @@ Enable in *Network & servers* settings. Reject Odmítnout reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -5877,8 +5837,8 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -5972,10 +5932,6 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later - No comment provided by engineer. - Revoke Odvolat @@ -6021,7 +5977,7 @@ Enable in *Network & servers* settings. Save Uložit alert button - chat item action +chat item action Save (and notify contacts) @@ -6544,7 +6500,7 @@ Enable in *Network & servers* settings. Share Sdílet alert action - chat item action +chat item action Share 1-time link @@ -6599,6 +6555,10 @@ Enable in *Network & servers* settings. Sdílet s kontakty No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -6691,6 +6651,10 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX kontaktní adresa @@ -6786,7 +6750,7 @@ Enable in *Network & servers* settings. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7117,19 +7081,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. @@ -7232,6 +7187,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován This is your own one-time link! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. @@ -7505,6 +7464,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu 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. @@ -7589,6 +7552,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu 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 @@ -7647,6 +7614,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. @@ -7910,10 +7881,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu XFTP server No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. No comment provided by engineer. @@ -7994,10 +7961,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. @@ -8223,11 +8186,6 @@ 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 @@ -8309,6 +8267,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. @@ -8318,11 +8281,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 @@ -8450,7 +8408,8 @@ Repeat connection request? blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8618,7 +8577,7 @@ Repeat connection request? default (%@) výchozí (%@) delete after time - pref value +pref value default (no) @@ -8743,18 +8702,10 @@ 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. @@ -8958,8 +8909,8 @@ Repeat connection request? off vypnuto enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9299,7 +9250,7 @@ last received msg: %2$@
- +
@@ -9335,7 +9286,7 @@ last received msg: %2$@
- +
@@ -9357,13 +9308,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9376,15 +9331,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9403,7 +9354,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 6674d1587b..06fd7c5a1d 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2,26 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (kann kopiert werden) @@ -332,11 +315,6 @@ %u übersprungene Nachrichten. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (Neu) @@ -347,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. @@ -417,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). @@ -458,11 +426,6 @@ - Nachrichtenverlauf bearbeiten No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sek @@ -477,7 +440,7 @@ 1 day Älter als ein Tag delete after time - time interval +time interval 1 hour @@ -493,13 +456,13 @@ 1 month Älter als ein Monat delete after time - time interval +time interval 1 week Älter als eine Woche delete after time - time interval +time interval 1 year @@ -531,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> @@ -543,16 +501,6 @@ <p><a href="%@">Verbinden Sie sich per SimpleX Chat mit mir</a></p> email text - - @%@ - @%@ - No comment provided by engineer. - - - @'%@' - @'%@' - No comment provided by engineer. - A few more things Ein paar weitere Dinge @@ -614,8 +562,8 @@ Accept Annehmen accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -636,7 +584,7 @@ Accept incognito Inkognito akzeptieren accept contact request via notification - swipe action +swipe action Accepted conditions @@ -798,9 +746,9 @@ 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 (text), and the list deleted. - Alle Chats werden von der Liste (text) entfernt und danach wird die Liste gelöscht. + + 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 @@ -848,6 +796,11 @@ 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. @@ -1138,11 +1091,6 @@ Datenbank wird archiviert No comment provided by engineer. - - Ask - Fragen - No comment provided by engineer. - Attach Anhängen @@ -1383,6 +1331,15 @@ 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! @@ -1432,7 +1389,7 @@ Cancel Abbrechen alert action - alert button +alert button Cancel migration @@ -1523,7 +1480,7 @@ Change self-destruct passcode Selbstzerstörungs-Zugangscode ändern authentication reason - set passcode view +set passcode view Chat @@ -1765,16 +1722,6 @@ 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: **%@**. @@ -1795,6 +1742,11 @@ ICE-Server konfigurieren No comment provided by engineer. + + Configure server operators + Server-Betreiber konfigurieren + No comment provided by engineer. + Confirm Bestätigen @@ -2243,7 +2195,7 @@ Das ist Ihr eigener Einmal-Link! Current profile - Aktueller Profil + Aktuelles Profil No comment provided by engineer. @@ -2393,7 +2345,7 @@ Das ist Ihr eigener Einmal-Link! Delete Löschen alert action - swipe action +swipe action Delete %lld messages of members? @@ -2889,7 +2841,7 @@ Das ist Ihr eigener Einmal-Link! Download Herunterladen alert button - chat item action +chat item action Download errors @@ -2966,9 +2918,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. @@ -3373,7 +3325,7 @@ Das ist Ihr eigener Einmal-Link! Error receiving file - Fehler beim Empfangen der Datei + Fehler beim Herunterladen der Datei alert title @@ -3544,7 +3496,9 @@ Das ist Ihr eigener Einmal-Link! Error: %@ Fehler: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3663,11 +3617,11 @@ Das ist Ihr eigener Einmal-Link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. Datei wurde vom Server-Betreiber blockiert: -(info.reason.text). +%@. file error text @@ -3697,12 +3651,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. @@ -4191,12 +4145,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. @@ -4988,12 +4942,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. @@ -5358,7 +5312,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. @@ -5368,7 +5322,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 @@ -5587,7 +5541,7 @@ Dies erfordert die Aktivierung eines VPNs. Open Öffnen - No comment provided by engineer. + alert action Open Settings @@ -5619,26 +5573,15 @@ Dies erfordert die Aktivierung eines VPNs. Gruppe öffnen No comment provided by engineer. - - Open link - Web-Link öffnen - No comment provided by engineer. - - - Open links from chat list - Web-Links aus der Chat-Liste öffnen - No comment provided by engineer. + + Open link? + alert title Open migration to another device Migration auf ein anderes Gerät öffnen authentication reason - - Open web link? - Web-Link öffnen? - No comment provided by engineer. - Opening app… App wird geöffnet… @@ -5945,11 +5888,21 @@ 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 @@ -6224,7 +6177,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. @@ -6316,7 +6269,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Reject Ablehnen reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6448,9 +6401,9 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Verstoß melden: Nur Gruppenmoderatoren werden es sehen. report reason - - Report: (text.isEmpty ? reason.text : text) - Meldung: (text.isEmpty ? reason.text : text) + + Report: %@ + Meldung: %@ report in notification @@ -6553,11 +6506,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Nutzungsbedingungen einsehen No comment provided by engineer. - - Review later - Später einsehen - No comment provided by engineer. - Revoke Widerrufen @@ -6595,7 +6543,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Safely receive files - Dateien sicher empfangen + Dateien sicher herunterladen No comment provided by engineer. @@ -6607,7 +6555,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Save Speichern alert button - chat item action +chat item action Save (and notify contacts) @@ -7178,7 +7126,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Share Teilen alert action - chat item action +chat item action Share 1-time link @@ -7240,6 +7188,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Mit Kontakten teilen No comment provided by engineer. + + Short link + Verkürzter Link + No comment provided by engineer. + Show QR code QR-Code anzeigen @@ -7340,6 +7293,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. SimpleX-Adresse oder Einmal-Link? No comment provided by engineer. + + SimpleX channel link + SimpleX-Kanal-Link + simplex link type + SimpleX contact address SimpleX-Kontaktadressen-Link @@ -7446,7 +7404,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Spam Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7505,17 +7463,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. @@ -7805,21 +7763,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! @@ -7877,7 +7825,7 @@ 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. - Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden! + Es werden alle herunter- und hochgeladenen Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -7935,6 +7883,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Das ist Ihr eigener Einmal-Link! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App. @@ -8234,6 +8187,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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. @@ -8261,6 +8219,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Updated conditions + Aktualisierte Nutzungsbedingungen No comment provided by engineer. @@ -8328,6 +8287,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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 @@ -8393,6 +8357,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Verwende Server No comment provided by engineer. + + Use short links (BETA) + Kurze Links verwenden (BETA) + No comment provided by engineer. + Use the app while in the call. Die App kann während eines Anrufs genutzt werden. @@ -8475,12 +8444,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Video will be received when your contact completes uploading it. - Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat. + Das Video wird heruntergeladen, sobald Ihr Kontakt das Hochladen beendet hat. No comment provided by engineer. Video will be received when your contact is online, please wait or check later! - Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später! + Das Video wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später! No comment provided by engineer. @@ -8688,11 +8657,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s XFTP-Server No comment provided by engineer. - - Yes - Ja - No comment provided by engineer. - You **must not** use the same database on two devices. Sie dürfen die selbe Datenbank **nicht** auf zwei Geräten nutzen. @@ -8785,11 +8749,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. @@ -9032,11 +8991,6 @@ 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 @@ -9122,6 +9076,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. + Das Profil wird 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. @@ -9132,11 +9091,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 @@ -9275,7 +9229,8 @@ Verbindungsanfrage wiederholen? blocked by admin wurde vom Administrator blockiert - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -9446,7 +9401,7 @@ Verbindungsanfrage wiederholen? default (%@) Default (%@) delete after time - pref value +pref value default (no) @@ -9573,21 +9528,11 @@ 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 @@ -9797,8 +9742,8 @@ Verbindungsanfrage wiederholen? off Aus enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9842,10 +9787,12 @@ Verbindungsanfrage wiederholen? pending + ausstehend No comment provided by engineer. pending approval + ausstehende Genehmigung No comment provided by engineer. @@ -9865,6 +9812,7 @@ Verbindungsanfrage wiederholen? rejected + abgelehnt No comment provided by engineer. @@ -10165,7 +10113,7 @@ Zuletzt empfangene Nachricht: %2$@
- +
@@ -10195,14 +10143,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
- +
@@ -10224,7 +10172,7 @@ Zuletzt empfangene Nachricht: %2$@
- +
@@ -10232,6 +10180,11 @@ Zuletzt empfangene Nachricht: %2$@ %d neue Ereignisse notification body + + From %d chat(s) + Von %d Chat(s) + notification body + From: %@ Von: %@ @@ -10247,16 +10200,11 @@ Zuletzt empfangene Nachricht: %2$@ Neue Nachrichten notification - - New messages in %d chats - Neue Nachrichten in %d Chats - notification body -
- +
@@ -10278,7 +10226,7 @@ Zuletzt empfangene Nachricht: %2$@
- +
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..fc1846942c 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -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. 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 39d229c5fa..fd71e0dee6 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2,26 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (can be copied) @@ -332,11 +315,6 @@ %u messages skipped. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (new) @@ -347,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. @@ -417,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). @@ -458,11 +426,6 @@ - editing history. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -477,7 +440,7 @@ 1 day 1 day delete after time - time interval +time interval 1 hour @@ -493,13 +456,13 @@ 1 month 1 month delete after time - time interval +time interval 1 week 1 week delete after time - time interval +time interval 1 year @@ -531,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> @@ -543,16 +501,6 @@ <p><a href="%@">Connect to me via SimpleX Chat</a></p> email text - - @%@ - @%@ - No comment provided by engineer. - - - @'%@' - @'%@' - No comment provided by engineer. - A few more things A few more things @@ -614,8 +562,8 @@ Accept Accept accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -636,7 +584,7 @@ Accept incognito Accept incognito accept contact request via notification - swipe action +swipe action Accepted conditions @@ -798,9 +746,9 @@ All chats and messages will be deleted - this cannot be undone! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. - All chats will be removed from the list (text), and the list deleted. + + 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 @@ -848,6 +796,11 @@ 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. @@ -1138,11 +1091,6 @@ Archiving database No comment provided by engineer. - - Ask - Ask - No comment provided by engineer. - Attach Attach @@ -1383,6 +1331,15 @@ 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! @@ -1432,7 +1389,7 @@ Cancel Cancel alert action - alert button +alert button Cancel migration @@ -1523,7 +1480,7 @@ Change self-destruct passcode Change self-destruct passcode authentication reason - set passcode view +set passcode view Chat @@ -1765,16 +1722,6 @@ 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. - Conditions will be accepted for the operator(s): **%@**. Conditions will be accepted for the operator(s): **%@**. @@ -1795,6 +1742,11 @@ Configure ICE servers No comment provided by engineer. + + Configure server operators + Configure server operators + No comment provided by engineer. + Confirm Confirm @@ -2393,7 +2345,7 @@ This is your own one-time link! Delete Delete alert action - swipe action +swipe action Delete %lld messages of members? @@ -2889,7 +2841,7 @@ This is your own one-time link! Download Download alert button - chat item action +chat item action Download errors @@ -2966,9 +2918,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. @@ -3544,7 +3496,9 @@ This is your own one-time link! Error: %@ Error: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3663,11 +3617,11 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5587,7 +5541,7 @@ Requires compatible VPN. Open Open - No comment provided by engineer. + alert action Open Settings @@ -5619,26 +5573,16 @@ Requires compatible VPN. Open group No comment provided by engineer. - - Open link - Open link - No comment provided by engineer. - - - Open links from chat list - Open links from chat list - No comment provided by engineer. + + Open link? + Open link? + alert title Open migration to another device Open migration to another device authentication reason - - Open web link? - Open web link? - No comment provided by engineer. - Opening app… Opening app… @@ -5945,11 +5889,21 @@ 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 @@ -6316,7 +6270,7 @@ Enable in *Network & servers* settings. Reject Reject reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6448,9 +6402,9 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) - Report: (text.isEmpty ? reason.text : text) + + Report: %@ + Report: %@ report in notification @@ -6553,11 +6507,6 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later - Review later - No comment provided by engineer. - Revoke Revoke @@ -6607,7 +6556,7 @@ Enable in *Network & servers* settings. Save Save alert button - chat item action +chat item action Save (and notify contacts) @@ -7178,7 +7127,7 @@ Enable in *Network & servers* settings. Share Share alert action - chat item action +chat item action Share 1-time link @@ -7240,6 +7189,11 @@ Enable in *Network & servers* settings. Share with contacts No comment provided by engineer. + + Short link + Short link + No comment provided by engineer. + Show QR code Show QR code @@ -7340,6 +7294,11 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX channel link + SimpleX channel link + simplex link type + SimpleX contact address SimpleX contact address @@ -7446,7 +7405,7 @@ Enable in *Network & servers* settings. Spam Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7805,21 +7764,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! @@ -7935,6 +7884,11 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. This link was used with another mobile device, please create a new link on the desktop. @@ -8234,6 +8188,11 @@ To connect, please ask your contact to create another connection link and check 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. @@ -8329,6 +8288,11 @@ To connect, please ask your contact to create another connection link and check 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 @@ -8394,6 +8358,11 @@ To connect, please ask your contact to create another connection link and check Use servers No comment provided by engineer. + + Use short links (BETA) + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Use the app while in the call. @@ -8689,11 +8658,6 @@ To connect, please ask your contact to create another connection link and check XFTP server No comment provided by engineer. - - Yes - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. You **must not** use the same database on two devices. @@ -8786,11 +8750,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. @@ -9033,11 +8992,6 @@ 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 @@ -9123,6 +9077,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. @@ -9133,11 +9092,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 @@ -9276,7 +9230,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 @@ -9447,7 +9402,7 @@ Repeat connection request? default (%@) default (%@) delete after time - pref value +pref value default (no) @@ -9574,21 +9529,11 @@ 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 @@ -9798,8 +9743,8 @@ Repeat connection request? off off enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -10169,7 +10114,7 @@ last received msg: %2$@
- +
@@ -10206,7 +10151,7 @@ last received msg: %2$@
- +
@@ -10228,7 +10173,7 @@ last received msg: %2$@
- +
@@ -10236,6 +10181,11 @@ last received msg: %2$@ %d new events notification body + + From %d chat(s) + From %d chat(s) + notification body + From: %@ From: %@ @@ -10251,16 +10201,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - New messages in %d chats - notification body -
- +
@@ -10282,7 +10227,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 e9b4de3017..d39fb61249 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2,26 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (puede copiarse) @@ -332,11 +315,6 @@ %u mensaje(s) omitido(s). No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nuevo) @@ -347,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. @@ -379,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. @@ -417,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). @@ -458,11 +426,6 @@ - historial de edición. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 seg @@ -477,7 +440,7 @@ 1 day un dia delete after time - time interval +time interval 1 hour @@ -493,17 +456,17 @@ 1 month un mes delete after time - time interval +time interval 1 week una semana delete after time - time interval +time interval 1 year - Un año + 1 año delete after time @@ -531,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> @@ -543,16 +501,6 @@ <p><a href="%@"> Conecta conmigo a través de SimpleX Chat</a></p> email text - - @%@ - @%@ - No comment provided by engineer. - - - @'%@' - @'%@' - No comment provided by engineer. - A few more things Algunas cosas más @@ -614,8 +562,8 @@ Accept Aceptar accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -636,7 +584,7 @@ Accept incognito Aceptar incógnito accept contact request via notification - swipe action +swipe action Accepted conditions @@ -795,12 +743,12 @@ 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 (text), and the list deleted. - Todos los chats serán quitados de la lista (text) y esta será eliminada. + + 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 @@ -825,12 +773,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. @@ -848,6 +796,11 @@ 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. @@ -925,7 +878,7 @@ Allow to report messsages to moderators. - Se permite informar de mensajes a los moderadores. + Permitir informar de mensajes a los moderadores. No comment provided by engineer. @@ -1045,7 +998,7 @@ App icon - Icono aplicación + Icono de la aplicación No comment provided by engineer. @@ -1138,11 +1091,6 @@ Archivando base de datos No comment provided by engineer. - - Ask - Preguntar - No comment provided by engineer. - Attach Adjuntar @@ -1245,7 +1193,7 @@ Better groups performance - Mejor rendimiento de los grupos + Rendimiento de grupos mejorado No comment provided by engineer. @@ -1270,7 +1218,7 @@ Better privacy and security - Mejor privacidad y seguridad + Privacidad y seguridad mejoradas No comment provided by engineer. @@ -1383,6 +1331,15 @@ 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! @@ -1432,7 +1389,7 @@ Cancel Cancelar alert action - alert button +alert button Cancel migration @@ -1523,7 +1480,7 @@ Change self-destruct passcode Cambiar código autodestrucción authentication reason - set passcode view +set passcode view Chat @@ -1617,12 +1574,12 @@ 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. @@ -1765,16 +1722,6 @@ 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): **%@**. @@ -1795,6 +1742,11 @@ Configure servidores ICE No comment provided by engineer. + + Configure server operators + Configurar operadores de servidores + No comment provided by engineer. + Confirm Confirmar @@ -2078,7 +2030,7 @@ This is your own one-time link! 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. @@ -2393,7 +2345,7 @@ This is your own one-time link! Delete Eliminar alert action - swipe action +swipe action Delete %lld messages of members? @@ -2567,7 +2519,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. @@ -2587,7 +2539,7 @@ This is your own one-time link! Delete report - Borrar informe + Eliminar informe No comment provided by engineer. @@ -2889,7 +2841,7 @@ This is your own one-time link! Download Descargar alert button - chat item action +chat item action Download errors @@ -2966,9 +2918,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. @@ -3544,7 +3496,9 @@ This is your own one-time link! Error: %@ Error: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3663,16 +3617,16 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. Archivo bloqueado por el operador del servidor -(info.reason.text). +%@. 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 @@ -3946,7 +3900,7 @@ Error: %2$@ Get notified when mentioned. - Las menciones ahora notifican. + Las menciones ahora se notifican. No comment provided by engineer. @@ -4046,12 +4000,12 @@ 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. @@ -4066,7 +4020,7 @@ Error: %2$@ Help admins moderating their groups. - Ayuda a los admins moderar sus grupos. + Ayuda a los admins a moderar sus grupos. No comment provided by engineer. @@ -4783,7 +4737,7 @@ This is your link for group %@! Member reports - Informes de miembro + Informes de miembros chat feature @@ -4803,12 +4757,12 @@ 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. @@ -4853,7 +4807,7 @@ This is your link for group %@! Mention members 👋 - Mencionar miembros 👋 + Menciona a miembros 👋 No comment provided by engineer. @@ -4968,7 +4922,7 @@ This is your link for group %@! Messages in this chat will never be deleted. - Los mensajes de esta conversación nunca se borran. + Los mensajes de esta conversación nunca se eliminan. alert message @@ -4983,7 +4937,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 @@ -5587,7 +5541,7 @@ Requiere activación de la VPN. Open Abrir - No comment provided by engineer. + alert action Open Settings @@ -5619,26 +5573,15 @@ Requiere activación de la VPN. Grupo abierto No comment provided by engineer. - - Open link - Abrir enlace - No comment provided by engineer. - - - Open links from chat list - Abrir enlaces desde listado de chats - No comment provided by engineer. + + Open link? + alert title Open migration to another device Abrir menú migración a otro dispositivo authentication reason - - Open web link? - ¿Abrir enlace web? - No comment provided by engineer. - Opening app… Iniciando aplicación… @@ -5686,7 +5629,7 @@ Requiere activación de la VPN. Organize chats into lists - Organiza los chats en listas + Organiza tus chats en listas No comment provided by engineer. @@ -5945,11 +5888,21 @@ 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 @@ -5957,7 +5910,7 @@ Error: %@ Private media file names. - Nombres privados de archivos de media. + Nombres privados en archivos de media. No comment provided by engineer. @@ -6134,7 +6087,7 @@ Actívalo en ajustes de *Servidores y Redes*. Reachable chat toolbar - Barra de chat accesible + Barra de menú accesible No comment provided by engineer. @@ -6316,7 +6269,7 @@ Actívalo en ajustes de *Servidores y Redes*. Reject Rechazar reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6448,9 +6401,9 @@ Actívalo en ajustes de *Servidores y Redes*. Informar de violación: sólo los moderadores del grupo lo verán. report reason - - Report: (text.isEmpty ? reason.text : text) - Informe: (text.isEmpty ? reason.text : text) + + Report: %@ + Informe: %@ report in notification @@ -6553,11 +6506,6 @@ Actívalo en ajustes de *Servidores y Redes*. Revisar condiciones No comment provided by engineer. - - Review later - Revisar más tarde - No comment provided by engineer. - Revoke Revocar @@ -6607,7 +6555,7 @@ Actívalo en ajustes de *Servidores y Redes*. Save Guardar alert button - chat item action +chat item action Save (and notify contacts) @@ -7091,7 +7039,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. @@ -7131,7 +7079,7 @@ Actívalo en ajustes de *Servidores y Redes*. Set message expiration in chats. - Establece vencimiento de los mensajes en chats. + Establece el vencimiento para los mensajes en los chats. No comment provided by engineer. @@ -7178,7 +7126,7 @@ Actívalo en ajustes de *Servidores y Redes*. Share Compartir alert action - chat item action +chat item action Share 1-time link @@ -7240,6 +7188,11 @@ Actívalo en ajustes de *Servidores y Redes*. Compartir con contactos No comment provided by engineer. + + Short link + Enlace corto + No comment provided by engineer. + Show QR code Mostrar código QR @@ -7332,7 +7285,7 @@ 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. @@ -7340,6 +7293,11 @@ Actívalo en ajustes de *Servidores y Redes*. ¿Dirección SimpleX o enlace de un uso? No comment provided by engineer. + + SimpleX channel link + Enlace de canal SimpleX + simplex link type + SimpleX contact address Dirección de contacto SimpleX @@ -7446,7 +7404,7 @@ Actívalo en ajustes de *Servidores y Redes*. Spam Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7742,7 +7700,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. @@ -7805,21 +7763,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! @@ -7847,7 +7795,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. 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. @@ -7887,7 +7835,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. 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 podrá deshacerse! + Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No puede deshacerse! alert message @@ -7935,6 +7883,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. ¡Este es tu propio enlace de un solo uso! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Este enlace requiere una versión más reciente de la aplicación. Por favor, actualiza la aplicación o pide a tu contacto un enlace compatible. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador. @@ -7942,7 +7895,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This message was deleted or not received yet. - El mensaje ha sido borrado o aún no se ha recibido. + El mensaje ha sido eliminado o aún no se ha recibido. No comment provided by engineer. @@ -7999,7 +7952,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. @@ -8129,7 +8082,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.
Unblock member for all? - ¿Desbloquear miembro para todos? + ¿Desbloquear el miembro para todos? No comment provided by engineer. @@ -8234,6 +8187,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión 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. @@ -8261,6 +8219,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Updated conditions + Condiciones actualizadas No comment provided by engineer. @@ -8328,6 +8287,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión 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 @@ -8393,6 +8357,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar servidores No comment provided by engineer. + + Use short links (BETA) + Usar enlaces cortos (BETA) + No comment provided by engineer. + Use the app while in the call. Usar la aplicación durante la llamada. @@ -8688,11 +8657,6 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Servidor XFTP No comment provided by engineer. - - Yes - Si - No comment provided by engineer. - You **must not** use the same database on two devices. **No debes** usar la misma base de datos en dos dispositivos. @@ -8785,11 +8749,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. @@ -9032,11 +8991,6 @@ 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 @@ -9122,6 +9076,11 @@ 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. @@ -9132,11 +9091,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 @@ -9275,7 +9229,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 @@ -9446,7 +9401,7 @@ Repeat connection request? default (%@) predeterminado (%@) delete after time - pref value +pref value default (no) @@ -9573,21 +9528,11 @@ 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 @@ -9797,8 +9742,8 @@ Repeat connection request? off desactivado enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9842,10 +9787,12 @@ Repeat connection request? pending + pendiente No comment provided by engineer. pending approval + pendiente de aprobación No comment provided by engineer. @@ -9865,6 +9812,7 @@ Repeat connection request? rejected + rechazado No comment provided by engineer. @@ -10165,7 +10113,7 @@ last received msg: %2$@
- +
@@ -10202,7 +10150,7 @@ last received msg: %2$@
- +
@@ -10224,7 +10172,7 @@ last received msg: %2$@
- +
@@ -10232,6 +10180,11 @@ last received msg: %2$@ %d evento(s) nuevo(s) notification body + + From %d chat(s) + De %d chat(s) + notification body + From: %@ De: %@ @@ -10247,16 +10200,11 @@ last received msg: %2$@ Mensajes nuevos notification - - New messages in %d chats - Mensajes nuevos en %d chat(s) - notification body -
- +
@@ -10278,7 +10226,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 9d7c9c4bbb..a54666bb10 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (voidaan kopioida) @@ -312,11 +296,6 @@ %u viestit ohitettu. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) No comment provided by engineer. @@ -325,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. @@ -390,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). @@ -425,11 +394,6 @@ - historian muokkaaminen. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec time to disappear @@ -443,7 +407,7 @@ 1 day 1 päivä delete after time - time interval +time interval 1 hour @@ -459,13 +423,13 @@ 1 month 1 kuukausi delete after time - time interval +time interval 1 week 1 viikko delete after time - time interval +time interval 1 year @@ -494,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> @@ -506,14 +465,6 @@ <p> <a href="%@"> Ollaan yhteydessä SimpleX Chatin kautta</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things Muutama asia lisää @@ -573,8 +524,8 @@ Accept Hyväksy accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -594,7 +545,7 @@ Accept incognito Hyväksy tuntematon accept contact request via notification - swipe action +swipe action Accepted conditions @@ -736,8 +687,8 @@ Kaikki keskustelut ja viestit poistetaan - tätä ei voi kumota! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. alert message @@ -779,6 +730,10 @@ 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ä. @@ -1043,10 +998,6 @@ Archiving database No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach Liitä @@ -1261,6 +1212,12 @@ 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! @@ -1305,7 +1262,7 @@ Cancel Peruuta alert action - alert button +alert button Cancel migration @@ -1390,7 +1347,7 @@ Change self-destruct passcode Vaihda itsetuhoutuva pääsykoodi authentication reason - set passcode view +set passcode view Chat @@ -1603,14 +1560,6 @@ 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 will be accepted for the operator(s): **%@**. No comment provided by engineer. @@ -1628,6 +1577,10 @@ Määritä ICE-palvelimet No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Vahvista @@ -1970,6 +1923,7 @@ This is your own one-time link! Create profile + Luo profiilisi No comment provided by engineer. @@ -2168,7 +2122,7 @@ This is your own one-time link! Delete Poista alert action - swipe action +swipe action Delete %lld messages of members? @@ -2625,7 +2579,7 @@ This is your own one-time link! Download alert button - chat item action +chat item action Download errors @@ -2694,8 +2648,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. @@ -3228,7 +3182,9 @@ This is your own one-time link! Error: %@ Virhe: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3334,9 +3290,9 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5085,7 +5041,7 @@ Edellyttää VPN:n sallimista. Open - No comment provided by engineer. + alert action Open Settings @@ -5114,22 +5070,14 @@ Edellyttää VPN:n sallimista. Open group No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device authentication reason - - Open web link? - No comment provided by engineer. - Opening app… No comment provided by engineer. @@ -5402,11 +5350,19 @@ 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 @@ -5739,7 +5695,7 @@ Enable in *Network & servers* settings. Reject Hylkää reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -5857,8 +5813,8 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -5952,10 +5908,6 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later - No comment provided by engineer. - Revoke Peruuta @@ -6001,7 +5953,7 @@ Enable in *Network & servers* settings. Save Tallenna alert button - chat item action +chat item action Save (and notify contacts) @@ -6523,7 +6475,7 @@ Enable in *Network & servers* settings. Share Jaa alert action - chat item action +chat item action Share 1-time link @@ -6578,6 +6530,10 @@ Enable in *Network & servers* settings. Jaa kontaktien kanssa No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -6670,6 +6626,10 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX-yhteystiedot @@ -6764,7 +6724,7 @@ Enable in *Network & servers* settings. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7095,19 +7055,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. @@ -7210,6 +7161,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.This is your own one-time link! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. @@ -7482,6 +7437,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja 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. @@ -7566,6 +7525,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja 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 @@ -7624,6 +7587,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. @@ -7887,10 +7854,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja XFTP server No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. No comment provided by engineer. @@ -7971,10 +7934,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. @@ -8200,11 +8159,6 @@ 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 @@ -8286,6 +8240,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. @@ -8295,11 +8254,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 @@ -8427,7 +8381,8 @@ Repeat connection request? blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8594,7 +8549,7 @@ Repeat connection request? default (%@) oletusarvo (%@) delete after time - pref value +pref value default (no) @@ -8719,19 +8674,10 @@ 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. @@ -8935,8 +8881,8 @@ Repeat connection request? off pois enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9275,7 +9221,7 @@ last received msg: %2$@
- +
@@ -9311,7 +9257,7 @@ last received msg: %2$@
- +
@@ -9333,13 +9279,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9352,15 +9302,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9379,7 +9325,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 31eef4b3b1..59bde0650e 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (peut être copié) @@ -208,6 +192,7 @@ %d seconds(s) + %d seconde(s) delete after time @@ -330,11 +315,6 @@ %u messages sautés. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nouveau) @@ -345,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. @@ -415,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). @@ -456,11 +426,6 @@ - l'historique de modification. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -475,7 +440,7 @@ 1 day 1 jour delete after time - time interval +time interval 1 hour @@ -491,16 +456,17 @@ 1 month 1 mois delete after time - time interval +time interval 1 week 1 semaine delete after time - time interval +time interval 1 year + 1 an delete after time @@ -528,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> @@ -540,14 +501,6 @@ <p><a href="%@">Contactez-moi via SimpleX Chat</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things Encore quelques points @@ -609,8 +562,8 @@ Accept Accepter accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -631,7 +584,7 @@ Accept incognito Accepter en incognito accept contact request via notification - swipe action +swipe action Accepted conditions @@ -650,6 +603,7 @@ Active + Actif token status text @@ -669,6 +623,7 @@ Add list + Ajouter une liste No comment provided by engineer. @@ -698,6 +653,7 @@ Add to list + Ajouter à la liste No comment provided by engineer. @@ -777,6 +733,7 @@ All + Tout No comment provided by engineer. @@ -789,8 +746,9 @@ 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 (text), and the list deleted. + + 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 @@ -835,6 +793,11 @@ All reports will be archived for you. + Tous les rapports seront archivés pour vous. + No comment provided by engineer. + + + All servers No comment provided by engineer. @@ -914,6 +877,7 @@ Allow to report messsages to moderators. + Permettre de signaler des messages aux modérateurs. No comment provided by engineer. @@ -998,6 +962,7 @@ Another reason + Autre raison report reason @@ -1076,14 +1041,17 @@ 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. @@ -1098,14 +1066,17 @@ 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 @@ -1118,10 +1089,6 @@ Archivage de la base de données No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach Attacher @@ -1224,6 +1191,7 @@ Better groups performance + Meilleure performance des groupes No comment provided by engineer. @@ -1248,6 +1216,7 @@ Better privacy and security + Meilleure protection de la privacité et de la sécurité No comment provided by engineer. @@ -1352,6 +1321,7 @@ Businesses + Entreprises No comment provided by engineer. @@ -1359,6 +1329,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é ! @@ -1408,7 +1387,7 @@ Cancel Annuler alert action - alert button +alert button Cancel migration @@ -1447,6 +1426,7 @@ Change automatic message deletion? + Modifier la suppression automatique des messages ? alert title @@ -1498,7 +1478,7 @@ Change self-destruct passcode Modifier le code d'autodestruction authentication reason - set passcode view +set passcode view Chat @@ -1672,10 +1652,12 @@ Clear group? + Vider le groupe ? No comment provided by engineer. Clear or delete group? + Vider ou supprimer le groupe ? No comment provided by engineer. @@ -1700,6 +1682,7 @@ Community guidelines violation + Infraction aux règles communautaires report reason @@ -1737,16 +1720,6 @@ Conditions d'utilisation No comment provided by engineer. - - Conditions will be accepted for enabled operators after 30 days. - Les conditions seront acceptées pour les opérateurs activés après 30 jours. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Les conditions seront acceptées pour le(s) opérateur(s) : **%@**. - No comment provided by engineer. - Conditions will be accepted for the operator(s): **%@**. Les conditions seront acceptées pour le(s) opérateur(s) : **%@**. @@ -1767,6 +1740,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 @@ -1819,6 +1797,7 @@ Confirmed + Confirmé token status text @@ -1942,6 +1921,7 @@ Il s'agit de votre propre lien unique ! Connection blocked + Connexion bloquée No comment provided by engineer. @@ -1957,10 +1937,13 @@ Il s'agit de votre propre lien unique ! 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. @@ -1975,6 +1958,7 @@ Il s'agit de votre propre lien unique ! Connection requires encryption renegotiation. + La connexion nécessite une renégociation du cryptage. No comment provided by engineer. @@ -2059,6 +2043,7 @@ Il s'agit de votre propre lien unique ! Content violates conditions of use + Le contenu enfreint les conditions d'utilisation blocking reason @@ -2138,6 +2123,7 @@ Il s'agit de votre propre lien unique ! Create list + Créer une liste No comment provided by engineer. @@ -2357,7 +2343,7 @@ Il s'agit de votre propre lien unique ! Delete Supprimer alert action - swipe action +swipe action Delete %lld messages of members? @@ -2401,6 +2387,7 @@ Il s'agit de votre propre lien unique ! Delete chat messages from your device. + Supprimer les messages de chat de votre appareil. No comment provided by engineer. @@ -2495,6 +2482,7 @@ Il s'agit de votre propre lien unique ! Delete list? + Supprimer la liste ? alert title @@ -2549,6 +2537,7 @@ Il s'agit de votre propre lien unique ! Delete report + Supprimer le rapport No comment provided by engineer. @@ -2713,10 +2702,12 @@ Il s'agit de votre propre lien unique ! Disable automatic message deletion? + Désactiver la suppression automatique des messages ? alert title Disable delete messages + Désactiver la suppression des messages alert button @@ -2811,6 +2802,7 @@ Il s'agit de votre propre lien unique ! Documents: + Documents: No comment provided by engineer. @@ -2825,6 +2817,7 @@ Il s'agit de votre propre lien unique ! Don't miss important messages. + Ne manquez pas les messages importants. No comment provided by engineer. @@ -2834,6 +2827,7 @@ Il s'agit de votre propre lien unique ! Done + Terminé No comment provided by engineer. @@ -2845,7 +2839,7 @@ Il s'agit de votre propre lien unique ! Download Télécharger alert button - chat item action +chat item action Download errors @@ -2922,9 +2916,9 @@ Il s'agit de votre propre lien unique ! Activer (conserver les remplacements) No comment provided by engineer. - - Enable Flux - Activer 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. @@ -3069,6 +3063,7 @@ Il s'agit de votre propre lien unique ! Encryption renegotiation in progress. + Renégociation du chiffrement en cours. No comment provided by engineer. @@ -3183,6 +3178,7 @@ Il s'agit de votre propre lien unique ! Error checking token status + Erreur lors de la vérification de l'état du jeton (token) No comment provided by engineer. @@ -3207,6 +3203,7 @@ Il s'agit de votre propre lien unique ! Error creating list + Erreur lors de la création de la liste alert title @@ -3226,6 +3223,7 @@ Il s'agit de votre propre lien unique ! Error creating report + Erreur lors de la création du rapport No comment provided by engineer. @@ -3340,6 +3338,7 @@ Il s'agit de votre propre lien unique ! Error registering for notifications + Erreur lors de l'inscription aux notifications alert title @@ -3349,6 +3348,7 @@ Il s'agit de votre propre lien unique ! Error reordering lists + Erreur lors de la réorganisation des listes alert title @@ -3363,6 +3363,7 @@ Il s'agit de votre propre lien unique ! Error saving chat list + Erreur lors de l'enregistrement de la liste des chats alert title @@ -3447,6 +3448,7 @@ Il s'agit de votre propre lien unique ! Error testing server connection + Erreur lors du test de connexion au serveur No comment provided by engineer. @@ -3492,7 +3494,9 @@ Il s'agit de votre propre lien unique ! Error: %@ Erreur : %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3531,6 +3535,7 @@ Il s'agit de votre propre lien unique ! Expired + Expiré token status text @@ -3575,6 +3580,7 @@ Il s'agit de votre propre lien unique ! Faster deletion of groups. + Suppression plus rapide des groupes. No comment provided by engineer. @@ -3584,6 +3590,7 @@ Il s'agit de votre propre lien unique ! Faster sending messages. + Envoi plus rapide des messages. No comment provided by engineer. @@ -3593,6 +3600,7 @@ Il s'agit de votre propre lien unique ! Favorites + Favoris No comment provided by engineer. @@ -3607,9 +3615,11 @@ Il s'agit de votre propre lien unique ! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. + Le fichier est bloqué par l'opérateur du serveur : +%@. file error text @@ -5495,7 +5505,7 @@ Nécessite l'activation d'un VPN. Open Ouvrir - No comment provided by engineer. + alert action Open Settings @@ -5527,23 +5537,15 @@ Nécessite l'activation d'un VPN. Ouvrir le groupe No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device Ouvrir le transfert vers un autre appareil authentication reason - - Open web link? - No comment provided by engineer. - Opening app… Ouverture de l'app… @@ -5846,11 +5848,19 @@ Erreur : %@ Respect de la vie privée de vos clients. No comment provided by engineer. + + Privacy policy and conditions of use. + No comment provided by engineer. + Privacy redefined La vie privée redéfinie No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + No comment provided by engineer. + Private filenames Noms de fichiers privés @@ -6212,7 +6222,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Reject Rejeter reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6337,8 +6347,8 @@ Activez-le dans les paramètres *Réseau et serveurs*. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -6439,11 +6449,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Vérifier les conditions No comment provided by engineer. - - Review later - Vérifier plus tard - No comment provided by engineer. - Revoke Révoquer @@ -6493,7 +6498,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Save Enregistrer alert button - chat item action +chat item action Save (and notify contacts) @@ -7060,7 +7065,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 @@ -7122,6 +7127,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Partager avec vos contacts No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code Afficher le code QR @@ -7222,6 +7231,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Adresse SimpleX ou lien unique ? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address Adresse de contact SimpleX @@ -7327,7 +7340,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7683,21 +7696,11 @@ 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 **%@**. Les mêmes conditions s'appliquent à l'opérateur **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - Les mêmes conditions s'appliquent à(aux) l'opérateur(s) : **%@**. - No comment provided by engineer. - The second preset operator in the app! Le deuxième opérateur prédéfini de l'application ! @@ -7812,6 +7815,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Voici votre propre lien unique ! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le bureau. @@ -8109,6 +8116,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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. @@ -8202,6 +8213,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 @@ -8267,6 +8282,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser les serveurs No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Utiliser l'application pendant l'appel. @@ -8561,10 +8580,6 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Serveur XFTP No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. Vous **ne devez pas** utiliser la même base de données sur deux appareils. @@ -8657,11 +8672,6 @@ 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. - Vous pouvez définir les opérateurs dans les paramètres Réseau et serveurs. - No comment provided by engineer. - You can configure servers via settings. Vous pouvez configurer les serveurs via les paramètres. @@ -8903,11 +8913,6 @@ 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 @@ -8993,6 +8998,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. @@ -9003,11 +9013,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 @@ -9145,7 +9150,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 @@ -9316,7 +9322,7 @@ Répéter la demande de connexion ? default (%@) défaut (%@) delete after time - pref value +pref value default (no) @@ -9443,21 +9449,11 @@ 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. - pour une meilleure protection des métadonnées. - No comment provided by engineer. - forwarded transféré @@ -9666,8 +9662,8 @@ Répéter la demande de connexion ? off off enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -10034,7 +10030,7 @@ dernier message reçu : %2$@
- +
@@ -10071,7 +10067,7 @@ dernier message reçu : %2$@
- +
@@ -10093,7 +10089,7 @@ dernier message reçu : %2$@
- +
@@ -10101,6 +10097,10 @@ dernier message reçu : %2$@ %d nouveaux événements notification body + + From %d chat(s) + notification body + From: %@ De : %@ @@ -10116,16 +10116,11 @@ dernier message reçu : %2$@ Nouveaux messages notification - - New messages in %d chats - Nouveaux messages dans %d chats - notification body -
- +
@@ -10147,7 +10142,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..f76d7eba1e 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -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. 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 bdb3083f5a..6ad4d159c7 100644 --- a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff +++ b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff @@ -2619,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. 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 cd2c8022c1..78bee138e4 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -2,26 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (másolható) @@ -169,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 @@ -324,7 +307,7 @@ %u messages failed to decrypt. - %u üzenet visszafejtése sikertelen. + Nem sikerült visszafejteni %u üzenetet. No comment provided by engineer. @@ -332,11 +315,6 @@ %u üzenet kihagyva. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (új) @@ -347,11 +325,6 @@ (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. **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. @@ -417,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). @@ -458,11 +426,6 @@ - előzmények szerkesztése. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 mp @@ -477,7 +440,7 @@ 1 day 1 nap delete after time - time interval +time interval 1 hour @@ -493,13 +456,13 @@ 1 month 1 hónap delete after time - time interval +time interval 1 week 1 hét delete after time - time interval +time interval 1 year @@ -508,12 +471,12 @@ 1-time link - Egyszer használható meghívási 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ási hivatkozás csak *egyetlen partnerrel használható* – személyesen vagy bármilyen üzenetváltó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. @@ -531,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> @@ -543,16 +501,6 @@ <p><a href="%@">Csatlakozzon hozzám a SimpleX Chaten keresztül</a></p> email text - - @%@ - @%@ - No comment provided by engineer. - - - @'%@' - @'%@' - No comment provided by engineer. - A few more things Néhány további dolog @@ -592,7 +540,7 @@ Abort changing address? - Cím módosításának megszakítása? + Megszakítja a cím módosítását? No comment provided by engineer. @@ -614,8 +562,8 @@ Accept Elfogadás accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -624,19 +572,19 @@ Accept connection request? - Kapcsolatkérés elfogadása? + Elfogadja a meghívási kérést? No comment provided by engineer. Accept contact request from %@? - Elfogadja %@ kapcsolatkérését? + Elfogadja %@ meghívási kérését? notification body Accept incognito Elfogadás inkognitóban accept contact request via notification - swipe action +swipe action Accepted conditions @@ -665,7 +613,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 a partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve az Ön partnerei 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. @@ -755,7 +703,7 @@ Address or 1-time link? - Cím vagy egyszer használható meghívási hivatkozás? + Cím vagy egyszer használható meghívó? No comment provided by engineer. @@ -798,9 +746,9 @@ 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 (text), and the list deleted. - Az összes csevegés el lesz távolítva a(z) (text) nevű listáról, és a lista is törölve lesz. + + 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 @@ -810,7 +758,7 @@ All data is kept private on your device. - Az összes adat privát módon van tárolva az Ön eszközén. + Az összes adat privát módon van tárolva az eszközén. No comment provided by engineer. @@ -820,7 +768,7 @@ 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á kvantumálló titkosítással is rendelkeznek. + 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. @@ -835,7 +783,7 @@ All new messages from %@ will be hidden! - Az összes %@ által írt új üzenet el lesz rejtve! + %@ összes új üzenete el lesz rejtve! No comment provided by engineer. @@ -848,6 +796,11 @@ Az összes jelentés archiválva lesz az Ön számára. No comment provided by engineer. + + All servers + Összes kiszolgáló + No comment provided by engineer. + All your contacts will remain connected. Az összes partnerével kapcsolatban marad. @@ -855,7 +808,7 @@ 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 az Ön partnerei számára. + A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. No comment provided by engineer. @@ -875,7 +828,7 @@ Allow calls? - Hívások engedélyezése? + Engedélyezi a hívásokat? No comment provided by engineer. @@ -950,7 +903,7 @@ Allow voice messages? - Hangüzenetek engedélyezése? + Engedélyezi a hangüzeneteket? No comment provided by engineer. @@ -1095,12 +1048,12 @@ Archive %lld reports? - %lld jelentés archiválása? + Archivál %lld jelentést? No comment provided by engineer. Archive all reports? - Az összes jelentés archiválása? + Archiválja az összes jelentést? No comment provided by engineer. @@ -1120,7 +1073,7 @@ Archive report? - Jelentés archiválása? + Archiválja a jelentést? No comment provided by engineer. @@ -1138,14 +1091,9 @@ Adatbázis archiválása No comment provided by engineer. - - Ask - Mindig kérdezzen rá - No comment provided by engineer. - Attach - Csatolás + Mellékelés No comment provided by engineer. @@ -1195,7 +1143,7 @@ Auto-accept contact requests - Kapcsolatkérések automatikus elfogadása + Meghívási kérések automatikus elfogadása No comment provided by engineer. @@ -1310,12 +1258,12 @@ 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. @@ -1383,6 +1331,15 @@ 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! @@ -1432,7 +1389,7 @@ Cancel Mégse alert action - alert button +alert button Cancel migration @@ -1471,7 +1428,7 @@ Change automatic message deletion? - Automatikus üzenettörlés módosítása? + Módosítja az automatikus üzenettörlést? alert title @@ -1481,7 +1438,7 @@ Change database passphrase? - Az adatbázis jelmondatának módosítása? + Módosítja az adatbázis jelmondatát? No comment provided by engineer. @@ -1491,7 +1448,7 @@ Change member role? - Tag szerepkörének módosítása? + Módosítja a tag szerepkörét? No comment provided by engineer. @@ -1523,7 +1480,7 @@ Change self-destruct passcode Önmegsemmisítő-jelkód módosítása authentication reason - set passcode view +set passcode view Chat @@ -1692,12 +1649,12 @@ Clear conversation? - Üzenetek kiürítése? + Kiüríti az üzeneteket? No comment provided by engineer. Clear group? - Csoport kiürítése? + Kiüríti a csoportot? No comment provided by engineer. @@ -1707,7 +1664,7 @@ Clear private notes? - Privát jegyzetek kiürítése? + Kiüríti a privát jegyzeteket? No comment provided by engineer. @@ -1747,7 +1704,7 @@ Conditions accepted on: %@. - Feltételek elfogadva: %@. + Feltételek elfogadásának ideje: %@. No comment provided by engineer. @@ -1765,16 +1722,6 @@ 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ők 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: **%@**. @@ -1782,12 +1729,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 el lesznek fogadva az engedélyezett üzemeltetők 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. @@ -1795,6 +1742,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 @@ -1877,21 +1829,21 @@ Connect to yourself? - Kapcsolódás saját magához? + Kapcsolódik saját magához? No comment provided by engineer. Connect to yourself? This is your own SimpleX address! - Kapcsolódás saját magához? -Ez az Ön SimpleX-címe! + Kapcsolódik saját magához? +Ez a saját SimpleX-címe! No comment provided by engineer. Connect to yourself? This is your own one-time link! - Kapcsolódás saját magához? -Ez az Ön egyszer használható meghívási hivatkozása! + Kapcsolódik saját magához? +Ez a saját egyszer használható meghívója! No comment provided by engineer. @@ -1906,7 +1858,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Connect via one-time link - Kapcsolódás egyszer használható meghívási hivatkozáson keresztül + Kapcsolódás egyszer használható meghívón keresztül No comment provided by engineer. @@ -2003,7 +1955,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Connection request sent! - Kapcsolatkérés elküldve! + Meghívási kérés elküldve! No comment provided by engineer. @@ -2128,7 +2080,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Correct name to %@? - Név helyesbítése a következőre: %@? + Helyesbíti a nevet a következőre: %@? No comment provided by engineer. @@ -2138,7 +2090,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Create 1-time link - Egyszer használható meghívási hivatkozás létrehozása + Egyszer használható meghívó létrehozása No comment provided by engineer. @@ -2258,7 +2210,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Customizable message shape. - Testre szabható üzenetbuborékok. + Személyre szabható üzenetbuborékok. No comment provided by engineer. @@ -2393,7 +2345,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete Törlés alert action - swipe action +swipe action Delete %lld messages of members? @@ -2402,7 +2354,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete %lld messages? - %lld üzenet törlése? + Töröl %lld üzenetet? No comment provided by engineer. @@ -2412,7 +2364,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete address? - Cím törlése? + Törli a címet? No comment provided by engineer. @@ -2437,7 +2389,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete chat messages from your device. - Csevegési üzenetek törlése az Ön eszközéről. + Csevegési üzenetek törlése a saját eszközéről. No comment provided by engineer. @@ -2447,12 +2399,12 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete chat profile? - Csevegési profil törlése? + Törli a csevegési profilt? No comment provided by engineer. Delete chat? - Csevegés törlése? + Törli a csevegést? No comment provided by engineer. @@ -2467,7 +2419,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete contact? - Partner törlése? + Törli a partnert? No comment provided by engineer. @@ -2487,7 +2439,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete files and media? - A fájlok- és a médiatartalmak törlése? + Törli a fájl- és a médiatartalmakat? No comment provided by engineer. @@ -2502,7 +2454,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete for me - Csak számomra + Csak nálam No comment provided by engineer. @@ -2512,7 +2464,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete group? - Csoport törlése? + Törli a csoportot? No comment provided by engineer. @@ -2527,22 +2479,22 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete link? - Hivatkozás törlése? + Törli a hivatkozást? No comment provided by engineer. Delete list? - Lista törlése? + 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. @@ -2562,7 +2514,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete old database? - Régi adatbázis törlése? + Törli a régi adatbázist? No comment provided by engineer. @@ -2572,7 +2524,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete pending connection? - Függőben lévő partnerkérelem törlése? + Törli a függőben lévő meghívót? No comment provided by engineer. @@ -2597,7 +2549,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Delete user profile? - Felhasználói profil törlése? + Törli a felhasználói profilt? No comment provided by engineer. @@ -2752,7 +2704,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Disable automatic message deletion? - Automatikus üzenettörlés letiltása? + Letiltja az automatikus üzenettörlést? alert title @@ -2807,7 +2759,7 @@ Ez az Ön egyszer használható meghívási 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. @@ -2822,7 +2774,7 @@ Ez az Ön egyszer használható meghívási 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. @@ -2889,7 +2841,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Download Letöltés alert button - chat item action +chat item action Download errors @@ -2933,7 +2885,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Duplicate display name! - Duplikált megjelenített név! + Duplikált megjelenítendő név! No comment provided by engineer. @@ -2966,9 +2918,9 @@ Ez az Ön egyszer használható meghívási hivatkozása! Engedélyezés (felülírások megtartásával) 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. @@ -2983,7 +2935,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Enable automatic message deletion? - Automatikus üzenet törlés engedélyezése? + Engedélyezi az automatikus üzenettörlést? alert title @@ -3003,7 +2955,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Enable instant notifications? - Azonnali értesítések engedélyezése? + Engedélyezi az azonnali értesítéseket? No comment provided by engineer. @@ -3018,7 +2970,7 @@ Ez az Ön egyszer használható meghívási 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. @@ -3048,7 +3000,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Encrypt database? - Adatbázis titkosítása? + Titkosítja az adatbázist? No comment provided by engineer. @@ -3103,7 +3055,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Encryption re-negotiation error - Hiba a titkosítás újraegyeztetésekor + Hiba történt a titkosítás újraegyeztetésekor message decrypt error item @@ -3178,282 +3130,282 @@ Ez az Ön egyszer használható meghívási hivatkozása! Error aborting address change - Hiba a cím módosítá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 meghívási kérés elfogadásakor No comment provided by engineer. Error adding member(s) - Hiba a tag(ok) hozzáadásakor + Hiba történt a tag(ok) hozzáadásakor No comment provided by engineer. Error adding server - Hiba a kiszolgáló hozzáadásakor + Hiba történt a kiszolgáló hozzáadásakor alert title Error changing address - Hiba a cím módosításakor + Hiba történt a cím módosításakor No comment provided by engineer. 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 módosítá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 módosításakor + Hiba történt a beállítás módosításakor No comment provided by engineer. 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 a token állapotának ellenőrzésekor + 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. + Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később. No comment provided by engineer. Error creating address - Hiba 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 a lista létrehozásakor + Hiba történt a lista létrehozásakor alert title Error creating member contact - Hiba a partnerrel 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 a jelentés létrehozásakor + Hiba történt a jelentés létrehozásakor No comment provided by engineer. Error decrypting file - Hiba a fájl visszafejtésekor + Hiba történt a fájl visszafejtésekor No comment provided by engineer. Error deleting chat database - Hiba a csevegési adatbázis törlésekor + Hiba történt a csevegési adatbázis törlésekor No comment provided by engineer. Error deleting chat! - Hiba a csevegés törlésekor! + Hiba történt a csevegés törlésekor! No comment provided by engineer. Error deleting connection - Hiba a kapcsolat törlésekor + Hiba történt a kapcsolat törlésekor No comment provided by engineer. Error deleting database - Hiba az adatbázis törlésekor + Hiba történt az adatbázis törlésekor No comment provided by engineer. Error deleting old database - Hiba a régi adatbázis törlésekor + Hiba történt a régi adatbázis törlésekor No comment provided by engineer. Error deleting token - Hiba a token törlésekor + Hiba történt a token törlésekor No comment provided by engineer. Error deleting user profile - Hiba a felhasználó-profil törlésekor + Hiba történt a felhasználó-profil törlésekor No comment provided by engineer. Error downloading the archive - Hiba az archívum letöltésekor + Hiba történt az archívum letöltésekor No comment provided by engineer. Error enabling delivery receipts! - Hiba a kézbesítési jelentések engedélyezésekor! + Hiba történt a kézbesítési jelentések engedélyezésekor! No comment provided by engineer. Error enabling notifications - Hiba az értesítések engedélyezésekor + Hiba történt az értesítések engedélyezésekor No comment provided by engineer. Error encrypting database - Hiba az adatbázis titkosításakor + Hiba történt az adatbázis titkosításakor No comment provided by engineer. Error exporting chat database - Hiba a csevegési adatbázis exportálásakor + Hiba történt a csevegési adatbázis exportálásakor No comment provided by engineer. Error exporting theme: %@ - Hiba 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 + Hiba történt a csevegési adatbázis importálásakor No comment provided by engineer. Error joining group - Hiba a csoporthoz való csatlakozáskor + Hiba történt a csoporthoz való csatlakozáskor No comment provided by engineer. Error loading servers - Hiba a kiszolgálók betöltésekor + Hiba történt a kiszolgálók betöltésekor alert title Error migrating settings - Hiba a beállí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 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 az értesítések regisztrálásakor + Hiba történt az értesítések regisztrálásakor alert title Error removing member - Hiba a tag eltávolításakor + Hiba történt a tag eltávolításakor No comment provided by engineer. Error reordering lists - Hiba a listák újrarendezésekor + 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 a csevegési lista mentésekor + 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ó jelszavának mentésekor + Hiba történt a felhasználó jelszavának mentésekor No comment provided by engineer. Error scanning code: %@ - Hiba a kód beolvasá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. @@ -3463,7 +3415,7 @@ Ez az Ön egyszer használható meghívási 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. @@ -3473,47 +3425,47 @@ Ez az Ön egyszer használható meghívási 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 + Hiba történt a profilváltáskor No comment provided by engineer. Error switching profile! - Hiba a profilváltáskor! + 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 a kiszolgáló kapcsolatának tesztelésekor + 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 @@ -3523,17 +3475,17 @@ Ez az Ön egyszer használható meghívási 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. @@ -3544,7 +3496,9 @@ Ez az Ön egyszer használható meghívási hivatkozása! Error: %@ Hiba: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3663,11 +3617,11 @@ Ez az Ön egyszer használható meghívási hivatkozása! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. A kiszolgáló üzemeltetője letiltotta a fájlt: -(info.reason.text). +%@. file error text @@ -3812,7 +3766,7 @@ Ez az Ön egyszer használható meghívási 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 partnere 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. @@ -3837,7 +3791,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! Forward %d message(s)? - %d üzenet továbbítása? + Továbbít %d üzenetet? alert title @@ -3852,7 +3806,7 @@ Ez az Ön egyszer használható meghívási 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 @@ -3877,7 +3831,7 @@ Ez az Ön egyszer használható meghívási hivatkozása! 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. + A(z) %@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %@ célkiszolgálóhoz. Próbálja meg később. No comment provided by engineer. @@ -4101,7 +4055,7 @@ Hiba: %2$@ Hide: - Elrejtés: + Elrejtve: No comment provided by engineer. @@ -4216,7 +4170,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. @@ -4505,7 +4459,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 Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt. +2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt. 3. A kapcsolat sérült. No comment provided by engineer. @@ -4563,7 +4517,7 @@ További fejlesztések hamarosan! Join your group? This is your link for group %@! Csatlakozik a csoportjához? -Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! +Ez a saját hivatkozása a(z) %@ nevű csoporthoz! No comment provided by engineer. @@ -4588,7 +4542,7 @@ 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 @@ -4633,7 +4587,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. @@ -4643,7 +4597,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Leave group? - Csoport elhagyása? + Elhagyja a csoportot? No comment provided by engineer. @@ -4688,7 +4642,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! List name... - Listanév… + Lista neve… No comment provided by engineer. @@ -4808,7 +4762,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! 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. @@ -4963,7 +4917,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Messages from %@ will be shown! - Az összes %@ által írt üzenet meg fog jelenni! + %@ összes üzenete meg fog jelenni! No comment provided by engineer. @@ -4988,12 +4942,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! 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**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve. No comment provided by engineer. Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. - Az üzenetek, fájlok és hívások **végpontok közötti kvantumálló titkosítással** sérülés utáni titkosságvédelemmel, visszautasítással és feltörés utáni helyreállítással vannak védve. + Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve. No comment provided by engineer. @@ -5038,7 +4992,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. @@ -5128,7 +5082,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Network connection - Internetkapcsolat + Hálózati kapcsolat No comment provided by engineer. @@ -5148,7 +5102,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. @@ -5193,7 +5147,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! New contact request - Új kapcsolatkérés + Új meghívási kérés notification @@ -5208,7 +5162,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. @@ -5472,7 +5426,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! One-time invitation link - Egyszer használható meghívási hivatkozás + Egyszer használható meghívó No comment provided by engineer. @@ -5587,7 +5541,7 @@ VPN engedélyezése szükséges. Open Megnyitás - No comment provided by engineer. + alert action Open Settings @@ -5619,26 +5573,15 @@ VPN engedélyezése szükséges. Csoport megnyitása No comment provided by engineer. - - Open link - Hivatkozás megnyitása - No comment provided by engineer. - - - Open links from chat list - Hivatkozás megnyitása a csevegési listából - No comment provided by engineer. + + Open link? + alert title Open migration to another device Átköltöztetés indítása egy másik eszközre authentication reason - - Open web link? - Webhivatkozás megnyitása? - No comment provided by engineer. - Opening app… Az alkalmazás megnyitása… @@ -5820,7 +5763,7 @@ Minden további problémát osszon meg a fejlesztőkkel. 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. + Ellenőrizze a hálózati kapcsolatát a vele: %@, és próbálja újra. No comment provided by engineer. @@ -5942,12 +5885,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 szerver üzemeltetői számára. No comment provided by engineer. @@ -6012,7 +5965,7 @@ Hiba: %@ Profile update will be sent to your contacts. - A profilfrissítés el lesz küldve az Ön partnerei számára. + A profilfrissítés el lesz küldve a partnerei számára. alert message @@ -6094,7 +6047,7 @@ Engedélyezze a *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. @@ -6124,7 +6077,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Quantum resistant encryption - Kvantumálló titkosítás + Kvantumbiztos titkosítás No comment provided by engineer. @@ -6264,7 +6217,7 @@ Engedélyezze a *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. @@ -6274,12 +6227,12 @@ Engedélyezze a *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. @@ -6304,7 +6257,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Register notification token? - Értesítési token regisztrálása? + Regisztrálja az értesítési tokent? token info @@ -6316,7 +6269,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Reject Elutasítás reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6325,7 +6278,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Reject contact request - Kapcsolatkérés elutasítása + Meghívási kérés elutasítása No comment provided by engineer. @@ -6345,7 +6298,7 @@ Engedélyezze a *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. @@ -6360,12 +6313,12 @@ Engedélyezze a *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. @@ -6380,12 +6333,12 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Renegotiate encryption? - Titkosítás újraegyeztetése? + Újraegyezteti a titkosítást? No comment provided by engineer. Repeat connection request? - Kapcsolatkérés megismétlése? + Megismétli a meghívási kérést? No comment provided by engineer. @@ -6400,7 +6353,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Repeat join request? - Csatlakozáskérés megismétlése? + Megismétli a meghívási kérést? No comment provided by engineer. @@ -6448,9 +6401,9 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Szabálysértés jelentése: csak a csoport moderátorai látják. report reason - - Report: (text.isEmpty ? reason.text : text) - Jelentés: (text.isEmpty ? reason.text : text) + + Report: %@ + Jelentés: %@ report in notification @@ -6485,7 +6438,7 @@ Engedélyezze a *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. @@ -6530,12 +6483,12 @@ Engedélyezze a *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. @@ -6553,11 +6506,6 @@ Engedélyezze a *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 - No comment provided by engineer. - Revoke Visszavonás @@ -6570,7 +6518,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Revoke file? - Fájl visszavonása? + Visszavonja a fájlt? No comment provided by engineer. @@ -6607,7 +6555,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Save Mentés alert button - chat item action +chat item action Save (and notify contacts) @@ -6656,7 +6604,7 @@ Engedélyezze a *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 @@ -6671,17 +6619,17 @@ Engedélyezze a *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 @@ -6726,7 +6674,7 @@ Engedélyezze a *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. @@ -6871,12 +6819,12 @@ Engedélyezze a *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. @@ -6901,7 +6849,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. 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. @@ -6916,7 +6864,7 @@ Engedélyezze a *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üldője törölhette a meghívási kérést. No comment provided by engineer. @@ -7041,7 +6989,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Server operators - Kiszolgáló-üzemeltetők + Kiszolgálóüzemeltetők No comment provided by engineer. @@ -7178,16 +7126,16 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Share Megosztás alert action - chat item action +chat item action Share 1-time link - Egyszer használható meghívási 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ási hivatkozás megosztása egy baráttal + Egyszer használható meghívó megosztása egy baráttal No comment provided by engineer. @@ -7227,7 +7175,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Share this 1-time invite link - Ennek az egyszer használható meghívási hivatkozásnak a megosztása + Ennek az egyszer használható meghívónak a megosztása No comment provided by engineer. @@ -7240,6 +7188,11 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Megosztás a partnerekkel No comment provided by engineer. + + Short link + Rövid hivatkozás + No comment provided by engineer. + Show QR code QR-kód megjelenítése @@ -7282,7 +7235,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Show: - Megjelenítés: + Megjelenítve: No comment provided by engineer. @@ -7332,14 +7285,19 @@ Engedélyezze a *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ási hivatkozás biztonságosan megosztható bármilyen üzenetváltó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ási hivatkozás? + SimpleX-cím vagy egyszer használható meghívó? No comment provided by engineer. + + SimpleX channel link + SimpleX-csatornahivatkozás + simplex link type + SimpleX contact address SimpleX kapcsolattartási cím @@ -7372,7 +7330,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. SimpleX one-time invitation - Egyszer használható SimpleX-meghívási hivatkozás + Egyszer használható SimpleX-meghívó simplex link type @@ -7446,7 +7404,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Spam Kéretlen tartalom blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7460,7 +7418,7 @@ Engedélyezze a *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. @@ -7500,7 +7458,7 @@ Engedélyezze a *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. @@ -7510,12 +7468,12 @@ Engedélyezze a *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. @@ -7525,7 +7483,7 @@ Engedélyezze a *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 @@ -7575,7 +7533,7 @@ Engedélyezze a *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ási hivatkozásokhoz. + Csevegési profilváltás az egyszer használható meghívókhoz. No comment provided by engineer. @@ -7590,12 +7548,12 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. TCP connection - TCP kapcsolat + TCP-kapcsolat 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. @@ -7705,12 +7663,12 @@ Engedélyezze a *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. @@ -7722,7 +7680,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. 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 meghívási kéréseket kap – ezt a beállítások menüben engedélyezheti. No comment provided by engineer. @@ -7747,7 +7705,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. 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 partnere 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. @@ -7805,21 +7763,11 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. A régi adatbázis nem lett eltávolítva az átköltöztetéskor, ezért törölhető. No comment provided by engineer. - - The profile is only shared with your contacts. - A profilja csak a partnereivel van megosztva. - No comment provided by engineer. - The same conditions will apply to operator **%@**. Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**. No comment provided by engineer. - - The same conditions will apply to operator(s): **%@**. - Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k) 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! @@ -7902,7 +7850,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. This chat is protected by quantum resistant end-to-end encryption. - Ez a csevegés végpontok közötti kvantumálló titkosítással védett. + Ez a csevegés végpontok közötti kvantumbiztos titkosítással védett. E2EE info chat item @@ -7927,12 +7875,17 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. This is your own SimpleX address! - Ez az Ön SimpleX-címe! + Ez a saját SimpleX-címe! No comment provided by engineer. This is your own one-time link! - Ez az Ön egyszer használható meghívási hivatkozása! + Ez a saját egyszer használható meghívója! + No comment provided by engineer. + + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Ez a hivatkozás újabb alkalmazásverziót igényel. Frissítse az alkalmazást vagy kérjen egy kompatibilis hivatkozást a partnerétől. No comment provided by engineer. @@ -7999,7 +7952,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 Ön adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. + Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. No comment provided by engineer. @@ -8134,7 +8087,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. @@ -8211,7 +8164,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Unlink desktop? - Számítógép leválasztása? + Leválasztja a számítógépet? No comment provided by engineer. @@ -8234,6 +8187,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso 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 lesz elküldve az új tagok számára. @@ -8251,16 +8209,17 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Update network settings? - Hálózati beállítások módosítá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. @@ -8328,6 +8287,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso A következő TCP-port használata, amikor nincs port megadva: %@. No comment provided by engineer. + + Use TCP port 443 for preset servers only. + A 443-as TCP-port használata kizárólag az előre beállított kiszolgálokhoz. + No comment provided by engineer. + Use chat SimpleX Chat használata @@ -8393,6 +8357,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Kiszolgálók használata No comment provided by engineer. + + Use short links (BETA) + Rövid hivatkozások használata (béta) + No comment provided by engineer. + Use the app while in the call. Használja az alkalmazást hívás közben. @@ -8465,7 +8434,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Via secure quantum resistant protocol. - Biztonságos kvantumálló-protokollon keresztül. + Biztonságos kvantumbiztos protokollon keresztül. No comment provided by engineer. @@ -8688,11 +8657,6 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso XFTP-kiszolgáló No comment provided by engineer. - - Yes - Igen - No comment provided by engineer. - You **must not** use the same database on two devices. **Nem szabad** ugyanazt az adatbázist használni egyszerre két eszközön. @@ -8710,7 +8674,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso 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. @@ -8730,7 +8694,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso You are already connecting via this one-time link! - A kapcsolódás már folyamatban van ezen az egyszer használható meghívási hivatkozáson keresztül! + A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül! No comment provided by engineer. @@ -8757,7 +8721,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso You are already joining the group! Repeat join request? A csatlakozás már folyamatban van a csoporthoz! -Csatlakozáskérés megismétlése? +Megismétli a meghívási kérést? No comment provided by engineer. @@ -8785,11 +8749,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” menüben 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. @@ -8897,14 +8856,14 @@ Csatlakozáskérés megismétlése? You have already requested connection via this address! - Már küldött egy kapcsolatkérést ezen a címen keresztül! + Már küldött egy meghívási kérést ezen a címen keresztül! No comment provided by engineer. You have already requested connection! Repeat connection request? - Már küldött egy kapcsolódási kérelmet! -Kapcsolatkérés megismétlése? + Ön már küldött egy meghívási kérést! +Megismétli a meghívási kérést? No comment provided by engineer. @@ -8979,7 +8938,7 @@ 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 el lesz fogadva, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később! No comment provided by engineer. @@ -9019,7 +8978,7 @@ Kapcsolatkérés megismétlése? 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 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. @@ -9032,11 +8991,6 @@ 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 @@ -9122,6 +9076,11 @@ Kapcsolatkérés megismétlése? 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 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. @@ -9129,14 +9088,9 @@ Kapcsolatkérés megismétlése? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. - A profilja módosult. Ha elmenti, a profilfrissítés el lesz küldve az Ön partnerei számára. + A profilja módosult. Ha elmenti, 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, a partnerei és az elküldött üzenetei az Ön eszközén vannak tárolva. - No comment provided by engineer. - Your random profile Véletlenszerű profil @@ -9149,7 +9103,7 @@ Kapcsolatkérés megismétlése? Your servers - Az Ön kiszolgálói + Saját kiszolgálók No comment provided by engineer. @@ -9159,7 +9113,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. @@ -9275,7 +9229,8 @@ Kapcsolatkérés megismétlése? blocked by admin letiltva az adminisztrátor által - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -9374,7 +9329,7 @@ Kapcsolatkérés megismétlése? connecting (introduced) - kapcsolódás (bejelentve) + kapcsolódás (bemutatkozva) No comment provided by engineer. @@ -9446,7 +9401,7 @@ Kapcsolatkérés megismétlése? default (%@) alapértelmezett (%@) delete after time - pref value +pref value default (no) @@ -9573,21 +9528,11 @@ 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 @@ -9635,7 +9580,7 @@ Kapcsolatkérés megismétlése? incognito via one-time link - inkognitó egy egyszer használható meghívási hivatkozáson keresztül + inkognitó egy egyszer használható meghívón keresztül chat list item description @@ -9680,12 +9625,12 @@ Kapcsolatkérés megismétlése? invited to connect - meghívta, hogy csatlakozzon + Függőben lévő meghívó 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 @@ -9797,8 +9742,8 @@ Kapcsolatkérés megismétlése? off kikapcsolva enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9842,15 +9787,17 @@ Kapcsolatkérés megismétlése? pending + függőben No comment provided by engineer. pending approval + jóváhagyásra vár No comment provided by engineer. quantum resistant e2e encryption - végpontok közötti kvantumálló titkosítás + végpontok közötti kvantumbiztos titkosítás chat item text @@ -9865,6 +9812,7 @@ Kapcsolatkérés megismétlése? rejected + elutasítva No comment provided by engineer. @@ -9899,7 +9847,7 @@ Kapcsolatkérés megismétlése? requested to connect - kérelmezve a kapcsolódáshoz + Függőben lévő meghívási kérelem chat list item title @@ -10038,7 +9986,7 @@ utoljára fogadott üzenet: %2$@ via one-time link - egy egyszer használható meghívási hivatkozáson keresztül + egy egyszer használható meghívón keresztül chat list item description @@ -10138,12 +10086,12 @@ utoljára fogadott üzenet: %2$@ you shared one-time link - Ön egy egyszer használható meghívási 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ási hivatkozást osztott meg inkognitóban + Ön egy egyszer használható meghívót osztott meg inkognitóban chat list item description @@ -10165,7 +10113,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -10202,7 +10150,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -10224,7 +10172,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -10232,6 +10180,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: %@ @@ -10247,16 +10200,11 @@ utoljára fogadott üzenet: %2$@ Új üzenetek notification - - New messages in %d chats - Új üzenetek %d csevegésben - notification body -
- +
@@ -10278,7 +10226,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -10348,12 +10296,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. @@ -10438,7 +10386,7 @@ utoljára fogadott üzenet: %2$@ Slow network? - Lassú internetkapcsolat? + Lassú a hálózata? 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 7bc503c097..cf5f61918f 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2,26 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (può essere copiato) @@ -332,11 +315,6 @@ %u messaggi saltati. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nuovo) @@ -347,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. @@ -417,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). @@ -458,11 +426,6 @@ - cronologia delle modifiche. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -477,7 +440,7 @@ 1 day 1 giorno delete after time - time interval +time interval 1 hour @@ -493,13 +456,13 @@ 1 month 1 mese delete after time - time interval +time interval 1 week 1 settimana delete after time - time interval +time interval 1 year @@ -531,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> @@ -543,16 +501,6 @@ <p><a href="%@">Connettiti a me via SimpleX Chat</a></p> email text - - @%@ - @%@ - No comment provided by engineer. - - - @'%@' - @'%@' - No comment provided by engineer. - A few more things Qualche altra cosa @@ -614,8 +562,8 @@ Accept Accetta accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -636,7 +584,7 @@ Accept incognito Accetta in incognito accept contact request via notification - swipe action +swipe action Accepted conditions @@ -798,9 +746,9 @@ Tutte le chat e i messaggi verranno eliminati. Non è reversibile! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. - Tutte le chat verranno rimosse dall'elenco (testo) e l'elenco eliminato. + + 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 @@ -848,6 +796,11 @@ 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. @@ -1138,11 +1091,6 @@ Archiviazione del database No comment provided by engineer. - - Ask - Chiedi - No comment provided by engineer. - Attach Allega @@ -1270,7 +1218,7 @@ Better privacy and security - Privacy e sicurezza migliorate + Privacy e sicurezza migliori No comment provided by engineer. @@ -1383,6 +1331,15 @@ 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! @@ -1432,7 +1389,7 @@ Cancel Annulla alert action - alert button +alert button Cancel migration @@ -1523,7 +1480,7 @@ Change self-destruct passcode Cambia codice di autodistruzione authentication reason - set passcode view +set passcode view Chat @@ -1765,16 +1722,6 @@ 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: **%@**. @@ -1795,6 +1742,11 @@ Configura server ICE No comment provided by engineer. + + Configure server operators + Configura gli operatori dei server + No comment provided by engineer. + Confirm Conferma @@ -2393,7 +2345,7 @@ Questo è il tuo link una tantum! Delete Elimina alert action - swipe action +swipe action Delete %lld messages of members? @@ -2889,7 +2841,7 @@ Questo è il tuo link una tantum! Download Scarica alert button - chat item action +chat item action Download errors @@ -2966,9 +2918,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. @@ -3544,7 +3496,9 @@ Questo è il tuo link una tantum! Error: %@ Errore: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3663,11 +3617,11 @@ Questo è il tuo link una tantum! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. Il file è bloccato dall'operatore del server: -(info.reason.text). +%@. file error text @@ -5587,7 +5541,7 @@ Richiede l'attivazione della VPN. Open Apri - No comment provided by engineer. + alert action Open Settings @@ -5619,26 +5573,15 @@ Richiede l'attivazione della VPN. Apri gruppo No comment provided by engineer. - - Open link - Apri link - No comment provided by engineer. - - - Open links from chat list - Apri i link dall'elenco delle chat - No comment provided by engineer. + + Open link? + alert title Open migration to another device Apri migrazione ad un altro dispositivo authentication reason - - Open web link? - Aprire il link? - No comment provided by engineer. - Opening app… Apertura dell'app… @@ -5945,11 +5888,21 @@ 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 @@ -6316,7 +6269,7 @@ Attivalo nelle impostazioni *Rete e server*. Reject Rifiuta reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6448,9 +6401,9 @@ Attivalo nelle impostazioni *Rete e server*. Segnala violazione: solo i moderatori del gruppo lo vedranno. report reason - - Report: (text.isEmpty ? reason.text : text) - Segnalazione: (text.isEmpty ? reason.text : text) + + Report: %@ + Segnalazione: %@ report in notification @@ -6553,11 +6506,6 @@ Attivalo nelle impostazioni *Rete e server*. Leggi le condizioni No comment provided by engineer. - - Review later - Leggi più tardi - No comment provided by engineer. - Revoke Revoca @@ -6607,7 +6555,7 @@ Attivalo nelle impostazioni *Rete e server*. Save Salva alert button - chat item action +chat item action Save (and notify contacts) @@ -7178,7 +7126,7 @@ Attivalo nelle impostazioni *Rete e server*. Share Condividi alert action - chat item action +chat item action Share 1-time link @@ -7240,6 +7188,11 @@ Attivalo nelle impostazioni *Rete e server*. Condividi con i contatti No comment provided by engineer. + + Short link + Link breve + No comment provided by engineer. + Show QR code Mostra codice QR @@ -7340,6 +7293,11 @@ Attivalo nelle impostazioni *Rete e server*. Indirizzo SimpleX o link una tantum? No comment provided by engineer. + + SimpleX channel link + Link del canale SimpleX + simplex link type + SimpleX contact address Indirizzo di contatto SimpleX @@ -7446,7 +7404,7 @@ Attivalo nelle impostazioni *Rete e server*. Spam Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7805,21 +7763,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! @@ -7935,6 +7883,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questo è il tuo link una tantum! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Questo link richiede una versione più recente dell'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop. @@ -8234,6 +8187,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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. @@ -8261,6 +8219,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Updated conditions + Condizioni aggiornate No comment provided by engineer. @@ -8328,6 +8287,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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 @@ -8393,6 +8357,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa i server No comment provided by engineer. + + Use short links (BETA) + Usa link brevi (BETA) + No comment provided by engineer. + Use the app while in the call. Usa l'app mentre sei in chiamata. @@ -8688,11 +8657,6 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Server XFTP No comment provided by engineer. - - Yes - - No comment provided by engineer. - You **must not** use the same database on two devices. **Non devi** usare lo stesso database su due dispositivi. @@ -8785,11 +8749,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. @@ -9032,11 +8991,6 @@ 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 @@ -9122,6 +9076,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. @@ -9132,11 +9091,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 @@ -9275,7 +9229,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 @@ -9446,7 +9401,7 @@ Ripetere la richiesta di connessione? default (%@) predefinito (%@) delete after time - pref value +pref value default (no) @@ -9573,21 +9528,11 @@ 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 @@ -9715,7 +9660,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 @@ -9797,8 +9742,8 @@ Ripetere la richiesta di connessione? off off enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9842,10 +9787,12 @@ Ripetere la richiesta di connessione? pending + in attesa No comment provided by engineer. pending approval + in attesa di approvazione No comment provided by engineer. @@ -9865,6 +9812,7 @@ Ripetere la richiesta di connessione? rejected + rifiutato No comment provided by engineer. @@ -10165,7 +10113,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -10202,7 +10150,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -10224,7 +10172,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -10232,6 +10180,11 @@ ultimo msg ricevuto: %2$@ %d nuovi eventi notification body + + From %d chat(s) + Da %d chat + notification body + From: %@ Da: %@ @@ -10247,16 +10200,11 @@ ultimo msg ricevuto: %2$@ Nuovi messaggi notification - - New messages in %d chats - Nuovi messaggi in %d chat - notification body -
- +
@@ -10278,7 +10226,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 1a79d251eb..27134216a7 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (コピー可能) @@ -330,11 +314,6 @@ %u 件のメッセージがスキップされました。 No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (新規) @@ -345,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. **コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。 @@ -415,11 +389,6 @@ \*太字* No comment provided by engineer. - - , - , - No comment provided by engineer. - - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -456,11 +425,6 @@ - 編集履歴。 No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 秒 @@ -475,7 +439,7 @@ 1 day 1日 delete after time - time interval +time interval 1 hour @@ -491,13 +455,13 @@ 1 month 1ヶ月 delete after time - time interval +time interval 1 week 1週間 delete after time - time interval +time interval 1 year @@ -528,11 +492,6 @@ 30秒 No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -540,14 +499,6 @@ <p><a href="%@">SimpleX Chatでつながろう</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things その他 @@ -607,8 +558,8 @@ Accept 承諾 accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -628,7 +579,7 @@ Accept incognito シークレットモードで承諾 accept contact request via notification - swipe action +swipe action Accepted conditions @@ -773,8 +724,8 @@ 全チャットとメッセージが削除されます(※元に戻せません※)! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. alert message @@ -817,6 +768,10 @@ 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. あなたの連絡先が繋がったまま継続します。 @@ -1092,10 +1047,6 @@ Archiving database No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach 添付する @@ -1311,6 +1262,12 @@ チャット プロファイル経由 (デフォルト) または [接続経由](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! 通話は既に終了してます! @@ -1355,7 +1312,7 @@ Cancel 中止 alert action - alert button +alert button Cancel migration @@ -1440,7 +1397,7 @@ Change self-destruct passcode 自己破壊パスコードを変更する authentication reason - set passcode view +set passcode view Chat @@ -1659,14 +1616,6 @@ 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 will be accepted for the operator(s): **%@**. No comment provided by engineer. @@ -1684,6 +1633,10 @@ ICEサーバを設定 No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm 確認 @@ -2037,6 +1990,7 @@ This is your own one-time link! Create profile + プロフィールを作成する No comment provided by engineer. @@ -2238,7 +2192,7 @@ This is your own one-time link! Delete 削除 alert action - swipe action +swipe action Delete %lld messages of members? @@ -2697,7 +2651,7 @@ This is your own one-time link! Download alert button - chat item action +chat item action Download errors @@ -2766,8 +2720,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. @@ -3301,7 +3255,9 @@ This is your own one-time link! Error: %@ エラー : %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3407,9 +3363,9 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5162,7 +5118,7 @@ VPN を有効にする必要があります。 Open 開く - No comment provided by engineer. + alert action Open Settings @@ -5191,22 +5147,14 @@ VPN を有効にする必要があります。 Open group No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device authentication reason - - Open web link? - No comment provided by engineer. - Opening app… No comment provided by engineer. @@ -5479,11 +5427,19 @@ 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 プライベートなファイル名 @@ -5816,7 +5772,7 @@ Enable in *Network & servers* settings. Reject 拒否 reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -5934,8 +5890,8 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -6029,10 +5985,6 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later - No comment provided by engineer. - Revoke 取り消す @@ -6078,7 +6030,7 @@ Enable in *Network & servers* settings. Save 保存 alert button - chat item action +chat item action Save (and notify contacts) @@ -6593,7 +6545,7 @@ Enable in *Network & servers* settings. Share 共有する alert action - chat item action +chat item action Share 1-time link @@ -6648,6 +6600,10 @@ Enable in *Network & servers* settings. 連絡先と共有する No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -6740,6 +6696,10 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX連絡先アドレス @@ -6835,7 +6795,7 @@ Enable in *Network & servers* settings. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7166,19 +7126,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. @@ -7280,6 +7231,10 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. @@ -7552,6 +7507,10 @@ To connect, please ask your contact to create another connection link and check 未読 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. @@ -7636,6 +7595,10 @@ To connect, please ask your contact to create another connection link and check 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 チャット @@ -7694,6 +7657,10 @@ To connect, please ask your contact to create another connection link and check Use servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. @@ -7957,10 +7924,6 @@ To connect, please ask your contact to create another connection link and check XFTP server No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. No comment provided by engineer. @@ -8041,10 +8004,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. @@ -8271,11 +8230,6 @@ Repeat connection request? あなたのICEサーバ No comment provided by engineer. - - Your SMP servers - あなたのSMPサーバ - No comment provided by engineer. - Your SimpleX address あなたのSimpleXアドレス @@ -8357,6 +8311,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 サーバーはあなたのプロファイルを参照できません。 @@ -8366,11 +8325,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 あなたのランダム・プロフィール @@ -8498,7 +8452,8 @@ Repeat connection request? blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8665,7 +8620,7 @@ Repeat connection request? default (%@) デフォルト (%@) delete after time - pref value +pref value default (no) @@ -8790,19 +8745,10 @@ 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. @@ -9006,8 +8952,8 @@ Repeat connection request? off オフ enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9346,7 +9292,7 @@ last received msg: %2$@
- +
@@ -9382,7 +9328,7 @@ last received msg: %2$@
- +
@@ -9404,13 +9350,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9423,15 +9373,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9450,7 +9396,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 d643600bc9..019f63cbc0 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -36,7 +36,7 @@ !1 colored! - !1 색상! + !1 색상 적용됨! No comment provided by engineer. @@ -166,6 +166,7 @@ %llds + No comment provided by engineer. @@ -188,7 +189,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. - **비공개**: 매 20분마다 새 메시지를 확인합니다. 푸시 서버에는 장치 토큰만 공유됩니다. 연락처 수나 메세지 메타데이터가 표시되지 않습니다. + **비공개**: 20분마다 새로운 메시지를 확인합니다. 푸시 서버에는 장치 토큰만 공유됩니다. 연락처 수나 메세지 메타데이터가 표시되지 않습니다. No comment provided by engineer. @@ -231,6 +232,7 @@ \*bold* + No comment provided by engineer. @@ -923,46 +925,58 @@ 현재 지원되는 최대 파일 크기는 %@입니다. 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. @@ -2853,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. @@ -4941,6 +4955,172 @@ This is your own SimpleX address! 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..0f795170c6 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -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. 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 aeb9c96696..4008c57ac0 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2,26 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (kan gekopieerd worden) @@ -332,11 +315,6 @@ %u berichten zijn overgeslagen. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nieuw) @@ -347,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. @@ -417,11 +390,6 @@ \*vetgedrukt* No comment provided by engineer. - - , - , - No comment provided by engineer. - - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -458,11 +426,6 @@ - bewerkingsgeschiedenis. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -477,7 +440,7 @@ 1 day 1 dag delete after time - time interval +time interval 1 hour @@ -493,13 +456,13 @@ 1 month 1 maand delete after time - time interval +time interval 1 week 1 week delete after time - time interval +time interval 1 year @@ -531,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> @@ -543,16 +501,6 @@ <p><a href="%@">Maak verbinding met mij via SimpleX Chat</a></p> email text - - @%@ - @%@ - No comment provided by engineer. - - - @'%@' - @'%@' - No comment provided by engineer. - A few more things Nog een paar dingen @@ -614,8 +562,8 @@ Accept Accepteer accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -636,7 +584,7 @@ Accept incognito Accepteer incognito accept contact request via notification - swipe action +swipe action Accepted conditions @@ -798,9 +746,9 @@ 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 (text), and the list deleted. - Alle chats worden uit de lijst (tekst) verwijderd en de lijst wordt verwijderd. + + 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 @@ -848,6 +796,10 @@ Alle rapporten worden voor u gearchiveerd. No comment provided by engineer. + + All servers + No comment provided by engineer. + All your contacts will remain connected. Al uw contacten blijven verbonden. @@ -1138,11 +1090,6 @@ Database archiveren No comment provided by engineer. - - Ask - Vragen - No comment provided by engineer. - Attach Bijvoegen @@ -1383,6 +1330,15 @@ 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! @@ -1432,7 +1388,7 @@ Cancel Annuleren alert action - alert button +alert button Cancel migration @@ -1523,7 +1479,7 @@ Change self-destruct passcode Zelfvernietigings code wijzigen authentication reason - set passcode view +set passcode view Chat @@ -1765,16 +1721,6 @@ 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): **%@**. @@ -1795,6 +1741,11 @@ ICE servers configureren No comment provided by engineer. + + Configure server operators + Serveroperators configureren + No comment provided by engineer. + Confirm Bevestigen @@ -2393,7 +2344,7 @@ Dit is uw eigen eenmalige link! Delete Verwijderen alert action - swipe action +swipe action Delete %lld messages of members? @@ -2867,7 +2818,7 @@ Dit is uw eigen eenmalige link! Don't miss important messages. - ‐Mis geen belangrijke berichten. + Mis geen belangrijke berichten. No comment provided by engineer. @@ -2889,7 +2840,7 @@ Dit is uw eigen eenmalige link! Download Downloaden alert button - chat item action +chat item action Download errors @@ -2966,9 +2917,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. @@ -3273,7 +3224,7 @@ Dit is uw eigen eenmalige link! Error creating report - Fout bij maken van rapport + Fout bij het rapporteren No comment provided by engineer. @@ -3544,7 +3495,9 @@ Dit is uw eigen eenmalige link! Error: %@ Fout: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3663,11 +3616,11 @@ Dit is uw eigen eenmalige link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. Bestand is geblokkeerd door serveroperator: -(info.reason.text). +%@. file error text @@ -5587,7 +5540,7 @@ Vereist het inschakelen van VPN. Open Open - No comment provided by engineer. + alert action Open Settings @@ -5619,26 +5572,15 @@ Vereist het inschakelen van VPN. Open groep No comment provided by engineer. - - Open link - Link openen - No comment provided by engineer. - - - Open links from chat list - Open links van chatlijst - No comment provided by engineer. + + Open link? + alert title Open migration to another device Open de migratie naar een ander apparaat authentication reason - - Open web link? - Weblink openen? - No comment provided by engineer. - Opening app… App openen… @@ -5945,11 +5887,21 @@ 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 @@ -6316,7 +6268,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Reject Afwijzen reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6415,7 +6367,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Report - Rapport + rapporteren chat item action @@ -6448,9 +6400,9 @@ Schakel dit in in *Netwerk en servers*-instellingen. Rapporteer overtreding: alleen groepsmoderators kunnen dit zien. report reason - - Report: (text.isEmpty ? reason.text : text) - Rapport: (text.isEmpty ? reason.text : text) + + Report: %@ + rapporteer: %@ report in notification @@ -6553,11 +6505,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Voorwaarden bekijken No comment provided by engineer. - - Review later - Later beoordelen - No comment provided by engineer. - Revoke Intrekken @@ -6607,7 +6554,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Save Opslaan alert button - chat item action +chat item action Save (and notify contacts) @@ -6886,7 +6833,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Send private reports - Verstuur rapporten privé + Rapporteer privé No comment provided by engineer. @@ -7178,7 +7125,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Share Deel alert action - chat item action +chat item action Share 1-time link @@ -7240,6 +7187,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Delen met contacten No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code Toon QR-code @@ -7340,6 +7291,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX adres of eenmalige link? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX contact adres @@ -7446,7 +7401,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Spam Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7805,21 +7760,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! @@ -7935,6 +7880,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Dit is uw eigen eenmalige link! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop. @@ -8234,6 +8183,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Ongelezen swipe action + + Unsupported connection link + 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. @@ -8261,6 +8214,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Updated conditions + Bijgewerkte voorwaarden No comment provided by engineer. @@ -8328,6 +8282,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik TCP-poort %@ als er geen poort is opgegeven. No comment provided by engineer. + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Gebruik chat @@ -8393,6 +8351,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Gebruik de app tijdens het gesprek. @@ -8688,11 +8650,6 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak XFTP server No comment provided by engineer. - - Yes - Ja - No comment provided by engineer. - You **must not** use the same database on two devices. U **mag** niet dezelfde database op twee apparaten gebruiken. @@ -8785,11 +8742,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. @@ -9032,11 +8984,6 @@ 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 @@ -9122,6 +9069,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. @@ -9132,11 +9084,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 @@ -9275,7 +9222,8 @@ Verbindingsverzoek herhalen? blocked by admin geblokkeerd door beheerder - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -9446,7 +9394,7 @@ Verbindingsverzoek herhalen? default (%@) standaard (%@) delete after time - pref value +pref value default (no) @@ -9573,21 +9521,11 @@ 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 @@ -9797,8 +9735,8 @@ Verbindingsverzoek herhalen? off uit enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9842,10 +9780,12 @@ Verbindingsverzoek herhalen? pending + In behandeling No comment provided by engineer. pending approval + in afwachting van goedkeuring No comment provided by engineer. @@ -9865,6 +9805,7 @@ Verbindingsverzoek herhalen? rejected + afgewezen No comment provided by engineer. @@ -10165,7 +10106,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -10202,7 +10143,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -10224,7 +10165,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -10232,6 +10173,10 @@ laatst ontvangen bericht: %2$@ ‐%d nieuwe gebeurtenissen notification body + + From %d chat(s) + notification body + From: %@ Van: %@ @@ -10247,16 +10192,11 @@ laatst ontvangen bericht: %2$@ Nieuwe berichten notification - - New messages in %d chats - Nieuwe berichten in %d chats - notification body -
- +
@@ -10278,7 +10218,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 aa257a2322..175c8b4112 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (można skopiować) @@ -331,11 +315,6 @@ %u pominiętych wiadomości. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nowy) @@ -346,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. @@ -416,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). @@ -457,11 +426,6 @@ - historia edycji. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sek @@ -476,7 +440,7 @@ 1 day 1 dzień delete after time - time interval +time interval 1 hour @@ -492,13 +456,13 @@ 1 month 1 miesiąc delete after time - time interval +time interval 1 week 1 tydzień delete after time - time interval +time interval 1 year @@ -530,11 +494,6 @@ 30 sekund No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -542,16 +501,6 @@ <p><a href="%@">Połącz się ze mną poprzez SimpleX Chat.</a></p> email text - - @%@ - @%@ - No comment provided by engineer. - - - @'%@' - @'%@' - No comment provided by engineer. - A few more things Jeszcze kilka rzeczy @@ -613,8 +562,8 @@ Accept Akceptuj accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -635,7 +584,7 @@ Accept incognito Akceptuj incognito accept contact request via notification - swipe action +swipe action Accepted conditions @@ -797,9 +746,9 @@ 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 (text), and the list deleted. - Wszystkie rozmowy zostaną usunięte z listy (text), a lista usunięta. + + 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 @@ -847,6 +796,10 @@ 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. @@ -1137,11 +1090,6 @@ Archiwizowanie bazy danych No comment provided by engineer. - - Ask - Zapytaj - No comment provided by engineer. - Attach Dołącz @@ -1380,6 +1328,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! @@ -1429,7 +1383,7 @@ Cancel Anuluj alert action - alert button +alert button Cancel migration @@ -1519,7 +1473,7 @@ Change self-destruct passcode Zmień pin samozniszczenia authentication reason - set passcode view +set passcode view Chat @@ -1758,15 +1712,6 @@ Warunki użytkowania No comment provided by engineer. - - Conditions will be accepted for enabled operators after 30 days. - Warunki zostaną zaakceptowane dla aktywowanych operatorów po 30 dniach. - 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): **%@**. No comment provided by engineer. @@ -1784,6 +1729,10 @@ Skonfiguruj serwery ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Potwierdź @@ -2370,7 +2319,7 @@ To jest twój jednorazowy link! Delete Usuń alert action - swipe action +swipe action Delete %lld messages of members? @@ -2853,7 +2802,7 @@ To jest twój jednorazowy link! Download Pobierz alert button - chat item action +chat item action Download errors @@ -2929,8 +2878,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. @@ -3493,7 +3442,9 @@ To jest twój jednorazowy link! Error: %@ Błąd: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3607,9 +3558,9 @@ To jest twój jednorazowy link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5468,7 +5419,7 @@ Wymaga włączenia VPN. Open Otwórz - No comment provided by engineer. + alert action Open Settings @@ -5498,23 +5449,15 @@ Wymaga włączenia VPN. Grupa otwarta No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device Otwórz migrację na innym urządzeniu authentication reason - - Open web link? - No comment provided by engineer. - Opening app… Otwieranie aplikacji… @@ -5811,11 +5754,19 @@ 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 @@ -6177,7 +6128,7 @@ Włącz w ustawianiach *Sieć i serwery* . Reject Odrzuć reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6302,8 +6253,8 @@ Włącz w ustawianiach *Sieć i serwery* . Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -6403,10 +6354,6 @@ Włącz w ustawianiach *Sieć i serwery* . Review conditions No comment provided by engineer. - - Review later - No comment provided by engineer. - Revoke Odwołaj @@ -6456,7 +6403,7 @@ Włącz w ustawianiach *Sieć i serwery* . Save Zapisz alert button - chat item action +chat item action Save (and notify contacts) @@ -7019,7 +6966,7 @@ Włącz w ustawianiach *Sieć i serwery* . Share Udostępnij alert action - chat item action +chat item action Share 1-time link @@ -7078,6 +7025,10 @@ Włącz w ustawianiach *Sieć i serwery* . Udostępnij kontaktom No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code Pokaż kod QR @@ -7175,6 +7126,10 @@ Włącz w ustawianiach *Sieć i serwery* . SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address Adres kontaktowy SimpleX @@ -7277,7 +7232,7 @@ Włącz w ustawianiach *Sieć i serwery* . Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7628,19 +7583,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. @@ -7752,6 +7698,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom To jest twój jednorazowy link! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze. @@ -8044,6 +7994,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc 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. @@ -8136,6 +8090,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc 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 @@ -8198,6 +8156,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Use servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Używaj aplikacji podczas połączenia. @@ -8489,10 +8451,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Serwer XFTP No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. **Nie możesz** używać tej samej bazy na dwóch urządzeniach. @@ -8584,10 +8542,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. @@ -8826,11 +8780,6 @@ 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 @@ -8916,6 +8865,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. @@ -8926,11 +8880,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 @@ -9067,7 +9016,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 @@ -9238,7 +9188,7 @@ Powtórzyć prośbę połączenia? default (%@) domyślne (%@) delete after time - pref value +pref value default (no) @@ -9365,20 +9315,11 @@ 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 @@ -9587,8 +9528,8 @@ Powtórzyć prośbę połączenia? off wyłączony enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9954,7 +9895,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
@@ -9991,7 +9932,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
@@ -10013,13 +9954,17 @@ ostatnia otrzymana wiadomość: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -10032,16 +9977,11 @@ ostatnia otrzymana wiadomość: %2$@ New messages notification - - New messages in %d chats - Nowe wiadomości w %d czatach - notification body -
- +
@@ -10063,7 +10003,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..bbb6c7d22a 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -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. 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..bc8bf79da1 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -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. 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 567e36c53a..419fa75375 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (можно скопировать) @@ -148,7 +132,7 @@ %@, %@ and %lld other members connected - %@, %@ и %lld других членов соединены + установлено соединение с %@, %@ и %lld другими членами группы No comment provided by engineer. @@ -248,7 +232,7 @@ %lld members - Членов группы: %lld + %lld членов No comment provided by engineer. @@ -331,11 +315,6 @@ %u сообщений пропущено. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (новое) @@ -346,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. **Добавить контакт**: создать и поделиться новой ссылкой-приглашением. @@ -416,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). @@ -457,11 +426,6 @@ - история редактирования. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -476,7 +440,7 @@ 1 day 1 день delete after time - time interval +time interval 1 hour @@ -492,13 +456,13 @@ 1 month 1 месяц delete after time - time interval +time interval 1 week 1 неделю delete after time - time interval +time interval 1 year @@ -530,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> @@ -542,14 +501,6 @@ <p><a href="%@">Соединитесь со мной в SimpleX Chat.</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things Еще несколько изменений @@ -573,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. @@ -611,8 +562,8 @@ Accept Принять accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -633,7 +584,7 @@ Accept incognito Принять инкогнито accept contact request via notification - swipe action +swipe action Accepted conditions @@ -652,6 +603,7 @@ Active + Активный token status text @@ -794,8 +746,9 @@ Все чаты и сообщения будут удалены - это нельзя отменить! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. + Все чаты будут удалены из списка %@, и список удален. alert message @@ -810,7 +763,7 @@ All group members will remain connected. - Все члены группы, которые соединились через эту ссылку, останутся в группе. + Все члены группы останутся соединены. No comment provided by engineer. @@ -840,6 +793,11 @@ All reports will be archived for you. + Все сообщения о нарушениях будут заархивированы для вас. + No comment provided by engineer. + + + All servers No comment provided by engineer. @@ -899,7 +857,7 @@ Allow sending direct messages to members. - Разрешить посылать прямые сообщения членам группы. + Разрешить личные сообщения членам группы. No comment provided by engineer. @@ -919,6 +877,7 @@ Allow to report messsages to moderators. + Разрешить отправлять сообщения о нарушениях модераторам. No comment provided by engineer. @@ -1033,6 +992,7 @@ App group: + Группа приложения: No comment provided by engineer. @@ -1087,10 +1047,12 @@ Archive %lld reports? + Архивировать %lld сообщений о нарушениях? No comment provided by engineer. Archive all reports? + Архивировать все сообщения о нарушениях? No comment provided by engineer. @@ -1105,16 +1067,17 @@ Archive report - Архивировать отчет + Архивировать сообщение о нарушении No comment provided by engineer. Archive report? - Архивировать отчет? + Архивировать сообщение о нарушении? No comment provided by engineer. Archive reports + Архивировать сообщения о нарушениях swipe action @@ -1127,10 +1090,6 @@ Подготовка архива No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach Прикрепить @@ -1233,6 +1192,7 @@ Better groups performance + Улучшенная производительность групп No comment provided by engineer. @@ -1257,6 +1217,7 @@ Better privacy and security + Улучшенная конфиденциальность и безопасность No comment provided by engineer. @@ -1286,7 +1247,7 @@ Block group members - Блокируйте членов группы + Заблокировать членов группы No comment provided by engineer. @@ -1296,7 +1257,7 @@ Block member for all? - Заблокировать члена для всех? + Заблокировать для всех? No comment provided by engineer. @@ -1361,6 +1322,7 @@ Businesses + Бизнесы No comment provided by engineer. @@ -1368,6 +1330,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! Звонок уже завершен! @@ -1395,7 +1366,7 @@ Can't call member - Не удается позвонить члену группы + Не удаётся позвонить члену группы No comment provided by engineer. @@ -1410,14 +1381,14 @@ Can't message member - Не удается написать члену группы + Не удаётся отправить сообщение члену группы No comment provided by engineer. Cancel Отменить alert action - alert button +alert button Cancel migration @@ -1456,6 +1427,7 @@ Change automatic message deletion? + Измененить автоматическое удаление сообщений? alert title @@ -1507,7 +1479,7 @@ Change self-destruct passcode Изменить код самоуничтожения authentication reason - set passcode view +set passcode view Chat @@ -1681,10 +1653,12 @@ Clear group? + Очистить группу? No comment provided by engineer. Clear or delete group? + Очистить или удалить группу? No comment provided by engineer. @@ -1709,6 +1683,7 @@ Community guidelines violation + Нарушение правил группы report reason @@ -1746,16 +1721,6 @@ Условия использования 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): **%@**. Условия будут приняты для оператора(ов): **%@**. @@ -1776,6 +1741,11 @@ Настройка ICE серверов No comment provided by engineer. + + Configure server operators + Настроить операторов серверов + No comment provided by engineer. + Confirm Подтвердить @@ -1828,6 +1798,7 @@ Confirmed + Подтвержденный token status text @@ -1951,6 +1922,7 @@ This is your own one-time link! Connection blocked + Соединение заблокировано No comment provided by engineer. @@ -1966,10 +1938,13 @@ This is your own one-time link! Connection is blocked by server operator: %@ + Соединение заблокировано сервером оператора: +%@ No comment provided by engineer. Connection not ready. + Соединение не готово. No comment provided by engineer. @@ -1984,6 +1959,7 @@ This is your own one-time link! Connection requires encryption renegotiation. + Соединение требует повторного согласования шифрования. No comment provided by engineer. @@ -2068,6 +2044,7 @@ This is your own one-time link! Content violates conditions of use + Содержание нарушает условия использования blocking reason @@ -2147,6 +2124,7 @@ This is your own one-time link! Create list + Создать список No comment provided by engineer. @@ -2366,7 +2344,7 @@ This is your own one-time link! Delete Удалить alert action - swipe action +swipe action Delete %lld messages of members? @@ -2410,6 +2388,7 @@ This is your own one-time link! Delete chat messages from your device. + Удалить сообщения с вашего устройства. No comment provided by engineer. @@ -2504,6 +2483,7 @@ This is your own one-time link! Delete list? + Удалить список? alert title @@ -2558,6 +2538,7 @@ This is your own one-time link! Delete report + Удалить сообщение о нарушении No comment provided by engineer. @@ -2702,12 +2683,11 @@ 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. @@ -2722,10 +2702,12 @@ This is your own one-time link! Disable automatic message deletion? + Отключить автоматическое удаление сообщений? alert title Disable delete messages + Отключить удаление сообщений alert button @@ -2810,7 +2792,6 @@ This is your own one-time link! Do not send history to new members. - Не отправлять историю новым членам. No comment provided by engineer. @@ -2820,6 +2801,7 @@ This is your own one-time link! Documents: + Документы: No comment provided by engineer. @@ -2834,6 +2816,7 @@ This is your own one-time link! Don't miss important messages. + Не пропустите важные сообщения. No comment provided by engineer. @@ -2843,6 +2826,7 @@ This is your own one-time link! Done + Готово No comment provided by engineer. @@ -2854,7 +2838,7 @@ This is your own one-time link! Download Загрузить alert button - chat item action +chat item action Download errors @@ -2931,9 +2915,8 @@ 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. No comment provided by engineer. @@ -3078,6 +3061,7 @@ This is your own one-time link! Encryption renegotiation in progress. + Выполняется повторное согласование шифрования. No comment provided by engineer. @@ -3157,7 +3141,6 @@ This is your own one-time link! Error adding member(s) - Ошибка при добавлении членов группы No comment provided by engineer. @@ -3192,6 +3175,7 @@ This is your own one-time link! Error checking token status + Ошибка проверки статуса токена No comment provided by engineer. @@ -3216,11 +3200,11 @@ This is your own one-time link! Error creating list + Ошибка создания списка alert title Error creating member contact - Ошибка создания контакта с членом группы No comment provided by engineer. @@ -3235,6 +3219,7 @@ This is your own one-time link! Error creating report + Ошибка создания сообщения о нарушении No comment provided by engineer. @@ -3349,15 +3334,16 @@ This is your own one-time link! Error registering for notifications + Ошибка регистрации для уведомлений alert title Error removing member - Ошибка при удалении члена группы No comment provided by engineer. Error reordering lists + Ошибка сортировки списков alert title @@ -3372,6 +3358,7 @@ This is your own one-time link! Error saving chat list + Ошибка сохранения списка чатов alert title @@ -3416,7 +3403,6 @@ This is your own one-time link! Error sending member contact invitation - Ошибка отправки приглашения члену группы No comment provided by engineer. @@ -3456,6 +3442,7 @@ This is your own one-time link! Error testing server connection + Ошибка проверки соединения с сервером No comment provided by engineer. @@ -3501,7 +3488,9 @@ This is your own one-time link! Error: %@ Ошибка: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3540,6 +3529,7 @@ This is your own one-time link! Expired + Истекший token status text @@ -3584,6 +3574,7 @@ This is your own one-time link! Faster deletion of groups. + Ускорено удаление групп. No comment provided by engineer. @@ -3593,6 +3584,7 @@ This is your own one-time link! Faster sending messages. + Ускорена отправка сообщений. No comment provided by engineer. @@ -3602,6 +3594,7 @@ This is your own one-time link! Favorites + Избранное No comment provided by engineer. @@ -3616,9 +3609,11 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. + Файл заблокирован оператором сервера: +%@. file error text @@ -3743,11 +3738,11 @@ 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. @@ -3767,6 +3762,7 @@ This is your own one-time link! For me + Для меня No comment provided by engineer. @@ -3875,7 +3871,6 @@ Error: %2$@ Fully decentralized – visible only to members. - Группа полностью децентрализована – она видна только членам. No comment provided by engineer. @@ -3895,6 +3890,7 @@ Error: %2$@ Get notified when mentioned. + Уведомления, когда Вас упомянули. No comment provided by engineer. @@ -3984,7 +3980,6 @@ Error: %2$@ Group profile is stored on members' devices, not on the servers. - Профиль группы хранится на устройствах членов, а не на серверах. No comment provided by engineer. @@ -3994,7 +3989,6 @@ Error: %2$@ Group will be deleted for all members - this cannot be undone! - Группа будет удалена для всех членов - это действие нельзя отменить! No comment provided by engineer. @@ -4004,6 +3998,7 @@ Error: %2$@ Groups + Группы No comment provided by engineer. @@ -4013,6 +4008,7 @@ Error: %2$@ Help admins moderating their groups. + Помогайте администраторам модерировать их группы. No comment provided by engineer. @@ -4057,7 +4053,6 @@ Error: %2$@ History is not sent to new members. - История не отправляется новым членам. No comment provided by engineer. @@ -4077,6 +4072,7 @@ Error: %2$@ How it works + Как это работает alert button @@ -4222,10 +4218,12 @@ More improvements are coming soon! Inappropriate content + Неприемлемый контент report reason Inappropriate profile + Неприемлемый профиль report reason @@ -4322,22 +4320,27 @@ More improvements are coming soon! 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 @@ -4397,7 +4400,6 @@ More improvements are coming soon! Invite members - Пригласить членов группы No comment provided by engineer. @@ -4615,14 +4617,17 @@ This is your link for group %@! 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. @@ -4707,16 +4712,15 @@ This is your link for group %@! Member - Член группы No comment provided by engineer. Member inactive - Член неактивен item status text Member reports + Сообщения о нарушениях chat feature @@ -4726,32 +4730,26 @@ 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. Member will be removed from chat - this cannot be undone! - Член будет удален из разговора - это действие нельзя отменить! No comment provided by engineer. Member will be removed from group - this cannot be undone! - Член группы будет удален - это действие нельзя отменить! No comment provided by engineer. Members can add message reactions. - Члены группы могут добавлять реакции на сообщения. No comment provided by engineer. Members can irreversibly delete sent messages. (24 hours) - Члены группы могут необратимо удалять отправленные сообщения. (24 часа) No comment provided by engineer. @@ -4760,27 +4758,22 @@ This is your link for group %@! Members can send SimpleX links. - Члены группы могут отправлять ссылки SimpleX. No comment provided by engineer. Members can send direct messages. - Члены группы могут посылать прямые сообщения. No comment provided by engineer. Members can send disappearing messages. - Члены группы могут посылать исчезающие сообщения. No comment provided by engineer. Members can send files and media. - Члены группы могут слать файлы и медиа. No comment provided by engineer. Members can send voice messages. - Члены группы могут отправлять голосовые сообщения. No comment provided by engineer. @@ -4819,7 +4812,6 @@ This is your link for group %@! Message may be delivered later if member becomes active. - Сообщение может быть доставлено позже, если член группы станет активным. item status description @@ -4899,6 +4891,7 @@ This is your link for group %@! Messages in this chat will never be deleted. + Сообщения в этом чате никогда не будут удалены. alert message @@ -5003,6 +4996,7 @@ This is your link for group %@! More + Больше swipe action @@ -5037,6 +5031,7 @@ This is your link for group %@! Mute all + Все без звука notification label action @@ -5091,6 +5086,7 @@ This is your link for group %@! New + Новый token status text @@ -5155,7 +5151,6 @@ This is your link for group %@! New member role - Роль члена группы No comment provided by engineer. @@ -5185,14 +5180,17 @@ This is your link for group %@! No chats + Нет чатов No comment provided by engineer. No chats found + Чаты не найдены No comment provided by engineer. No chats in list %@ + Нет чатов в списке %@ No comment provided by engineer. @@ -5247,6 +5245,7 @@ This is your link for group %@! No message + Нет сообщения No comment provided by engineer. @@ -5306,10 +5305,12 @@ This is your link for group %@! No token! + Нет токена! alert title No unread chats + Нет непрочитанных чатов No comment provided by engineer. @@ -5324,6 +5325,7 @@ This is your link for group %@! Notes + Заметки No comment provided by engineer. @@ -5348,6 +5350,7 @@ This is your link for group %@! Notifications error + Ошибка уведомлений alert title @@ -5357,15 +5360,13 @@ This is your link for group %@! Notifications status + Статус уведомлений alert title Now admins can: - delete members' messages. - disable members ("observer" role) - Теперь админы могут: -- удалять сообщения членов. -- приостанавливать членов (роль "наблюдатель") No comment provided by engineer. @@ -5444,10 +5445,12 @@ Requires compatible VPN. Only sender and moderators see it + Только отправитель и модераторы видят это No comment provided by engineer. Only you and moderators see it + Только вы и модераторы видят это No comment provided by engineer. @@ -5503,7 +5506,7 @@ Requires compatible VPN. Open Открыть - No comment provided by engineer. + alert action Open Settings @@ -5535,23 +5538,15 @@ Requires compatible VPN. Открыть группу No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device Открытие миграции на другое устройство authentication reason - - Open web link? - No comment provided by engineer. - Opening app… Приложение отрывается… @@ -5599,6 +5594,7 @@ Requires compatible VPN. Organize chats into lists + Организуйте чаты в списки No comment provided by engineer. @@ -5660,7 +5656,6 @@ Requires compatible VPN. Past member %@ - Бывший член %@ past/unknown group member @@ -5685,7 +5680,7 @@ Requires compatible VPN. Pending - В ожидании + Ожидает No comment provided by engineer. @@ -5794,14 +5789,17 @@ Error: %@ Please try to disable and re-enable notfications. + Попробуйте выключить и снова включить уведомления. token info Please wait for token activation to complete. + Пожалуйста, дождитесь завершения активации токена. token info Please wait for token to be registered. + Пожалуйста, дождитесь регистрации токена. token info @@ -5854,11 +5852,21 @@ 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 Защищенные имена файлов @@ -5866,6 +5874,7 @@ Error: %@ Private media file names. + Конфиденциальные названия медиафайлов. No comment provided by engineer. @@ -5945,6 +5954,7 @@ Error: %@ Prohibit reporting messages to moderators. + Запретить жаловаться модераторам группы. No comment provided by engineer. @@ -5954,7 +5964,6 @@ Error: %@ Prohibit sending direct messages to members. - Запретить посылать прямые сообщения членам группы. No comment provided by engineer. @@ -6206,21 +6215,24 @@ Enable in *Network & servers* settings. Register + Зарегистрировать No comment provided by engineer. Register notification token? + Зарегистрировать токен уведомлений? token info Registered + Зарегистрирован token status text Reject Отклонить reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6259,12 +6271,10 @@ Enable in *Network & servers* settings. Remove member - Удалить члена группы No comment provided by engineer. Remove member? - Удалить члена группы? No comment provided by engineer. @@ -6319,42 +6329,52 @@ Enable in *Network & servers* settings. Report + Пожаловаться chat item action Report content: only group moderators will see it. + Пожаловаться на сообщение: увидят только модераторы группы. report reason Report member profile: only group moderators will see it. + Пожаловаться на профиль: увидят только модераторы группы. report reason Report other: only group moderators will see it. + Пожаловаться: увидят только модераторы группы. report reason Report reason? + Причина сообщения? No comment provided by engineer. Report spam: only group moderators will see it. + Пожаловаться на спам: увидят только модераторы группы. report reason Report violation: only group moderators will see it. + Пожаловаться на нарушение: увидят только модераторы группы. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ + Сообщение о нарушении: %@ report in notification Reporting messages to moderators is prohibited. + Сообщения о нарушениях запрещены в этой группе. No comment provided by engineer. Reports + Сообщения о нарушениях No comment provided by engineer. @@ -6447,11 +6467,6 @@ Enable in *Network & servers* settings. Посмотреть условия No comment provided by engineer. - - Review later - Посмотреть позже - No comment provided by engineer. - Revoke Отозвать @@ -6501,7 +6516,7 @@ Enable in *Network & servers* settings. Save Сохранить alert button - chat item action +chat item action Save (and notify contacts) @@ -6515,7 +6530,6 @@ Enable in *Network & servers* settings. Save and notify group members - Сохранить и уведомить членов группы No comment provided by engineer. @@ -6535,6 +6549,7 @@ Enable in *Network & servers* settings. Save list + Сохранить список No comment provided by engineer. @@ -6779,6 +6794,7 @@ Enable in *Network & servers* settings. Send private reports + Вы можете сообщить о нарушениях No comment provided by engineer. @@ -6798,7 +6814,6 @@ Enable in *Network & servers* settings. Send up to 100 last messages to new members. - Отправить до 100 последних сообщений новым членам. No comment provided by engineer. @@ -6998,6 +7013,7 @@ Enable in *Network & servers* settings. Set chat name… + Имя чата… No comment provided by engineer. @@ -7022,6 +7038,7 @@ Enable in *Network & servers* settings. Set message expiration in chats. + Установите срок хранения сообщений в чатах. No comment provided by engineer. @@ -7041,7 +7058,6 @@ Enable in *Network & servers* settings. Set the message shown to new members! - Установить сообщение для новых членов группы! No comment provided by engineer. @@ -7068,7 +7084,7 @@ Enable in *Network & servers* settings. Share Поделиться alert action - chat item action +chat item action Share 1-time link @@ -7130,6 +7146,10 @@ Enable in *Network & servers* settings. Поделиться с контактами No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code Показать QR код @@ -7230,6 +7250,10 @@ Enable in *Network & servers* settings. Адрес SimpleX или одноразовая ссылка? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX ссылка-контакт @@ -7334,8 +7358,9 @@ Enable in *Network & servers* settings. Spam + Спам blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7424,6 +7449,7 @@ Enable in *Network & servers* settings. Storage + Хранилище No comment provided by engineer. @@ -7488,6 +7514,7 @@ Enable in *Network & servers* settings. TCP port for messaging + TCP-порт для отправки сообщений No comment provided by engineer. @@ -7567,6 +7594,7 @@ Enable in *Network & servers* settings. Test notifications + Протестировать уведомления No comment provided by engineer. @@ -7668,22 +7696,18 @@ It can happen because of some bug or when the connection is compromised. The message will be deleted for all members. - Сообщение будет удалено для всех членов группы. No comment provided by engineer. The message will be marked as moderated for all members. - Сообщение будет помечено как удаленное для всех членов группы. No comment provided by engineer. The messages will be deleted for all members. - Сообщения будут удалены для всех членов группы. No comment provided by engineer. The messages will be marked as moderated for all members. - Сообщения будут помечены как удаленные для всех членов группы. No comment provided by engineer. @@ -7691,21 +7715,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! Второй оператор серверов в приложении! @@ -7773,6 +7787,7 @@ It can happen because of some bug or when the connection is compromised. This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + Это действие нельзя отменить - сообщения в этом чате, отправленные или полученные раньше чем выбрано, будут удалены. alert message @@ -7802,7 +7817,6 @@ It can happen because of some bug or when the connection is compromised. This group has over %lld members, delivery receipts are not sent. - В группе более %lld членов, отчёты о доставке выключены. No comment provided by engineer. @@ -7820,6 +7834,10 @@ It can happen because of some bug or when the connection is compromised.Это ваша собственная одноразовая ссылка! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере. @@ -7827,6 +7845,7 @@ It can happen because of some bug or when the connection is compromised. This message was deleted or not received yet. + Это сообщение было удалено или еще не получено. No comment provided by engineer. @@ -7943,6 +7962,7 @@ You will be prompted to complete authentication before this feature is enabled.< Token status: %@. + Статус токена: %@. token status @@ -8007,17 +8027,14 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member - Разблокировать члена группы No comment provided by engineer. Unblock member for all? - Разблокировать члена для всех? No comment provided by engineer. Unblock member? - Разблокировать члена группы? No comment provided by engineer. @@ -8117,9 +8134,12 @@ To connect, please ask your contact to create another connection link and check Не прочитано swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. - До 100 последних сообщений отправляются новым членам. No comment provided by engineer. @@ -8144,6 +8164,7 @@ To connect, please ask your contact to create another connection link and check Updated conditions + Обновленные условия No comment provided by engineer. @@ -8208,6 +8229,11 @@ To connect, please ask your contact to create another connection link and check Use TCP port %@ when no port is specified. + Использовать TCP-порт %@, когда порт не указан. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. No comment provided by engineer. @@ -8275,6 +8301,10 @@ To connect, please ask your contact to create another connection link and check Использовать серверы No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Используйте приложение во время звонка. @@ -8287,6 +8317,7 @@ To connect, please ask your contact to create another connection link and check Use web port + Использовать веб-порт No comment provided by engineer. @@ -8569,10 +8600,6 @@ To connect, please ask your contact to create another connection link and check XFTP сервер No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. Вы **не должны** использовать одну и ту же базу данных на двух устройствах. @@ -8665,11 +8692,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. Вы можете настроить серверы позже. @@ -8727,7 +8749,6 @@ Repeat join request? You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. - Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились. No comment provided by engineer. @@ -8804,7 +8825,6 @@ Repeat connection request? You joined this group. Connecting to inviting group member. - Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы. No comment provided by engineer. @@ -8844,6 +8864,7 @@ Repeat connection request? You should receive notifications. + Вы должны получать уведомления. token info @@ -8873,7 +8894,6 @@ Repeat connection request? You will connect to all group members. - Вы соединитесь со всеми членами группы. No comment provided by engineer. @@ -8911,11 +8931,6 @@ Repeat connection request? Ваши ICE серверы No comment provided by engineer. - - Your SMP servers - Ваши SMP серверы - No comment provided by engineer. - Your SimpleX address Ваш адрес SimpleX @@ -9001,6 +9016,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 серверы не могут получить доступ к Вашему профилю. @@ -9011,11 +9031,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 Случайный профиль @@ -9098,7 +9113,6 @@ Repeat connection request? all members - все члены feature role @@ -9113,6 +9127,7 @@ Repeat connection request? archived report + заархивированное сообщение о нарушении No comment provided by engineer. @@ -9153,7 +9168,8 @@ Repeat connection request? blocked by admin заблокировано администратором - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -9324,7 +9340,7 @@ Repeat connection request? default (%@) по умолчанию (%@) delete after time - pref value +pref value default (no) @@ -9451,21 +9467,11 @@ 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 переслано @@ -9588,12 +9594,10 @@ Repeat connection request? member - член группы member role member %1$@ changed to %2$@ - член %1$@ изменился на %2$@ profile update event chat item @@ -9633,6 +9637,7 @@ Repeat connection request? moderator + модератор member role @@ -9674,8 +9679,8 @@ Repeat connection request? off нет enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9719,10 +9724,12 @@ Repeat connection request? pending + ожидает No comment provided by engineer. pending approval + ожидает утверждения No comment provided by engineer. @@ -9742,6 +9749,7 @@ Repeat connection request? rejected + отклонён No comment provided by engineer. @@ -10042,7 +10050,7 @@ last received msg: %2$@
- +
@@ -10079,7 +10087,7 @@ last received msg: %2$@
- +
@@ -10101,7 +10109,7 @@ last received msg: %2$@
- +
@@ -10109,6 +10117,10 @@ last received msg: %2$@ %d новых сообщений notification body + + From %d chat(s) + notification body + From: %@ От: %@ @@ -10124,16 +10136,11 @@ last received msg: %2$@ Новые сообщения notification - - New messages in %d chats - Новые сообщения в %d разговоре(ах) - notification body -
- +
@@ -10155,7 +10162,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 05ebf4337d..671dd87d7d 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (สามารถคัดลอกได้) @@ -306,11 +290,6 @@ %u ข้อความที่ถูกข้าม No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) No comment provided by engineer. @@ -319,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. @@ -384,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). @@ -419,11 +388,6 @@ - ประวัติการแก้ไข No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec time to disappear @@ -437,7 +401,7 @@ 1 day 1 วัน delete after time - time interval +time interval 1 hour @@ -453,13 +417,13 @@ 1 month 1 เดือน delete after time - time interval +time interval 1 week 1 สัปดาห์ delete after time - time interval +time interval 1 year @@ -488,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> @@ -500,14 +459,6 @@ <p><a href="%@">เชื่อมต่อกับฉันผ่าน SimpleX Chat</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things อีกสองสามอย่าง @@ -566,8 +517,8 @@ Accept รับ accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -586,7 +537,7 @@ Accept incognito ยอมรับโหมดไม่ระบุตัวตน accept contact request via notification - swipe action +swipe action Accepted conditions @@ -728,8 +679,8 @@ แชทและข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. alert message @@ -771,6 +722,10 @@ 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. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. @@ -1035,10 +990,6 @@ Archiving database No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach แนบ @@ -1253,6 +1204,12 @@ ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](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! สิ้นสุดการโทรแล้ว! @@ -1297,7 +1254,7 @@ Cancel ยกเลิก alert action - alert button +alert button Cancel migration @@ -1382,7 +1339,7 @@ Change self-destruct passcode เปลี่ยนรหัสผ่านแบบทำลายตัวเอง authentication reason - set passcode view +set passcode view Chat @@ -1595,14 +1552,6 @@ 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 will be accepted for the operator(s): **%@**. No comment provided by engineer. @@ -1620,6 +1569,10 @@ กำหนดค่าเซิร์ฟเวอร์ ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm ยืนยัน @@ -2157,7 +2110,7 @@ This is your own one-time link! Delete ลบ alert action - swipe action +swipe action Delete %lld messages of members? @@ -2612,7 +2565,7 @@ This is your own one-time link! Download alert button - chat item action +chat item action Download errors @@ -2681,8 +2634,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. @@ -3213,7 +3166,9 @@ This is your own one-time link! Error: %@ ข้อผิดพลาด: % @ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3319,9 +3274,9 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5064,7 +5019,7 @@ Requires compatible VPN. Open - No comment provided by engineer. + alert action Open Settings @@ -5093,22 +5048,14 @@ Requires compatible VPN. Open group No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device authentication reason - - Open web link? - No comment provided by engineer. - Opening app… No comment provided by engineer. @@ -5381,11 +5328,19 @@ 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 ชื่อไฟล์ส่วนตัว @@ -5717,7 +5672,7 @@ Enable in *Network & servers* settings. Reject ปฏิเสธ reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -5834,8 +5789,8 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -5929,10 +5884,6 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later - No comment provided by engineer. - Revoke ถอน @@ -5978,7 +5929,7 @@ Enable in *Network & servers* settings. Save บันทึก alert button - chat item action +chat item action Save (and notify contacts) @@ -6498,7 +6449,7 @@ Enable in *Network & servers* settings. Share แชร์ alert action - chat item action +chat item action Share 1-time link @@ -6553,6 +6504,10 @@ Enable in *Network & servers* settings. แชร์กับผู้ติดต่อ No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -6644,6 +6599,10 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address ที่อยู่ติดต่อ SimpleX @@ -6737,7 +6696,7 @@ Enable in *Network & servers* settings. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7069,19 +7028,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. @@ -7182,6 +7132,10 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. No comment provided by engineer. @@ -7454,6 +7408,10 @@ To connect, please ask your contact to create another connection link and check เปลี่ยนเป็นยังไม่ได้อ่าน 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. @@ -7538,6 +7496,10 @@ To connect, please ask your contact to create another connection link and check 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 ใช้แชท @@ -7594,6 +7556,10 @@ To connect, please ask your contact to create another connection link and check Use servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. @@ -7857,10 +7823,6 @@ To connect, please ask your contact to create another connection link and check XFTP server No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. No comment provided by engineer. @@ -7941,10 +7903,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. @@ -8169,11 +8127,6 @@ Repeat connection request? เซิร์ฟเวอร์ ICE ของคุณ No comment provided by engineer. - - Your SMP servers - เซิร์ฟเวอร์ SMP ของคุณ - No comment provided by engineer. - Your SimpleX address ที่อยู่ SimpleX ของคุณ @@ -8254,6 +8207,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 ไม่สามารถดูโปรไฟล์ของคุณได้ @@ -8263,11 +8221,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 โปรไฟล์แบบสุ่มของคุณ @@ -8395,7 +8348,8 @@ Repeat connection request? blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -8562,7 +8516,7 @@ Repeat connection request? default (%@) ค่าเริ่มต้น (%@) delete after time - pref value +pref value default (no) @@ -8686,18 +8640,10 @@ 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. @@ -8901,8 +8847,8 @@ Repeat connection request? off ปิด enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9241,7 +9187,7 @@ last received msg: %2$@
- +
@@ -9277,7 +9223,7 @@ last received msg: %2$@
- +
@@ -9299,13 +9245,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9318,15 +9268,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9345,7 +9291,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 9b832b61fc..bbee40c2b9 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (kopyalanabilir) @@ -330,11 +314,6 @@ %u mesajlar atlandı. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (yeni) @@ -345,11 +324,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. @@ -415,11 +389,6 @@ \*kalın* No comment provided by engineer. - - , - , - No comment provided by engineer. - - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -456,11 +425,6 @@ - düzenleme geçmişi. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 saniye @@ -475,7 +439,7 @@ 1 day 1 gün delete after time - time interval +time interval 1 hour @@ -491,13 +455,13 @@ 1 month 1 ay delete after time - time interval +time interval 1 week 1 hafta delete after time - time interval +time interval 1 year @@ -528,11 +492,6 @@ 30 saniye No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -540,14 +499,6 @@ <p><a href="%@">SimpleX Chat ile bana bağlanın</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things Birkaç şey daha @@ -609,8 +560,8 @@ Accept Kabul et accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -631,7 +582,7 @@ Accept incognito Takma adla kabul et accept contact request via notification - swipe action +swipe action Accepted conditions @@ -789,8 +740,8 @@ Tüm konuşmalar ve mesajlar silinecektir. Bu, geri alınamaz! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. alert message @@ -837,6 +788,10 @@ All reports will be archived for you. No comment provided by engineer. + + All servers + No comment provided by engineer. + All your contacts will remain connected. Konuştuğun kişilerin tümü bağlı kalacaktır. @@ -1118,10 +1073,6 @@ Veritabanı arşivleniyor No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach Ekle @@ -1359,6 +1310,12 @@ Sohbet profiline göre (varsayılan) veya [bağlantıya göre](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! Arama çoktan bitti! @@ -1408,7 +1365,7 @@ Cancel İptal et alert action - alert button +alert button Cancel migration @@ -1498,7 +1455,7 @@ Change self-destruct passcode Kendini yok eden parolayı değiştir authentication reason - set passcode view +set passcode view Chat @@ -1737,16 +1694,6 @@ Kullanım koşulları No comment provided by engineer. - - Conditions will be accepted for enabled operators after 30 days. - Koşullar 30 gün sonra etkin operatörler tarafından kabul edilecektir. - No comment provided by engineer. - - - Conditions will be accepted for operator(s): **%@**. - Koşullar operatör(ler) için kabul edilecektir: **%@**. - No comment provided by engineer. - Conditions will be accepted for the operator(s): **%@**. Koşullar bu operatör(ler) için kabul edilecektir: **%@**. @@ -1767,6 +1714,10 @@ ICE sunucularını ayarla No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Onayla @@ -2357,7 +2308,7 @@ Bu senin kendi tek kullanımlık bağlantın! Delete Sil alert action - swipe action +swipe action Delete %lld messages of members? @@ -2844,7 +2795,7 @@ Bu senin kendi tek kullanımlık bağlantın! Download İndir alert button - chat item action +chat item action Download errors @@ -2921,9 +2872,8 @@ Bu senin kendi tek kullanımlık bağlantın! Etkinleştir (geçersiz kılmaları koru) No comment provided by engineer. - - Enable Flux - Flux'u Etkinleştir + + Enable Flux in Network & servers settings for better metadata privacy. No comment provided by engineer. @@ -3491,7 +3441,9 @@ Bu senin kendi tek kullanımlık bağlantın! Error: %@ Hata: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3606,9 +3558,9 @@ Bu senin kendi tek kullanımlık bağlantın! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5480,7 +5432,7 @@ VPN'nin etkinleştirilmesi gerekir. Open - No comment provided by engineer. + alert action Open Settings @@ -5510,23 +5462,15 @@ VPN'nin etkinleştirilmesi gerekir. Grubu aç No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device Başka bir cihaza açık geçiş authentication reason - - Open web link? - No comment provided by engineer. - Opening app… Uygulama açılıyor… @@ -5823,11 +5767,19 @@ Hata: %@ Privacy for your customers. No comment provided by engineer. + + Privacy policy and conditions of use. + No comment provided by engineer. + Privacy redefined Gizlilik yeniden tanımlandı No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + No comment provided by engineer. + Private filenames Gizli dosya adları @@ -6189,7 +6141,7 @@ Enable in *Network & servers* settings. Reject Reddet reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6314,8 +6266,8 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -6415,10 +6367,6 @@ Enable in *Network & servers* settings. Review conditions No comment provided by engineer. - - Review later - No comment provided by engineer. - Revoke İptal et @@ -6468,7 +6416,7 @@ Enable in *Network & servers* settings. Save Kaydet alert button - chat item action +chat item action Save (and notify contacts) @@ -7031,7 +6979,7 @@ Enable in *Network & servers* settings. Share Paylaş alert action - chat item action +chat item action Share 1-time link @@ -7090,6 +7038,10 @@ Enable in *Network & servers* settings. Kişilerle paylaş No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code QR kodunu göster @@ -7187,6 +7139,10 @@ Enable in *Network & servers* settings. SimpleX address or 1-time link? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX kişi adresi @@ -7290,7 +7246,7 @@ Enable in *Network & servers* settings. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7643,19 +7599,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir. No comment provided by engineer. - - The 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): **%@**. - No comment provided by engineer. - The second preset operator in the app! No comment provided by engineer. @@ -7767,6 +7714,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu senin kendi tek kullanımlık bağlantın! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Bu bağlantı başka bir mobil cihazda kullanıldı, lütfen masaüstünde yeni bir bağlantı oluşturun. @@ -8059,6 +8010,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Okunmamış swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. Yeni üyelere 100e kadar en son mesajlar gönderildi. @@ -8151,6 +8106,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Use TCP port %@ when no port is specified. No comment provided by engineer. + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Sohbeti kullan @@ -8213,6 +8172,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Use servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Görüşme sırasında uygulamayı kullanın. @@ -8504,10 +8467,6 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste XFTP sunucusu No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. Aynı veritabanını iki cihazda **kullanmamalısınız**. @@ -8599,10 +8558,6 @@ 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. No comment provided by engineer. @@ -8841,11 +8796,6 @@ 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 @@ -8931,6 +8881,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. @@ -8941,11 +8896,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 @@ -9081,7 +9031,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 @@ -9252,7 +9203,7 @@ Bağlantı isteği tekrarlansın mı? default (%@) varsayılan (%@) delete after time - pref value +pref value default (no) @@ -9379,20 +9330,11 @@ 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. - No comment provided by engineer. - forwarded iletildi @@ -9601,8 +9543,8 @@ Bağlantı isteği tekrarlansın mı? off kapalı enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9968,7 +9910,7 @@ son alınan msj: %2$@
- +
@@ -10005,7 +9947,7 @@ son alınan msj: %2$@
- +
@@ -10027,13 +9969,17 @@ son alınan msj: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -10046,15 +9992,11 @@ son alınan msj: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -10076,7 +10018,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 bb9ae7d3cd..c0375e3b02 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (можна скопіювати) @@ -330,11 +314,6 @@ %u повідомлень пропущено. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (новий) @@ -345,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. **Додати контакт**: створити нове посилання-запрошення. @@ -415,11 +389,6 @@ \*жирний* No comment provided by engineer. - - , - , - No comment provided by engineer. - - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -456,11 +425,6 @@ - історія редагування. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -475,7 +439,7 @@ 1 day 1 день delete after time - time interval +time interval 1 hour @@ -491,13 +455,13 @@ 1 month 1 місяць delete after time - time interval +time interval 1 week 1 тиждень delete after time - time interval +time interval 1 year @@ -528,11 +492,6 @@ 30 секунд No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -540,14 +499,6 @@ <p><a href="%@"> Зв'яжіться зі мною через SimpleX Chat</a></p> email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things Ще кілька речей @@ -609,8 +560,8 @@ Accept Прийняти accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions @@ -631,7 +582,7 @@ Accept incognito Прийняти інкогніто accept contact request via notification - swipe action +swipe action Accepted conditions @@ -789,8 +740,8 @@ Всі чати та повідомлення будуть видалені - це неможливо скасувати! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. alert message @@ -837,6 +788,10 @@ 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. Всі ваші контакти залишаться на зв'язку. @@ -1118,10 +1073,6 @@ Архівування бази даних No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach Прикріпити @@ -1359,6 +1310,12 @@ Через профіль чату (за замовчуванням) або [за з'єднанням](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! Дзвінок вже закінчився! @@ -1408,7 +1365,7 @@ Cancel Скасувати alert action - alert button +alert button Cancel migration @@ -1498,7 +1455,7 @@ Change self-destruct passcode Змінити пароль самознищення authentication reason - set passcode view +set passcode view Chat @@ -1737,16 +1694,6 @@ Умови використання 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): **%@**. Для оператора(ів) приймаються умови: **%@**. @@ -1767,6 +1714,10 @@ Налаштування серверів ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Підтвердити @@ -2357,7 +2308,7 @@ This is your own one-time link! Delete Видалити alert action - swipe action +swipe action Delete %lld messages of members? @@ -2845,7 +2796,7 @@ This is your own one-time link! Download Завантажити alert button - chat item action +chat item action Download errors @@ -2922,9 +2873,8 @@ 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. No comment provided by engineer. @@ -3492,7 +3442,9 @@ This is your own one-time link! Error: %@ Помилка: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3607,9 +3559,9 @@ This is your own one-time link! %@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. file error text @@ -5495,7 +5447,7 @@ Requires compatible VPN. Open Відкрито - No comment provided by engineer. + alert action Open Settings @@ -5527,23 +5479,15 @@ Requires compatible VPN. Відкрита група No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device Відкрита міграція на інший пристрій authentication reason - - Open web link? - No comment provided by engineer. - Opening app… Відкриваємо програму… @@ -5846,11 +5790,19 @@ 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 Приватні імена файлів @@ -6212,7 +6164,7 @@ Enable in *Network & servers* settings. Reject Відхилити reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6337,8 +6289,8 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -6439,11 +6391,6 @@ Enable in *Network & servers* settings. Умови перегляду No comment provided by engineer. - - Review later - Перегляньте пізніше - No comment provided by engineer. - Revoke Відкликати @@ -6493,7 +6440,7 @@ Enable in *Network & servers* settings. Save Зберегти alert button - chat item action +chat item action Save (and notify contacts) @@ -7060,7 +7007,7 @@ Enable in *Network & servers* settings. Share Поділіться alert action - chat item action +chat item action Share 1-time link @@ -7122,6 +7069,10 @@ Enable in *Network & servers* settings. Поділіться з контактами No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code Показати QR-код @@ -7222,6 +7173,10 @@ Enable in *Network & servers* settings. SimpleX адреса або одноразове посилання? No comment provided by engineer. + + SimpleX channel link + simplex link type + SimpleX contact address Контактна адреса SimpleX @@ -7327,7 +7282,7 @@ Enable in *Network & servers* settings. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7683,21 +7638,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! Другий попередньо встановлений оператор у застосунку! @@ -7812,6 +7757,10 @@ It can happen because of some bug or when the connection is compromised.Це ваше власне одноразове посилання! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі. @@ -8109,6 +8058,10 @@ To connect, please ask your contact to create another connection link and check Непрочитане swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. Новим користувачам надсилається до 100 останніх повідомлень. @@ -8202,6 +8155,10 @@ To connect, please ask your contact to create another connection link and check 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 Використовуйте чат @@ -8267,6 +8224,10 @@ To connect, please ask your contact to create another connection link and check Використовуйте сервери No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Використовуйте додаток під час розмови. @@ -8561,10 +8522,6 @@ To connect, please ask your contact to create another connection link and check XFTP-сервер No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. Ви **не повинні використовувати** одну і ту ж базу даних на двох пристроях. @@ -8657,11 +8614,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. Ви можете налаштувати сервери за допомогою налаштувань. @@ -8903,11 +8855,6 @@ Repeat connection request? Ваші сервери ICE No comment provided by engineer. - - Your SMP servers - Ваші SMP-сервери - No comment provided by engineer. - Your SimpleX address Ваша адреса SimpleX @@ -8993,6 +8940,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 не бачать ваш профіль. @@ -9003,11 +8955,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 Ваш випадковий профіль @@ -9145,7 +9092,8 @@ Repeat connection request? blocked by admin заблоковано адміністратором - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -9316,7 +9264,7 @@ Repeat connection request? default (%@) за замовчуванням (%@) delete after time - pref value +pref value default (no) @@ -9443,21 +9391,11 @@ 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 переслано @@ -9666,8 +9604,8 @@ Repeat connection request? off вимкнено enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -10034,7 +9972,7 @@ last received msg: %2$@
- +
@@ -10071,7 +10009,7 @@ last received msg: %2$@
- +
@@ -10093,7 +10031,7 @@ last received msg: %2$@
- +
@@ -10101,6 +10039,10 @@ last received msg: %2$@ %d нових подій notification body + + From %d chat(s) + notification body + From: %@ Від: %@ @@ -10116,16 +10058,11 @@ last received msg: %2$@ Нові повідомлення notification - - New messages in %d chats - Нові повідомлення в чатах %d - notification body -
- +
@@ -10147,7 +10084,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 ef62814aff..d5411f86e3 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -2,25 +2,9 @@
- +
- - - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - ( - No comment provided by engineer. - (can be copied) (可复制) @@ -118,10 +102,12 @@ %@ server + 服务器 No comment provided by engineer. %@ servers + 服务器 No comment provided by engineer. @@ -136,6 +122,7 @@ %1$@, %2$@ + %1$@, %2$@ format for date separator in chat @@ -160,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 @@ -181,6 +172,7 @@ %d messages not forwarded + 未转发 %d 条消息 alert title @@ -200,11 +192,12 @@ %d seconds(s) + %d 秒 delete after time %d skipped message(s) - %d 跳过消息 + 跳过的 %d 条消息 integrity error chat item @@ -322,11 +315,6 @@ 已跳过 %u 条消息。 No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (新) @@ -337,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. **添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接. @@ -379,6 +362,7 @@ **Scan / Paste link**: to connect via a link you received. + **扫描/粘贴链接**:用您收到的链接连接。 No comment provided by engineer. @@ -406,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). @@ -447,11 +426,6 @@ - 编辑消息历史。 No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 秒 @@ -466,7 +440,7 @@ 1 day 1天 delete after time - time interval +time interval 1 hour @@ -482,24 +456,27 @@ 1 month 1月 delete after time - time interval +time interval 1 week 1周 delete after time - time interval +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. @@ -517,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> @@ -529,14 +501,6 @@ <p><a href="%@">通过 SimpleX Chat </a></p>与我联系 email text - - @%@ - No comment provided by engineer. - - - @'%@' - No comment provided by engineer. - A few more things 一些杂项 @@ -586,6 +550,7 @@ About operators + 关于运营方 No comment provided by engineer. @@ -597,11 +562,12 @@ Accept 接受 accept contact request via notification - accept incoming call via notification - swipe action +accept incoming call via notification +swipe action Accept conditions + 接受条款 No comment provided by engineer. @@ -618,10 +584,11 @@ Accept incognito 接受隐身聊天 accept contact request via notification - swipe action +swipe action Accepted conditions + 已接受的条款 No comment provided by engineer. @@ -636,6 +603,7 @@ Active + 活跃 token status text @@ -650,10 +618,12 @@ Add friends + 添加好友 No comment provided by engineer. Add list + 添加列表 No comment provided by engineer. @@ -673,6 +643,7 @@ Add team members + 添加团队成员 No comment provided by engineer. @@ -682,6 +653,7 @@ Add to list + 添加到列表 No comment provided by engineer. @@ -691,14 +663,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. @@ -728,10 +703,12 @@ Address or 1-time link? + 地址还是一次性链接? No comment provided by engineer. Address settings + 地址设置 No comment provided by engineer. @@ -756,6 +733,7 @@ All + 全部 No comment provided by engineer. @@ -768,8 +746,9 @@ 所有聊天记录和消息将被删除——这一行为无法撤销! No comment provided by engineer. - - All chats will be removed from the list (text), and the list deleted. + + All chats will be removed from the list %@, and the list deleted. + 列表 %@ 和其中全部聊天将被删除。 alert message @@ -789,6 +768,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + 所有消息和文件均通过**端到端加密**发送;私信以量子安全方式发送。 No comment provided by engineer. @@ -813,6 +793,12 @@ All reports will be archived for you. + 将为你存档所有举报。 + No comment provided by engineer. + + + All servers + 全部服务器 No comment provided by engineer. @@ -892,6 +878,7 @@ Allow to report messsages to moderators. + 允许向 moderators 举报消息。 No comment provided by engineer. @@ -976,6 +963,7 @@ Another reason + 另一个理由 report reason @@ -1005,6 +993,7 @@ App group: + 应用组: No comment provided by engineer. @@ -1024,6 +1013,7 @@ App session + 应用会话 No comment provided by engineer. @@ -1053,14 +1043,17 @@ Archive + 存档 No comment provided by engineer. Archive %lld reports? + 存档 %lld 个举报? No comment provided by engineer. Archive all reports? + 存档所有举报? No comment provided by engineer. @@ -1075,14 +1068,17 @@ Archive report + 存档举报 No comment provided by engineer. Archive report? + 存档举报? No comment provided by engineer. Archive reports + 存档举报 swipe action @@ -1095,10 +1091,6 @@ 正在存档数据库 No comment provided by engineer. - - Ask - No comment provided by engineer. - Attach 附件 @@ -1161,6 +1153,7 @@ Auto-accept settings + 自动接受设置 alert title @@ -1190,6 +1183,7 @@ Better calls + 更佳的通话 No comment provided by engineer. @@ -1199,10 +1193,12 @@ Better groups performance + 更好的群性能 No comment provided by engineer. Better message dates. + 更好的消息日期。 No comment provided by engineer. @@ -1217,18 +1213,22 @@ 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. @@ -1313,14 +1313,17 @@ Business address + 企业地址 No comment provided by engineer. Business chats + 企业聊天 No comment provided by engineer. Businesses + 企业 No comment provided by engineer. @@ -1328,6 +1331,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! 通话已结束! @@ -1377,7 +1389,7 @@ Cancel 取消 alert action - alert button +alert button Cancel migration @@ -1416,10 +1428,12 @@ Change automatic message deletion? + 更改消息自动删除设置? alert title Change chat profiles + 更改聊天资料 authentication reason @@ -1466,18 +1480,21 @@ 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. @@ -1542,6 +1559,7 @@ Chat preferences were changed. + 聊天偏好设置已修改。 alert message @@ -1556,10 +1574,12 @@ 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. @@ -1569,10 +1589,12 @@ Check messages every 20 min. + 每 20 分钟检查消息。 No comment provided by engineer. Check messages when allowed. + 在被允许时检查消息。 No comment provided by engineer. @@ -1632,10 +1654,12 @@ Clear group? + 清除群? No comment provided by engineer. Clear or delete group? + 清除还是删除群? No comment provided by engineer. @@ -1660,6 +1684,7 @@ Community guidelines violation + 违反社区指导方针 report reason @@ -1679,38 +1704,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. 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. @@ -1718,6 +1742,11 @@ 配置 ICE 服务器 No comment provided by engineer. + + Configure server operators + 配置服务器运营方 + No comment provided by engineer. + Confirm 确认 @@ -1770,6 +1799,7 @@ Confirmed + 已确定 token status text @@ -1893,6 +1923,7 @@ This is your own one-time link! Connection blocked + 连接被阻止 No comment provided by engineer. @@ -1908,10 +1939,12 @@ This is your own one-time link! Connection is blocked by server operator: %@ + 连接被运营方 %@ 阻止 No comment provided by engineer. Connection not ready. + 连接未就绪。 No comment provided by engineer. @@ -1926,10 +1959,12 @@ This is your own one-time link! Connection requires encryption renegotiation. + 连接需要加密重协商。 No comment provided by engineer. Connection security + 连接安全性 No comment provided by engineer. @@ -2009,6 +2044,7 @@ This is your own one-time link! Content violates conditions of use + 内容违反使用条款 blocking reason @@ -2038,6 +2074,7 @@ This is your own one-time link! Corner + 拐角 No comment provided by engineer. @@ -2052,6 +2089,7 @@ This is your own one-time link! Create 1-time link + 创建一次性链接 No comment provided by engineer. @@ -2086,6 +2124,7 @@ This is your own one-time link! Create list + 创建列表 No comment provided by engineer. @@ -2145,6 +2184,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. @@ -2169,6 +2209,7 @@ This is your own one-time link! Customizable message shape. + 可自定义消息形状。 No comment provided by engineer. @@ -2303,7 +2344,7 @@ This is your own one-time link! Delete 删除 alert action - swipe action +swipe action Delete %lld messages of members? @@ -2342,10 +2383,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. @@ -2360,6 +2403,7 @@ This is your own one-time link! Delete chat? + 删除聊天? No comment provided by engineer. @@ -2439,6 +2483,7 @@ This is your own one-time link! Delete list? + 删除列表? alert title @@ -2473,6 +2518,7 @@ This is your own one-time link! Delete or moderate up to 200 messages. + 允许自行删除或管理员移除最多200条消息。 No comment provided by engineer. @@ -2492,6 +2538,7 @@ This is your own one-time link! Delete report + 删除举报 No comment provided by engineer. @@ -2531,6 +2578,7 @@ This is your own one-time link! Delivered even when Apple drops them. + 已送达,即使苹果已将其删除。 No comment provided by engineer. @@ -2615,7 +2663,7 @@ This is your own one-time link! Device authentication is disabled. Turning off SimpleX Lock. - 设备验证被禁用。关闭 SimpleX 锁定。 + 设备验证已禁用。 SimpleX 已解锁。 No comment provided by engineer. @@ -2635,11 +2683,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. @@ -2654,10 +2703,12 @@ This is your own one-time link! Disable automatic message deletion? + 禁用消息自动销毁? alert title Disable delete messages + 停用消息删除 alert button @@ -2747,10 +2798,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. @@ -2765,6 +2818,7 @@ This is your own one-time link! Don't miss important messages. + 不错过重要消息。 No comment provided by engineer. @@ -2774,6 +2828,7 @@ This is your own one-time link! Done + 完成 No comment provided by engineer. @@ -2785,7 +2840,7 @@ This is your own one-time link! Download 下载 alert button - chat item action +chat item action Download errors @@ -2804,6 +2859,7 @@ This is your own one-time link! Download files + 下载文件 alert action @@ -2838,6 +2894,7 @@ This is your own one-time link! E2E encrypted notifications. + 端到端加密的通知。 No comment provided by engineer. @@ -2860,8 +2917,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. @@ -3006,6 +3064,7 @@ This is your own one-time link! Encryption renegotiation in progress. + 正进行加密重协商。 No comment provided by engineer. @@ -3075,6 +3134,7 @@ This is your own one-time link! Error accepting conditions + 接受条款出错 alert title @@ -3089,6 +3149,7 @@ This is your own one-time link! Error adding server + 添加服务器出错 alert title @@ -3098,6 +3159,7 @@ This is your own one-time link! Error changing connection profile + 更改连接资料出错 No comment provided by engineer. @@ -3112,6 +3174,7 @@ This is your own one-time link! Error changing to incognito! + 切换至隐身聊天出错! No comment provided by engineer. @@ -3140,6 +3203,7 @@ This is your own one-time link! Error creating list + 创建列表出错 alert title @@ -3159,6 +3223,7 @@ This is your own one-time link! Error creating report + 创建举报出错 No comment provided by engineer. @@ -3243,10 +3308,12 @@ This is your own one-time link! Error loading servers + 加载服务器出错 alert title Error migrating settings + 迁移设置出错 No comment provided by engineer. @@ -3271,6 +3338,7 @@ This is your own one-time link! Error registering for notifications + 注册消息推送出错 alert title @@ -3280,6 +3348,7 @@ This is your own one-time link! Error reordering lists + 重排列表出错 alert title @@ -3294,6 +3363,7 @@ This is your own one-time link! Error saving chat list + 保存聊天列表出错 alert title @@ -3313,6 +3383,7 @@ This is your own one-time link! Error saving servers + 保存服务器出错 alert title @@ -3362,6 +3433,7 @@ This is your own one-time link! Error switching profile + 切换配置文件出错 No comment provided by engineer. @@ -3376,6 +3448,7 @@ This is your own one-time link! Error testing server connection + 检验服务器连接出错 No comment provided by engineer. @@ -3390,6 +3463,7 @@ This is your own one-time link! Error updating server + 更新服务器出错 alert title @@ -3420,7 +3494,9 @@ This is your own one-time link! Error: %@ 错误: %@ - alert message + alert message +file error text +snd error text Error: URL is invalid @@ -3439,6 +3515,7 @@ This is your own one-time link! Errors in servers configuration. + 服务器配置有错误。 servers error @@ -3458,6 +3535,7 @@ This is your own one-time link! Expired + 已过期 token status text @@ -3502,6 +3580,7 @@ This is your own one-time link! Faster deletion of groups. + 更快地删除群。 No comment provided by engineer. @@ -3511,6 +3590,7 @@ This is your own one-time link! Faster sending messages. + 更快发送消息。 No comment provided by engineer. @@ -3520,6 +3600,7 @@ This is your own one-time link! Favorites + 收藏 No comment provided by engineer. @@ -3530,11 +3611,15 @@ This is your own one-time link! File errors: %@ + 文件错误: +%@ alert message - + File is blocked by server operator: -(info.reason.text). +%@. + 文件被服务器运营方阻止: +%@。 file error text @@ -3664,10 +3749,12 @@ This is your own one-time link! For all moderators + 所有 moderators No comment provided by engineer. For chat profile %@: + 为聊天资料 %@: servers error @@ -3677,18 +3764,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. @@ -3698,6 +3789,7 @@ This is your own one-time link! Forward %d message(s)? + 转发 %d 条消息? alert title @@ -3707,14 +3799,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. @@ -3729,6 +3824,7 @@ This is your own one-time link! Forwarding %lld messages + 正在转发 %lld 条消息 No comment provided by engineer. @@ -3802,6 +3898,7 @@ Error: %2$@ Get notified when mentioned. + 被提及时收到通知。 No comment provided by engineer. @@ -3911,6 +4008,7 @@ Error: %2$@ Groups + No comment provided by engineer. @@ -3920,6 +4018,7 @@ Error: %2$@ Help admins moderating their groups. + 帮助管理员管理群组。 No comment provided by engineer. @@ -3974,14 +4073,17 @@ 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 @@ -4011,6 +4113,7 @@ Error: %2$@ IP address + IP 地址 No comment provided by engineer. @@ -4091,6 +4194,8 @@ Error: %2$@ Improved delivery, reduced traffic usage. More improvements are coming soon! + 改善传送,降低流量使用。 +更多改进即将推出! No comment provided by engineer. @@ -4125,10 +4230,12 @@ More improvements are coming soon! Inappropriate content + 不当内容 report reason Inappropriate profile + 不当个人资料 report reason @@ -4225,22 +4332,27 @@ More improvements are coming soon! 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 @@ -4305,6 +4417,7 @@ More improvements are coming soon! Invite to chat + 邀请加入聊天 No comment provided by engineer. @@ -4467,10 +4580,12 @@ This is your link for group %@! Leave chat + 离开聊天 No comment provided by engineer. Leave chat? + 离开聊天? No comment provided by engineer. @@ -4515,14 +4630,17 @@ This is your link for group %@! 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. @@ -4617,10 +4735,12 @@ This is your link for group %@! Member reports + 成员举报 chat feature Member role will be changed to "%@". All chat members will be notified. + 将变更成员角色为“%@”。所有成员都会收到通知。 No comment provided by engineer. @@ -4635,6 +4755,7 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! + 将从聊天中删除成员 - 此操作无法撤销! No comment provided by engineer. @@ -4654,6 +4775,7 @@ This is your link for group %@! Members can report messsages to moderators. + 成员可以向 moderators 举报消息。 No comment provided by engineer. @@ -4683,6 +4805,7 @@ This is your link for group %@! Mention members 👋 + 提及成员👋 No comment provided by engineer. @@ -4752,6 +4875,7 @@ This is your link for group %@! Message shape + 消息形状 No comment provided by engineer. @@ -4796,6 +4920,7 @@ This is your link for group %@! Messages in this chat will never be deleted. + 此聊天中的消息永远不会被删除。 alert message @@ -4810,6 +4935,7 @@ This is your link for group %@! Messages were deleted after you selected them. + 在你选中消息后这些消息已被删除。 alert message @@ -4899,6 +5025,7 @@ This is your link for group %@! More + 更多 swipe action @@ -4913,6 +5040,7 @@ This is your link for group %@! More reliable notifications + 更可靠的通知 No comment provided by engineer. @@ -4932,6 +5060,7 @@ This is your link for group %@! Mute all + 全部静音 notification label action @@ -4956,6 +5085,7 @@ This is your link for group %@! Network decentralization + 网络去中心化 No comment provided by engineer. @@ -4970,6 +5100,7 @@ This is your link for group %@! Network operator + 网络运营方 No comment provided by engineer. @@ -4984,6 +5115,7 @@ This is your link for group %@! New + token status text @@ -4993,10 +5125,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. @@ -5031,6 +5165,7 @@ This is your link for group %@! New events + 新事件 notification @@ -5060,6 +5195,7 @@ This is your link for group %@! New server + 新服务器 No comment provided by engineer. @@ -5074,14 +5210,17 @@ This is your link for group %@! No chats + 无聊天 No comment provided by engineer. No chats found + 找不到聊天 No comment provided by engineer. No chats in list %@ + 列表 %@ 中无聊天 No comment provided by engineer. @@ -5131,14 +5270,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 @@ -5148,10 +5290,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. @@ -5171,26 +5315,32 @@ 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. @@ -5205,6 +5355,7 @@ This is your link for group %@! Notes + 附注 No comment provided by engineer. @@ -5214,6 +5365,7 @@ This is your link for group %@! Nothing to forward! + 无可转发! alert title @@ -5228,14 +5380,17 @@ This is your link for group %@! Notifications error + 通知错误 alert title Notifications privacy + 通知隐私 No comment provided by engineer. Notifications status + 通知状态 alert title @@ -5293,6 +5448,7 @@ Requires compatible VPN. Only chat owners can change preferences. + 仅聊天所有人可更改首选项。 No comment provided by engineer. @@ -5322,10 +5478,12 @@ Requires compatible VPN. Only sender and moderators see it + 仅发送人和moderators能看到 No comment provided by engineer. Only you and moderators see it + 只有你和moderators能看到 No comment provided by engineer. @@ -5381,7 +5539,7 @@ Requires compatible VPN. Open 打开 - No comment provided by engineer. + alert action Open Settings @@ -5390,6 +5548,7 @@ Requires compatible VPN. Open changes + 打开更改 No comment provided by engineer. @@ -5404,6 +5563,7 @@ Requires compatible VPN. Open conditions + 打开条款 No comment provided by engineer. @@ -5411,23 +5571,15 @@ Requires compatible VPN. 打开群 No comment provided by engineer. - - Open link - No comment provided by engineer. - - - Open links from chat list - No comment provided by engineer. + + Open link? + alert title Open migration to another device 打开迁移到另一台设备 authentication reason - - Open web link? - No comment provided by engineer. - Opening app… 正在打开应用程序… @@ -5435,14 +5587,17 @@ Requires compatible VPN. Operator + 运营方 No comment provided by engineer. Operator server + 运营方服务器 alert title Or import archive file + 或者导入或者导入压缩文件 No comment provided by engineer. @@ -5467,10 +5622,12 @@ Requires compatible VPN. Or to share privately + 或者私下分享 No comment provided by engineer. Organize chats into lists + 将聊天组织到列表 No comment provided by engineer. @@ -5520,6 +5677,7 @@ Requires compatible VPN. Password + 密码 No comment provided by engineer. @@ -5720,11 +5878,21 @@ 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 私密文件名 @@ -6085,7 +6253,7 @@ Enable in *Network & servers* settings. Reject 拒绝 reject incoming call via notification - swipe action +swipe action Reject (sender NOT notified) @@ -6209,8 +6377,8 @@ Enable in *Network & servers* settings. Report violation: only group moderators will see it. report reason - - Report: (text.isEmpty ? reason.text : text) + + Report: %@ report in notification @@ -6308,25 +6476,22 @@ Enable in *Network & servers* settings. Review conditions - No comment provided by engineer. - - - Review later + 审阅条款 No comment provided by engineer. Revoke - 撤销 + 吊销 No comment provided by engineer. Revoke file - 撤销文件 + 吊销文件 cancel file action Revoke file? - 撤销文件? + 吊销文件? No comment provided by engineer. @@ -6336,7 +6501,7 @@ Enable in *Network & servers* settings. Run chat - 运行聊天程序 + 运行聊天 No comment provided by engineer. @@ -6362,7 +6527,7 @@ Enable in *Network & servers* settings. Save 保存 alert button - chat item action +chat item action Save (and notify contacts) @@ -6396,6 +6561,7 @@ Enable in *Network & servers* settings. Save list + 保存列表 No comment provided by engineer. @@ -6435,6 +6601,7 @@ Enable in *Network & servers* settings. Save your profile? + 保存您的个人资料? alert title @@ -6459,6 +6626,7 @@ Enable in *Network & servers* settings. Saving %lld messages + 正在保存 %lld 条消息 No comment provided by engineer. @@ -6920,7 +7088,7 @@ Enable in *Network & servers* settings. Share 分享 alert action - chat item action +chat item action Share 1-time link @@ -6978,6 +7146,10 @@ Enable in *Network & servers* settings. 与联系人分享 No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code 显示二维码 @@ -7035,6 +7207,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. @@ -7069,12 +7242,19 @@ 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 channel link + SimpleX 频道链接 + simplex link type + SimpleX contact address SimpleX 联系地址 @@ -7082,12 +7262,12 @@ Enable in *Network & servers* settings. SimpleX encrypted message or connection event - SimpleX 加密消息或连接项目 + SimpleX 加密的消息或连接事件 notification SimpleX group link - SimpleX 群组链接 + SimpleX 群链接 simplex link type @@ -7112,6 +7292,7 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. + SimpleX 协议由 Trail of Bits 审阅。 No comment provided by engineer. @@ -7146,6 +7327,7 @@ Enable in *Network & servers* settings. Some app settings were not migrated. + 部分应用设置未被迁移。 No comment provided by engineer. @@ -7176,7 +7358,7 @@ Enable in *Network & servers* settings. Spam blocking reason - report reason +report reason Square, circle, or anything in between. @@ -7526,19 +7708,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. @@ -7649,6 +7822,10 @@ It can happen because of some bug or when the connection is compromised.这是你自己的一次性链接! No comment provided by engineer. + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + This link was used with another mobile device, please create a new link on the desktop. 此链接已在其他移动设备上使用,请在桌面上创建新链接。 @@ -7939,6 +8116,10 @@ To connect, please ask your contact to create another connection link and check 未读 swipe action + + Unsupported connection link + No comment provided by engineer. + Up to 100 last messages are sent to new members. 给新成员发送了最多 100 条历史消息。 @@ -8030,6 +8211,10 @@ To connect, please ask your contact to create another connection link and check 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 使用聊天 @@ -8092,6 +8277,10 @@ To connect, please ask your contact to create another connection link and check Use servers No comment provided by engineer. + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. 通话时使用本应用. @@ -8382,10 +8571,6 @@ To connect, please ask your contact to create another connection link and check XFTP 服务器 No comment provided by engineer. - - Yes - No comment provided by engineer. - You **must not** use the same database on two devices. 您 **不得** 在两台设备上使用相同的数据库。 @@ -8477,10 +8662,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. @@ -8719,11 +8900,6 @@ Repeat connection request? 您的 ICE 服务器 No comment provided by engineer. - - Your SMP servers - 您的 SMP 服务器 - No comment provided by engineer. - Your SimpleX address 您的 SimpleX 地址 @@ -8806,6 +8982,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 服务器无法看到您的资料。 @@ -8815,11 +8996,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 您的随机资料 @@ -8955,7 +9131,8 @@ Repeat connection request? blocked by admin 由管理员封禁 - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold @@ -9126,7 +9303,7 @@ Repeat connection request? default (%@) 默认 (%@) delete after time - pref value +pref value default (no) @@ -9175,7 +9352,7 @@ Repeat connection request? duplicates - 复本 + 副本 No comment provided by engineer. @@ -9253,20 +9430,11 @@ 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 已转发 @@ -9475,8 +9643,8 @@ Repeat connection request? off 关闭 enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -9842,7 +10010,7 @@ last received msg: %2$@
- +
@@ -9879,7 +10047,7 @@ last received msg: %2$@
- +
@@ -9901,13 +10069,17 @@ last received msg: %2$@
- +
%d new events notification body + + From %d chat(s) + notification body + From: %@ notification body @@ -9920,15 +10092,11 @@ last received msg: %2$@ New messages notification - - New messages in %d chats - notification body -
- +
@@ -9950,7 +10118,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..3ea46ee364 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -109,7 +109,7 @@ %d skipped message(s) - %d錯過了訊息 + 錯過的 %d 則訊息 integrity error chat item @@ -124,17 +124,17 @@ %lld contact(s) selected - %lld 已選擇聯絡人(s) + 已選擇 %lld 個聯絡人 No comment provided by engineer. %lld file(s) with total size of %@ - %lld 檔案(s) 的總共大小為%@ + %lld 個檔案,總共大小 %@ No comment provided by engineer. %lld members - %lld 成員 + %lld 個成員 No comment provided by engineer. @@ -224,7 +224,7 @@ **Warning**: Instant push notifications require passphrase saved in Keychain. - **警告**:即時推送訊息通知需要數據庫的密碼儲存在資料庫中。 + **警告**:即時推送訊息通知需要將數據庫的密碼儲存在資料庫中。 No comment provided by engineer. @@ -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. @@ -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. @@ -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 @@ -4702,7 +4702,7 @@ Available in v5.1 %u messages failed to decrypt. - %u 訊息解密失敗。 + %u 則訊息解密失敗。 No comment provided by engineer. @@ -5152,7 +5152,7 @@ Available in v5.1 Tap to activate profile. - 點擊以激活配置檔案。 + 點擊以激活設定檔。 No comment provided by engineer. @@ -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 ba94463d61..176da2481e 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, _): @@ -818,15 +1034,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 { @@ -841,14 +1052,14 @@ func updateNetCfg() { } func apiGetActiveUser() -> User? { - let r = sendSimpleXCmd(.showActiveUser) + let r: APIResult = sendSimpleXCmd(NSEChatCommand.showActiveUser) logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)") switch r { - case let .activeUser(user): return user - case .chatCmdError(_, .error(.noActiveUser)): + case let .result(.activeUser(user)): return user + case .error(.error(.noActiveUser)): logger.debug("apiGetActiveUser sendSimpleXCmd no active user") return nil - case let .chatCmdError(_, err): + case let .error(err): logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))") return nil default: @@ -858,101 +1069,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? { @@ -969,38 +1172,32 @@ func autoReceiveFile(_ file: CIFile) -> ChatItem? { } func setNetworkConfig(_ cfg: NetCfg) throws { - let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg)) - if case .cmdOk = r { return } - throw r + let r: APIResult = sendSimpleXCmd(NSEChatCommand.apiSetNetworkConfig(networkConfig: cfg)) + if case .result(.cmdOk) = r { return } + throw r.unexpected } -struct 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 .sndFileConnection: .noNtf + case .rcvFileConnection: .noNtf + case let .userContactConnection(_, userContact): + userContact.groupId == nil + ? .connectionEvent(user, connEntity) + : .noNtf } } } 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 a57961c934..999bb3608f 100644 --- a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings @@ -10,6 +10,3 @@ /* notification */ "New messages" = "Nouveaux messages"; -/* notification body */ -"New messages in %d chats" = "Nouveaux messages dans %d chats"; - 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..12d1e01f1d 100644 --- a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings @@ -10,6 +10,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/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings index 6ba39ccc63..7205b37e7f 100644 --- a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings @@ -10,6 +10,3 @@ /* notification */ "New messages" = "Новые сообщения"; -/* notification body */ -"New messages in %d chats" = "Новые сообщения в %d разговоре(ах)"; - diff --git a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings index 69cc53bff1..ceace71e34 100644 --- a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings @@ -10,6 +10,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..3e901c73eb 100644 --- a/apps/ios/SimpleX SE/ShareAPI.swift +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -13,58 +13,58 @@ 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 + 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, live: false, @@ -72,32 +72,33 @@ func apiSendMessages( 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 +106,121 @@ 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, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + + var cmdString: String { + switch self { + case .showActiveUser: return "/u" + case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))" + case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" + case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" + case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" + case let .apiSetAppFilePaths(filesFolder, tempFolder, assetsFolder): + return "/set file paths \(encodeJSON(AppFilePaths(appFilesFolder: filesFolder, appTempFolder: tempFolder, appAssetsFolder: assetsFolder)))" + case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" + case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" + case let .apiCreateChatItems(noteFolderId, composedMessages): + let msgs = encodeJSON(composedMessages) + return "/_create *\(noteFolderId) json \(msgs)" + case let .apiSendMessages(type, id, live, ttl, composedMessages): + let msgs = encodeJSON(composedMessages) + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + } + } + + func ref(_ type: ChatType, _ id: Int64) -> String { + "\(type.rawValue)\(id)" + } +} + +enum SEChatResponse: Decodable, ChatAPIResult { + case activeUser(user: User) + case chatStarted + case chatRunning + case apiChats(user: UserRef, chats: [ChatData]) + case newChatItems(user: UserRef, chatItems: [AChatItem]) + case cmdOk(user_: UserRef?) + + var responseType: String { + switch self { + case .activeUser: "activeUser" + case .chatStarted: "chatStarted" + case .chatRunning: "chatRunning" + case .apiChats: "apiChats" + case .newChatItems: "newChatItems" + case .cmdOk: "cmdOk" + } + } + + var details: String { + switch self { + case let .activeUser(user): return String(describing: user) + case .chatStarted: return noDetails + case .chatRunning: return noDetails + case let .apiChats(u, chats): return withUser(u, String(describing: chats)) + case let .newChatItems(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case .cmdOk: return noDetails + } + } + + static func fallbackResult(_ type: String, _ json: NSDictionary) -> SEChatResponse? { + if type == "apiChats", let r = parseApiChats(json) { + .apiChats(user: r.user, chats: r.chats) + } else { + nil + } + } +} + +enum SEChatEvent: Decodable, ChatAPIResult { + case chatSuspended + case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) + case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) + case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) + case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + + var responseType: String { + switch self { + case .chatSuspended: "chatSuspended" + case .sndFileProgressXFTP: "sndFileProgressXFTP" + case .sndFileCompleteXFTP: "sndFileCompleteXFTP" + case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated" + case .sndFileError: "sndFileError" + case .sndFileWarning: "sndFileWarning" + } + } + + var details: String { + switch self { + case .chatSuspended: return noDetails + case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)") + case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .chatItemsStatusesUpdated(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + } + } +} diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index 21908026b2..12a775f85c 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -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, @@ -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 } diff --git a/apps/ios/SimpleX SE/ShareView.swift b/apps/ios/SimpleX SE/ShareView.swift index f2b9de9f72..07180ffa1b 100644 --- a/apps/ios/SimpleX SE/ShareView.swift +++ b/apps/ios/SimpleX SE/ShareView.swift @@ -160,7 +160,7 @@ struct ShareView: View { } } - @ViewBuilder private func linkPreview(_ linkPreview: LinkPreview) -> some View { + private func linkPreview(_ linkPreview: LinkPreview) -> some View { previewArea { HStack(alignment: .center, spacing: 8) { if let uiImage = imageFromBase64(linkPreview.image) { diff --git a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings index acdc621e0c..2fedf0e6f1 100644 --- a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -38,10 +38,10 @@ "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: %@"; @@ -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: %@"; diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 957f58d5f5..9326ae9abe 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -174,8 +174,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.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 */; }; @@ -242,6 +242,8 @@ 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 */ @@ -531,8 +533,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.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 = ""; }; @@ -643,6 +645,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 */ @@ -688,8 +692,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -774,8 +778,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.0.7-Er4xsZkxTrnBI7tHHcaHrY.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */, ); path = Libraries; sourceTree = ""; @@ -795,6 +799,7 @@ 5C764E87279CBC8E000C6508 /* Model */ = { isa = PBXGroup; children = ( + E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */, 5C764E88279CBCB3000C6508 /* ChatModel.swift */, 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, @@ -990,6 +995,7 @@ isa = PBXGroup; children = ( 5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */, + E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */, 5CDCAD472818589900503DA2 /* NotificationService.swift */, 5CDCAD492818589900503DA2 /* Info.plist */, 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */, @@ -1006,9 +1012,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 */, @@ -1534,6 +1540,7 @@ 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 */, @@ -1606,6 +1613,7 @@ buildActionMask = 2147483647; files = ( 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */, + E5DDBE702DC4217900A0EFF0 /* NSEAPITypes.swift in Sources */, 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1963,7 +1971,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1988,7 +1996,8 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; + OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -2012,7 +2021,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2037,7 +2046,8 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; + OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -2053,11 +2063,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2073,11 +2083,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2098,7 +2108,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2113,7 +2123,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2135,7 +2145,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2150,7 +2160,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2172,7 +2182,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2198,7 +2208,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2223,7 +2233,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2249,7 +2259,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2274,7 +2284,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2289,7 +2299,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2308,7 +2318,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 267; + CURRENT_PROJECT_VERSION = 282; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2323,7 +2333,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.3; + MARKETING_VERSION = 6.3.6; 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 5e5f047611..0dd3483fd7 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -46,7 +46,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio var cConfirm = confirm.rawValue.cString(using: .utf8)! // the last parameter of chat_migrate_init is used to return the pointer to chat controller let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, backgroundMode ? 1 : 0, &chatController)! - let dbRes = dbMigrationResult(fromCString(cjson)) + let dbRes = dbMigrationResult(dataFromCString(cjson)) let encrypted = dbKey != "" let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey) let result = (encrypted, keychainErr ? .errorKeychain : dbRes) @@ -63,7 +63,7 @@ public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation var cKey = dbKey.cString(using: .utf8)! var cConfirm = confirmation.rawValue.cString(using: .utf8)! let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &temporaryController)! - return (dbMigrationResult(fromCString(cjson)), temporaryController) + return (dbMigrationResult(dataFromCString(cjson)), temporaryController) } public func chatInitControllerRemovingDatabases() { @@ -110,27 +110,42 @@ public func resetChatCtrl() { migrationResult = nil } -public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> ChatResponse { - var c = cmd.cmdString.cString(using: .utf8)! - let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)! - return chatResponse(fromCString(cjson)) +@inline(__always) +public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> APIResult { + if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl) { + decodeAPIResult(d) + } else { + APIResult.error(.invalidJSON(json: nil)) + } +} + +@inline(__always) +public func sendSimpleXCmdStr(_ cmd: String, _ ctrl: chat_ctrl? = nil) -> Data? { + var c = cmd.cString(using: .utf8)! + return if let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c) { + dataFromCString(cjson) + } else { + nil + } } // in microseconds 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,34 @@ struct ParsedServerAddress: Decodable { var parseError: String } +@inline(__always) public func fromCString(_ c: UnsafeMutablePointer) -> String { let s = String.init(cString: c) free(c) return s } -public func chatResponse(_ s: String) -> ChatResponse { - let d = s.data(using: .utf8)! - // TODO is there a way to do it without copying the data? e.g: - // let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) - // let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) - do { - let r = try jsonDecoder.decode(APIResponse.self, from: d) - return r.resp - } catch { - logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") +@inline(__always) +public func dataFromCString(_ c: UnsafeMutablePointer) -> Data? { + let len = strlen(c) + if len > 0 { + return Data(bytesNoCopy: c, count: len, deallocator: .free) + } else { + free(c) + return nil } - - var type: String? - var json: String? - if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary { - if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 { - type = jResp.allKeys[0] as? String - if jResp.count == 2 && type == "_owsf" { - type = jResp.allKeys[1] as? String - } - if type == "apiChats" { - if let jApiChats = jResp["apiChats"] as? NSDictionary, - let user: UserRef = try? decodeObject(jApiChats["user"] as Any), - let jChats = jApiChats["chats"] as? NSArray { - let chats = jChats.map { jChat in - if let chatData = try? parseChatData(jChat) { - return chatData.0 - } - 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, navInfo) = try? parseChatData(jChat, jApiChat["navInfo"] as? NSDictionary) { - return .apiChat(user: user, chat: chat, navInfo: navInfo) - } - } 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 +221,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,11 +229,15 @@ private func errorJson(_ jDict: NSDictionary) -> String? { } } -func parseChatData(_ jChat: Any, _ jNavInfo: Any? = nil) throws -> (ChatData, NavigationInfo) { +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 = jNavInfo == nil ? NavigationInfo() : try decodeObject((jNavInfo as! NSDictionary)["navInfo"]!) + 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) { @@ -260,16 +246,18 @@ func parseChatData(_ jChat: Any, _ jNavInfo: Any? = nil) throws -> (ChatData, Na 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), 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) @@ -277,28 +265,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 { @@ -337,15 +349,15 @@ public enum MTRError: Decodable, Equatable { case different(appMigration: String, dbMigration: String) } -func dbMigrationResult(_ s: String) -> DBMigrationResult { - let d = s.data(using: .utf8)! -// TODO is there a way to do it without copying the data? e.g: -// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) -// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) - do { - return try jsonDecoder.decode(DBMigrationResult.self, from: d) - } catch let error { - logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") - return .unknown(json: s) +func dbMigrationResult(_ d: Data?) -> DBMigrationResult { + if let d { + do { + return try jsonDecoder.decode(DBMigrationResult.self, from: d) + } catch let error { + logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") + return .unknown(json: dataToString(d)) + } + } else { + return .unknown(json: "no data") } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 20c8785b3a..b8d2361ac8 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -13,1231 +13,162 @@ 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(chatId: ChatId, pagination: ChatPagination, search: String) - case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) - case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) - case apiCreateChatTag(tag: ChatTagData) - case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64]) - case apiDeleteChatTag(tagId: Int64) - case apiUpdateChatTag(tagId: Int64, tagData: ChatTagData) - case apiReorderChatTags(tagIds: [Int64]) - case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) - case apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String) - case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool) - case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) - case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64]) - case apiArchiveReceivedReports(groupId: Int64) - case apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode) - case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) - case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction) - case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64]) - case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) - case apiGetNtfToken - case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) - case apiVerifyToken(token: DeviceToken, nonce: String, code: String) - case apiCheckToken(token: DeviceToken) - case apiDeleteToken(token: DeviceToken) - case apiGetNtfConns(nonce: String, encNtfInfo: String) - case apiGetConnNtfMessages(connIds: [String]) - case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) - case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) - case apiJoinGroup(groupId: Int64) - case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole) - case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool) - case apiRemoveMembers(groupId: Int64, memberIds: [Int64]) - 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(chatId, pagination, search): return "/_get chat \(chatId) \(pagination.cmdString)" + - (search == "" ? "" : " search=\(search)") - case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)" - case let .apiSendMessages(type, id, live, ttl, composedMessages): - let msgs = encodeJSON(composedMessages) - let ttlStr = ttl != nil ? "\(ttl!)" : "default" - return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" - case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))" - case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id)) \(tagIds.map({ "\($0)" }).joined(separator: ","))" - case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)" - case let .apiUpdateChatTag(tagId, tagData): return "/_update tag \(tagId) \(encodeJSON(tagData))" - case let .apiReorderChatTags(tagIds): return "/_reorder tags \(tagIds.map({ "\($0)" }).joined(separator: ","))" - case let .apiCreateChatItems(noteFolderId, composedMessages): - let msgs = encodeJSON(composedMessages) - return "/_create *\(noteFolderId) json \(msgs)" - case let .apiReportMessage(groupId, chatItemId, reportReason, reportText): - return "/_report #\(groupId) \(chatItemId) reason=\(reportReason) \(reportText)" - case let .apiUpdateChatItem(type, id, itemId, um, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(um.cmdString)" - case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" - case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))" - case let .apiArchiveReceivedReports(groupId): return "/_archive reports #\(groupId)" - case let .apiDeleteReceivedReports(groupId, itemIds, mode): return "/_delete reports #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" - case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" - case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))" - case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))" - case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl): - let ttlStr = ttl != nil ? "\(ttl!)" : "default" - return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)" - case .apiGetNtfToken: return "/_ntf get " - case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" - case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" - case let .apiCheckToken(token): return "/_ntf check \(token.cmdString)" - case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" - case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)" - case let .apiGetConnNtfMessages(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 .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): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ","))" - 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 .apiArchiveReceivedReports: return "apiArchiveReceivedReports" - case .apiDeleteReceivedReports: return "apiDeleteReceivedReports" - case .apiChatItemReaction: return "apiChatItemReaction" - case .apiGetReactionMembers: return "apiGetReactionMembers" - case .apiPlanForwardChatItems: return "apiPlanForwardChatItems" - case .apiForwardChatItems: return "apiForwardChatItems" - case .apiGetNtfToken: return "apiGetNtfToken" - case .apiRegisterToken: return "apiRegisterToken" - case .apiVerifyToken: return "apiVerifyToken" - case .apiCheckToken: return "apiCheckToken" - case .apiDeleteToken: return "apiDeleteToken" - case .apiGetNtfConns: return "apiGetNtfConns" - case .apiGetConnNtfMessages: return "apiGetConnNtfMessages" - case .apiNewGroup: return "apiNewGroup" - case .apiAddMember: return "apiAddMember" - case .apiJoinGroup: return "apiJoinGroup" - case .apiMembersRole: return "apiMembersRole" - case .apiBlockMembersForAll: return "apiBlockMembersForAll" - case .apiRemoveMembers: return "apiRemoveMembers" - case .apiLeaveGroup: return "apiLeaveGroup" - case .apiListMembers: return "apiListMembers" - case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" - case .apiCreateGroupLink: return "apiCreateGroupLink" - case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" - case .apiDeleteGroupLink: return "apiDeleteGroupLink" - case .apiGetGroupLink: return "apiGetGroupLink" - case .apiCreateMemberContact: return "apiCreateMemberContact" - case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" - case .apiTestProtoServer: return "apiTestProtoServer" - case .apiGetServerOperators: return "apiGetServerOperators" - case .apiSetServerOperators: return "apiSetServerOperators" - case .apiGetUserServers: return "apiGetUserServers" - case .apiSetUserServers: return "apiSetUserServers" - case .apiValidateServers: return "apiValidateServers" - case .apiGetUsageConditions: return "apiGetUsageConditions" - case .apiSetConditionsNotified: return "apiSetConditionsNotified" - case .apiAcceptConditions: return "apiAcceptConditions" - case .apiSetChatItemTTL: return "apiSetChatItemTTL" - case .apiGetChatItemTTL: return "apiGetChatItemTTL" - case .apiSetChatTTL: return "apiSetChatTTL" - case .apiSetNetworkConfig: return "apiSetNetworkConfig" - case .apiGetNetworkConfig: return "apiGetNetworkConfig" - case .apiSetNetworkInfo: return "apiSetNetworkInfo" - case .reconnectAllServers: return "reconnectAllServers" - case .reconnectServer: return "reconnectServer" - case .apiSetChatSettings: return "apiSetChatSettings" - case .apiSetMemberSettings: return "apiSetMemberSettings" - case .apiContactInfo: return "apiContactInfo" - case .apiGroupMemberInfo: return "apiGroupMemberInfo" - case .apiContactQueueInfo: return "apiContactQueueInfo" - case .apiGroupMemberQueueInfo: return "apiGroupMemberQueueInfo" - case .apiSwitchContact: return "apiSwitchContact" - case .apiSwitchGroupMember: return "apiSwitchGroupMember" - case .apiAbortSwitchContact: return "apiAbortSwitchContact" - case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember" - case .apiSyncContactRatchet: return "apiSyncContactRatchet" - case .apiSyncGroupMemberRatchet: return "apiSyncGroupMemberRatchet" - case .apiGetContactCode: return "apiGetContactCode" - case .apiGetGroupMemberCode: return "apiGetGroupMemberCode" - case .apiVerifyContact: return "apiVerifyContact" - case .apiVerifyGroupMember: return "apiVerifyGroupMember" - case .apiAddContact: return "apiAddContact" - case .apiSetConnectionIncognito: return "apiSetConnectionIncognito" - case .apiChangeConnectionUser: return "apiChangeConnectionUser" - case .apiConnectPlan: return "apiConnectPlan" - case .apiConnect: return "apiConnect" - case .apiDeleteChat: return "apiDeleteChat" - case .apiClearChat: return "apiClearChat" - case .apiListContacts: return "apiListContacts" - case .apiUpdateProfile: return "apiUpdateProfile" - case .apiSetContactPrefs: return "apiSetContactPrefs" - case .apiSetContactAlias: return "apiSetContactAlias" - case .apiSetGroupAlias: return "apiSetGroupAlias" - case .apiSetConnectionAlias: return "apiSetConnectionAlias" - case .apiSetUserUIThemes: return "apiSetUserUIThemes" - case .apiSetChatUIThemes: return "apiSetChatUIThemes" - case .apiCreateMyAddress: return "apiCreateMyAddress" - case .apiDeleteMyAddress: return "apiDeleteMyAddress" - case .apiShowMyAddress: return "apiShowMyAddress" - case .apiSetProfileAddress: return "apiSetProfileAddress" - case .apiAddressAutoAccept: return "apiAddressAutoAccept" - case .apiAcceptContact: return "apiAcceptContact" - case .apiRejectContact: return "apiRejectContact" - case .apiSendCallInvitation: return "apiSendCallInvitation" - case .apiRejectCall: return "apiRejectCall" - case .apiSendCallOffer: return "apiSendCallOffer" - case .apiSendCallAnswer: return "apiSendCallAnswer" - case .apiSendCallExtraInfo: return "apiSendCallExtraInfo" - case .apiEndCall: return "apiEndCall" - case .apiGetCallInvitations: return "apiGetCallInvitations" - case .apiCallStatus: return "apiCallStatus" - case .apiGetNetworkStatuses: return "apiGetNetworkStatuses" - case .apiChatRead: return "apiChatRead" - case .apiChatItemsRead: return "apiChatItemsRead" - case .apiChatUnread: return "apiChatUnread" - case .receiveFile: return "receiveFile" - case .setFileToReceive: return "setFileToReceive" - case .cancelFile: return "cancelFile" - case .setLocalDeviceName: return "setLocalDeviceName" - case .connectRemoteCtrl: return "connectRemoteCtrl" - case .findKnownRemoteCtrl: return "findKnownRemoteCtrl" - case .confirmRemoteCtrl: return "confirmRemoteCtrl" - case .verifyRemoteCtrlSession: return "verifyRemoteCtrlSession" - case .listRemoteCtrls: return "listRemoteCtrls" - case .stopRemoteCtrl: return "stopRemoteCtrl" - case .deleteRemoteCtrl: return "deleteRemoteCtrl" - case .apiUploadStandaloneFile: return "apiUploadStandaloneFile" - case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" - case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" - case .showVersion: return "showVersion" - case .getAgentSubsTotal: return "getAgentSubsTotal" - case .getAgentServersSummary: return "getAgentServersSummary" - case .resetAgentServersStats: return "resetAgentServersStats" - case .string: return "console command" - } - } - } - - func ref(_ type: ChatType, _ id: Int64) -> String { - "\(type.rawValue)\(id)" - } - - func joinedIds(_ ids: [Int64]) -> String { - ids.map { "\($0)" }.joined(separator: ",") - } - - func chatItemTTLStr(seconds: Int64?) -> String { - if let seconds = seconds { - return String(seconds) - } else { - return "default" - } - } - - 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, 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 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 userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [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 membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole) - case memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool) - case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [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 enum APIResult: Decodable where R: Decodable, R: ChatAPIResult { + case result(R) + case error(ChatError) + case invalid(type: String, json: Data) + 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 .userDeletedMembers: return "userDeletedMembers" - 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 .membersRoleUser: return "membersRoleUser" - case .memberBlockedForAll: return "memberBlockedForAll" - case .membersBlockedForAllUser: return "membersBlockedForAllUser" - 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" + 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)) +} - 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, 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 .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 .userDeletedMembers(u, groupInfo, members): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)") - 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 .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)") - case let .memberBlockedForAll(u, groupInfo, byMember, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nblocked: \(blocked)") - case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\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) +// 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)) } - } - - 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 + return (user, chats) + } else { + return nil } } -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 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 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 { - public static let INITIAL_COUNT = 75 - public static let PRELOAD_COUNT = 100 - public 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)" - } - } -} - -public struct ChatTagData: Encodable { - public var emoji: String? - public var text: String - - public init(emoji: String?, text: String) { - self.emoji = emoji - self.text = text - } +public func simplexChatLink(_ uri: String) -> String { + uri.starts(with: "simplex:/") + ? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") + : uri } public struct ComposedMessage: Encodable { @@ -1254,477 +185,11 @@ public struct ComposedMessage: Encodable { } } -public struct UpdatedMessage: Encodable { - public var msgContent: MsgContent - public var mentions: [String: Int64] - - public init(msgContent: MsgContent, mentions: [String: Int64] = [:]) { - self.msgContent = msgContent - self.mentions = mentions - } - - var cmdString: String { - "json \(encodeJSON(self))" - } -} - -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] @@ -1775,7 +240,7 @@ 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 smpWebPortServers: SMPWebPortServers = .preset public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpTimeoutPerKb: Int // microseconds @@ -1871,6 +336,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 @@ -1990,56 +469,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? @@ -2105,16 +534,6 @@ public enum MsgFilter: String, Codable, Hashable { } } -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 struct ConnectionStats: Decodable, Hashable { public var connAgentVersion: Int public var rcvQueuesInfo: [RcvQueueInfo] @@ -2182,108 +601,11 @@ 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 struct AutoAccept: Codable, Hashable { - public var businessAddress: Bool - public var acceptIncognito: Bool - public var autoReply: MsgContent? - - public init(businessAddress: Bool, acceptIncognito: Bool, autoReply: MsgContent? = nil) { - self.businessAddress = businessAddress - self.acceptIncognito = acceptIncognito - self.autoReply = autoReply - } - - static func cmdString(_ autoAccept: AutoAccept?) -> String { - guard let autoAccept = autoAccept else { return "off" } - var s = "on" - if autoAccept.acceptIncognito { - s += " incognito=on" - } else if autoAccept.businessAddress { - s += " business" - } - guard let msg = autoAccept.autoReply else { return s } - return s + " " + msg.cmdString - } -} - public protocol SelectableItem: Identifiable, Equatable { var label: LocalizedStringKey { get } static var values: [Self] { get } } -public struct DeviceToken: Decodable { - var pushProvider: PushProvider - var token: String - - public init(pushProvider: PushProvider, token: String) { - self.pushProvider = pushProvider - self.token = token - } - - public var cmdString: String { - "\(pushProvider) \(token)" - } -} - -public enum PushEnvironment: String { - case development - case production -} - -public enum PushProvider: String, Decodable { - case apns_dev - case apns_prod - - public init(env: PushEnvironment) { - switch env { - case .development: self = .apns_dev - case .production: self = .apns_prod - } - } -} - -// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable, -// and .local for periodic background checks -public enum NotificationsMode: String, Decodable, SelectableItem { - case off = "OFF" - case periodic = "PERIODIC" - case instant = "INSTANT" - - public var label: LocalizedStringKey { - switch self { - case .off: "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 @@ -2302,63 +624,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) @@ -2375,13 +640,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 { @@ -2404,8 +682,8 @@ 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 contactNotReady(contact: Contact) case contactNotActive(contact: Contact) @@ -2521,6 +799,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 { @@ -2710,425 +989,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 45a05a1cd6..29ccab7357 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -40,7 +40,7 @@ let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode" let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback" -let GROUP_DEFAULT_NETWORK_SMP_WEB_PORT = "networkSMPWebPort" +let GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS = "networkSMPWebPortServers" let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" @@ -72,7 +72,7 @@ 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_SMP_WEB_PORT: false, + GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS: SMPWebPortServers.preset.rawValue, GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, @@ -251,6 +251,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) @@ -305,12 +311,14 @@ public class EnumDefault where T.RawValue == String { } public class BoolDefault: Default { + @inline(__always) public func get() -> Bool { self.defaults.bool(forKey: self.key) } } public class IntDefault: Default { + @inline(__always) public func get() -> Int { self.defaults.integer(forKey: self.key) } @@ -320,11 +328,13 @@ public class Default { var defaults: UserDefaults var key: String + @inline(__always) public init(defaults: UserDefaults = UserDefaults.standard, forKey: String) { self.defaults = defaults self.key = forKey } + @inline(__always) public func set(_ value: T) { defaults.set(value, forKey: key) defaults.synchronize() @@ -338,7 +348,7 @@ public func getNetCfg() -> NetCfg { let sessionMode = networkSessionModeGroupDefault.get() let smpProxyMode = networkSMPProxyModeGroupDefault.get() let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get() - let smpWebPort = groupDefaults.bool(forKey: GROUP_DEFAULT_NETWORK_SMP_WEB_PORT) + let smpWebPortServers = networkSMPWebPortServersDefault.get() let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) @@ -362,7 +372,7 @@ public func getNetCfg() -> NetCfg { sessionMode: sessionMode, smpProxyMode: smpProxyMode, smpProxyFallback: smpProxyFallback, - smpWebPort: smpWebPort, + smpWebPortServers: smpWebPortServers, tcpConnectTimeout: tcpConnectTimeout, tcpTimeout: tcpTimeout, tcpTimeoutPerKb: tcpTimeoutPerKb, @@ -381,7 +391,7 @@ 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.smpWebPort, forKey: GROUP_DEFAULT_NETWORK_SMP_WEB_PORT) + networkSMPWebPortServersDefault.set(cfg.smpWebPortServers) groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 468bc2ea8f..88246465e1 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1201,7 +1201,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { 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") @@ -1333,6 +1333,19 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { + get { + switch self { + case let .direct(contact): return contact.userCantSendReason + case let .group(groupInfo): return groupInfo.userCantSendReason + case let .local(noteFolder): return noteFolder.userCantSendReason + case let .contactRequest(contactRequest): return contactRequest.userCantSendReason + case let .contactConnection(contactConnection): return contactConnection.userCantSendReason + case .invalidJSON: return ("can't send messages", nil) + } + } + } + public var sendMsgEnabled: Bool { get { switch self { @@ -1589,7 +1602,7 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike { self.chatStats = chatStats } - public static func invalidJSON(_ json: String) -> ChatData { + public static func invalidJSON(_ json: Data?) -> ChatData { ChatData( chatInfo: .invalidJSON(json: json), chatItems: [], @@ -1642,15 +1655,16 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { 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 userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { + // TODO [short links] this will have additional statuses for pending contact requests before they are accepted + if nextSendGrpInv { return nil } + if !active { return ("contact deleted", nil) } + if !sndReady { return ("contact not ready", nil) } + if activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) } + if activeConn?.connDisabled ?? true { return ("contact disabled", nil) } + return nil + } + public var sendMsgEnabled: Bool { userCantSendReason == nil } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } @@ -1829,6 +1843,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable { public var id: ChatId { get { "<@\(contactRequestId)" } } public var apiId: Int64 { get { contactRequestId } } var ready: Bool { get { true } } + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { ("can't send messages", nil) } public var sendMsgEnabled: Bool { get { false } } public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } @@ -1853,7 +1868,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 @@ -1861,6 +1876,7 @@ 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 userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { ("can't send messages", nil) } public var sendMsgEnabled: Bool { get { false } } var localDisplayName: String { get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) } @@ -1990,7 +2006,20 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { 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 userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { + return if membership.memberActive { + membership.memberRole == .observer ? ("you are observer", "Please contact group admin.") : nil + } else { + switch membership.memberStatus { + case .memRejected: ("request to join rejected", nil) + case .memGroupDeleted: ("group is deleted", nil) + case .memRemoved: ("removed from group", nil) + case .memLeft: ("you left", nil) + default: ("can't send messages", nil) + } + } + } + public var sendMsgEnabled: Bool { userCantSendReason == nil } public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } public var image: String? { get { groupProfile.image } } @@ -2357,6 +2386,7 @@ 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 userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { nil } public var sendMsgEnabled: Bool { get { true } } public var displayName: String { get { ChatInfo.privateNotesChatName } } public var fullName: String { get { "" } } @@ -2399,16 +2429,29 @@ public enum ConnectionEntity: Decodable, Hashable { 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 + userContact.id default: - return nil + nil } } + + // public var localDisplayName: String? { + // switch self { + // case let .rcvDirectMsgConnection(conn, contact): + // if let name = contact?.localDisplayName { "@\(name)" } else { conn.id } + // case let .rcvGroupMsgConnection(_, g, m): + // "#\(g.localDisplayName) @\(m.localDisplayName)" + // case let .userContactConnection(_, userContact): + // userContact.id + // default: + // nil + // } + // } public var conn: Connection { switch self { @@ -2422,10 +2465,11 @@ public enum ConnectionEntity: Decodable, Hashable { } 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 { @@ -2433,6 +2477,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? @@ -2881,7 +2963,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, @@ -3328,7 +3410,7 @@ public enum CIContent: Decodable, ItemContent, Hashable { case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo) case sndGroupE2EEInfo(e2eeInfo: E2EEInfo) case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo) - case invalidJSON(json: String) + case invalidJSON(json: Data?) public var text: String { get { @@ -3827,7 +3909,7 @@ 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) @@ -3911,7 +3993,7 @@ public enum MsgContent: Equatable, Hashable { } } - var cmdString: String { + public var cmdString: String { "json \(encodeJSON(self))" } @@ -4063,12 +4145,14 @@ public enum SimplexLinkType: String, Decodable, Hashable { case contact case invitation case group + case channel public var description: String { switch self { case .contact: return NSLocalizedString("SimpleX contact address", comment: "simplex link type") case .invitation: return NSLocalizedString("SimpleX one-time invitation", comment: "simplex link type") case .group: return NSLocalizedString("SimpleX group link", comment: "simplex link type") + case .channel: return NSLocalizedString("SimpleX channel link", comment: "simplex link type") } } } @@ -4083,18 +4167,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 } } } @@ -4119,6 +4201,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 { diff --git a/apps/ios/SimpleXChat/CryptoFile.swift b/apps/ios/SimpleXChat/CryptoFile.swift index 0e539ba97c..dfe833f832 100644 --- a/apps/ios/SimpleXChat/CryptoFile.swift +++ b/apps/ios/SimpleXChat/CryptoFile.swift @@ -18,10 +18,10 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs { memcpy(ptr, (data as NSData).bytes, data.count) var cPath = path.cString(using: .utf8)! let cjson = chat_write_file(getChatCtrl(), &cPath, ptr, Int32(data.count))! - let d = fromCString(cjson).data(using: .utf8)! + let d = dataFromCString(cjson)! // TODO [unsafe] switch try jsonDecoder.decode(WriteFileResult.self, from: d) { case let .result(cfArgs): return cfArgs - case let .error(err): throw RuntimeError(err) + case let .error(err): throw RuntimeError(err) // TODO [unsafe] } } @@ -51,10 +51,10 @@ public func encryptCryptoFile(fromPath: String, toPath: String) throws -> Crypto var cFromPath = fromPath.cString(using: .utf8)! var cToPath = toPath.cString(using: .utf8)! let cjson = chat_encrypt_file(getChatCtrl(), &cFromPath, &cToPath)! - let d = fromCString(cjson).data(using: .utf8)! + let d = dataFromCString(cjson)! // TODO [unsafe] switch try jsonDecoder.decode(WriteFileResult.self, from: d) { case let .result(cfArgs): return cfArgs - case let .error(err): throw RuntimeError(err) + case let .error(err): throw RuntimeError(err) // TODO [unsafe] } } diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift index d0bf5eeb6e..a433d2313b 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -37,22 +37,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("\(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))") } } } @@ -94,22 +90,21 @@ 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)): + 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 } } diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 97cc633115..5579449caa 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -205,7 +205,7 @@ func hideSecrets(_ cItem: ChatItem) -> String { } else { let mc = cItem.content.msgContent if case let .report(text, reason) = mc { - return NSLocalizedString("Report: \(text.isEmpty ? reason.text : text)", comment: "report in notification") + return String.localizedStringWithFormat(NSLocalizedString("Report: %@", comment: "report in notification"), text.isEmpty ? reason.text : text) } else { return cItem.text } 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 b697b58222..e4bc8f2150 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)"; @@ -263,7 +242,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 ден"; /* time interval */ @@ -273,11 +252,11 @@ "1 minute" = "1 минута"; /* delete after time - time interval */ +time interval */ "1 month" = "1 месец"; /* delete after time - time interval */ +time interval */ "1 week" = "1 седмица"; /* No comment provided by engineer. */ @@ -329,8 +308,8 @@ "Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Приеми"; /* No comment provided by engineer. */ @@ -343,7 +322,7 @@ "Accept contact request from %@?" = "Приемане на заявка за контакт от %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Приеми инкогнито"; /* call status */ @@ -739,7 +718,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. */ @@ -815,7 +795,7 @@ "Can't invite contacts!" = "Не може да поканят контактите!"; /* alert action - alert button */ +alert button */ "Cancel" = "Отказ"; /* No comment provided by engineer. */ @@ -867,7 +847,7 @@ "Change self-destruct mode" = "Промени режима на самоунищожение"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Промени кода за достъп за самоунищожение"; /* chat item text */ @@ -1273,7 +1253,7 @@ "Decryption error" = "Грешка при декриптиране"; /* delete after time - pref value */ +pref value */ "default (%@)" = "по подразбиране (%@)"; /* No comment provided by engineer. */ @@ -1283,7 +1263,7 @@ "default (yes)" = "по подразбиране (да)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Изтрий"; /* No comment provided by engineer. */ @@ -1512,7 +1492,7 @@ "Downgrade and open chat" = "Понижи версията и отвори чата"; /* alert button - chat item action */ +chat item action */ "Download" = "Изтегли"; /* No comment provided by engineer. */ @@ -1866,7 +1846,9 @@ /* No comment provided by engineer. */ "Error: " = "Грешка: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Грешка: %@"; /* No comment provided by engineer. */ @@ -1878,9 +1860,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Дори когато е деактивиран в разговора."; -/* No comment provided by engineer. */ -"event happened" = "събитие се случи"; - /* No comment provided by engineer. */ "Exit without saving" = "Изход без запазване"; @@ -2767,8 +2746,8 @@ "observer" = "наблюдател"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "изключено"; /* blur media */ @@ -2846,7 +2825,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Само вашият контакт може да изпраща гласови съобщения."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Отвори"; /* No comment provided by engineer. */ @@ -3153,7 +3132,7 @@ "Reduced battery usage" = "Намалена консумация на батерията"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Отхвърляне"; /* No comment provided by engineer. */ @@ -3280,7 +3259,7 @@ "Safer groups" = "По-безопасни групи"; /* alert button - chat item action */ +chat item action */ "Save" = "Запази"; /* alert button */ @@ -3536,7 +3515,7 @@ "Shape profile images" = "Променете формата на профилните изображения"; /* alert action - chat item action */ +chat item action */ "Share" = "Сподели"; /* No comment provided by engineer. */ @@ -3798,7 +3777,7 @@ "The old database was not removed during the migration, it can be deleted." = "Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Профилът се споделя само с вашите контакти."; +"Your profile is stored on your device and only shared with your contacts." = "Профилът се споделя само с вашите контакти."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Втората отметка, която пропуснахме! ✅"; @@ -4484,6 +4463,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Вашият адрес в SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Вашите SMP сървъри"; - diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index eef7930c6f..08a94615a3 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)"; @@ -19,27 +13,12 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 5 minutové hlasové zprávy.\n- vlastní čas mizení.\n- historie úprav."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 barevný!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(this device v%@)" = "(toto zařízení v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* 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)"; @@ -145,6 +124,18 @@ /* time interval */ "%d days" = "%d dní"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d soubor(y) stále stahován(y)."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d soubor(y) se nepodařilo stáhnout."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d soubor(y) smazán(y)."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d soubor(y) nestažen(y)."; + /* time interval */ "%d hours" = "%d hodin"; @@ -236,7 +227,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 den"; /* time interval */ @@ -246,11 +237,11 @@ "1 minute" = "1 minutu"; /* delete after time - time interval */ +time interval */ "1 month" = "1 měsíc"; /* delete after time - time interval */ +time interval */ "1 week" = "1 týden"; /* No comment provided by engineer. */ @@ -293,8 +284,8 @@ "above, then choose:" = "výše, pak vyberte:"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Přijmout"; /* No comment provided by engineer. */ @@ -304,7 +295,7 @@ "Accept contact request from %@?" = "Přijmout žádost o kontakt od %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Přijmout inkognito"; /* call status */ @@ -569,7 +560,7 @@ "Can't invite contacts!" = "Nelze pozvat kontakty!"; /* alert action - alert button */ +alert button */ "Cancel" = "Zrušit"; /* feature offered item */ @@ -609,7 +600,7 @@ "Change self-destruct mode" = "Změnit režim sebedestrukce"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Změnit sebedestrukční heslo"; /* chat item text */ @@ -831,6 +822,9 @@ /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Vytvořit nový profil v [desktop app](https://simplex.chat/downloads/). 💻"; +/* No comment provided by engineer. */ +"Create profile" = "Vytvořte si profil"; + /* server test step */ "Create queue" = "Vytvořit frontu"; @@ -931,7 +925,7 @@ "Decryption error" = "Chyba dešifrování"; /* delete after time - pref value */ +pref value */ "default (%@)" = "výchozí (%@)"; /* No comment provided by engineer. */ @@ -941,7 +935,7 @@ "default (yes)" = "výchozí (ano)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Smazat"; /* No comment provided by engineer. */ @@ -1430,7 +1424,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. */ @@ -2148,8 +2144,8 @@ "observer" = "pozorovatel"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "vypnuto"; /* blur media */ @@ -2224,7 +2220,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Hlasové zprávy může odesílat pouze váš kontakt."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Otevřít"; /* No comment provided by engineer. */ @@ -2456,7 +2452,7 @@ "Reduced battery usage" = "Snížení spotřeby baterie"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Odmítnout"; /* No comment provided by engineer. */ @@ -2556,7 +2552,7 @@ "Run chat" = "Spustit chat"; /* alert button - chat item action */ +chat item action */ "Save" = "Uložit"; /* alert button */ @@ -2770,7 +2766,7 @@ "Settings" = "Nastavení"; /* alert action - chat item action */ +chat item action */ "Share" = "Sdílet"; /* No comment provided by engineer. */ @@ -2993,7 +2989,7 @@ "The old database was not removed during the migration, it can be deleted." = "Stará databáze nebyla během přenášení odstraněna, lze ji smazat."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty."; +"Your profile is stored on your device and only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Druhé zaškrtnutí jsme přehlédli! ✅"; @@ -3493,6 +3489,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Vaše SimpleX adresa"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Vaše servery SMP"; - diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index eb3238b43b..8da7835c43 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (kann kopiert werden)"; @@ -25,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)"; @@ -58,12 +34,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Stern auf GitHub vergeben](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"@'%@'" = "@'%@'"; - -/* 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."; @@ -299,7 +269,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "Älter als ein Tag"; /* time interval */ @@ -309,11 +279,11 @@ "1 minute" = "1 Minute"; /* delete after time - time interval */ +time interval */ "1 month" = "Älter als ein Monat"; /* delete after time - time interval */ +time interval */ "1 week" = "Älter als eine Woche"; /* delete after time */ @@ -371,8 +341,8 @@ "Accent" = "Akzent"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Annehmen"; /* No comment provided by engineer. */ @@ -385,7 +355,7 @@ "Accept contact request from %@?" = "Die Kontaktanfrage von %@ annehmen?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Inkognito akzeptieren"; /* call status */ @@ -503,7 +473,7 @@ "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 (text), and the list deleted." = "Alle Chats werden von der Liste (text) entfernt und danach wird die Liste gelöscht."; +"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."; @@ -535,6 +505,9 @@ /* 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."; @@ -718,9 +691,6 @@ /* No comment provided by engineer. */ "Archiving database" = "Datenbank wird archiviert"; -/* No comment provided by engineer. */ -"Ask" = "Fragen"; - /* No comment provided by engineer. */ "Attach" = "Anhängen"; @@ -847,7 +817,8 @@ /* rcv group event chat item */ "blocked %@" = "%@ wurde blockiert"; -/* marked deleted chat item preview text */ +/* blocked chat item +marked deleted chat item preview text */ "blocked by admin" = "wurde vom Administrator blockiert"; /* No comment provided by engineer. */ @@ -892,6 +863,9 @@ /* 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"; @@ -932,7 +906,7 @@ "Can't message member" = "Mitglied kann nicht benachrichtigt werden"; /* alert action - alert button */ +alert button */ "Cancel" = "Abbrechen"; /* No comment provided by engineer. */ @@ -990,7 +964,7 @@ "Change self-destruct mode" = "Selbstzerstörungs-Modus ändern"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Selbstzerstörungs-Zugangscode ändern"; /* chat item text */ @@ -1158,12 +1132,6 @@ /* No comment provided by engineer. */ "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: **%@**."; @@ -1176,6 +1144,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"; @@ -1489,7 +1460,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 %@."; @@ -1585,7 +1556,7 @@ "decryption errors" = "Entschlüsselungs-Fehler"; /* delete after time - pref value */ +pref value */ "default (%@)" = "Default (%@)"; /* No comment provided by engineer. */ @@ -1595,7 +1566,7 @@ "default (yes)" = "Voreinstellung (Ja)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Löschen"; /* No comment provided by engineer. */ @@ -1911,7 +1882,7 @@ "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. */ @@ -1975,7 +1946,7 @@ "Enable camera access" = "Kamera-Zugriff aktivieren"; /* 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. */ "Enable for all" = "Für Alle aktivieren"; @@ -2254,7 +2225,7 @@ "Error opening chat" = "Fehler beim Öffnen des Chats"; /* 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"; @@ -2355,7 +2326,9 @@ /* No comment provided by engineer. */ "Error: " = "Fehler: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Fehler: %@"; /* No comment provided by engineer. */ @@ -2373,9 +2346,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"; @@ -2434,7 +2404,7 @@ "File errors:\n%@" = "Datei-Fehler:\n%@"; /* file error text */ -"File is blocked by server operator:\n(info.reason.text)." = "Datei wurde vom Server-Betreiber blockiert:\n(info.reason.text)."; +"File is blocked by server operator:\n%@." = "Datei wurde vom Server-Betreiber blockiert:\n%@."; /* file error text */ "File not found - most likely file was deleted or cancelled." = "Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen."; @@ -2452,10 +2422,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: %@"; @@ -2514,9 +2484,6 @@ /* No comment provided by engineer. */ "For all moderators" = "Für alle Moderatoren"; -/* No comment provided by engineer. */ -"for better metadata privacy." = "für einen besseren Metadatenschutz."; - /* servers error */ "For chat profile %@:" = "Für das Chat-Profil %@:"; @@ -2758,10 +2725,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"; @@ -3307,10 +3274,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"; @@ -3559,13 +3526,13 @@ "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."; @@ -3619,8 +3586,8 @@ "observer" = "Beobachter"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "Aus"; /* blur media */ @@ -3710,7 +3677,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Nur Ihr Kontakt kann Sprachnachrichten versenden."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Öffnen"; /* No comment provided by engineer. */ @@ -3728,21 +3695,12 @@ /* No comment provided by engineer. */ "Open group" = "Gruppe öffnen"; -/* No comment provided by engineer. */ -"Open link" = "Web-Link öffnen"; - -/* No comment provided by engineer. */ -"Open links from chat list" = "Web-Links aus der Chat-Liste öffnen"; - /* authentication reason */ "Open migration to another device" = "Migration auf ein anderes Gerät öffnen"; /* No comment provided by engineer. */ "Open Settings" = "Geräte-Einstellungen öffnen"; -/* No comment provided by engineer. */ -"Open web link?" = "Web-Link öffnen?"; - /* No comment provided by engineer. */ "Opening app…" = "App wird geöffnet…"; @@ -3830,9 +3788,15 @@ /* 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. */ "Periodic" = "Periodisch"; @@ -3938,9 +3902,15 @@ /* 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"; @@ -4113,7 +4083,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"; @@ -4167,7 +4137,7 @@ "Registered" = "Registriert"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Ablehnen"; /* No comment provided by engineer. */ @@ -4176,6 +4146,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Kontaktanfrage ablehnen"; +/* No comment provided by engineer. */ +"rejected" = "abgelehnt"; + /* call status */ "rejected call" = "Abgelehnter Anruf"; @@ -4267,7 +4240,7 @@ "Report violation: only group moderators will see it." = "Verstoß melden: Nur Gruppenmoderatoren werden es sehen."; /* report in notification */ -"Report: (text.isEmpty ? reason.text : text)" = "Meldung: (text.isEmpty ? reason.text : text)"; +"Report: %@" = "Meldung: %@"; /* No comment provided by engineer. */ "Reporting messages to moderators is prohibited." = "Melden von Nachrichten an Moderatoren ist nicht erlaubt."; @@ -4332,9 +4305,6 @@ /* No comment provided by engineer. */ "Review conditions" = "Nutzungsbedingungen einsehen"; -/* No comment provided by engineer. */ -"Review later" = "Später einsehen"; - /* No comment provided by engineer. */ "Revoke" = "Widerrufen"; @@ -4351,13 +4321,13 @@ "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 */ @@ -4733,7 +4703,7 @@ "Shape profile images" = "Form der Profil-Bilder"; /* alert action - chat item action */ +chat item action */ "Share" = "Teilen"; /* No comment provided by engineer. */ @@ -4772,6 +4742,9 @@ /* No comment provided by engineer. */ "Share with contacts" = "Mit Kontakten teilen"; +/* No comment provided by engineer. */ +"Short link" = "Verkürzter Link"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Bei Nachrichten, die über privates Routing versendet wurden, → anzeigen."; @@ -4814,6 +4787,9 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX-Adresse oder Einmal-Link?"; +/* simplex link type */ +"SimpleX channel link" = "SimpleX-Kanal-Link"; + /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen."; @@ -4899,7 +4875,7 @@ "Somebody" = "Jemand"; /* blocking reason - report reason */ +report reason */ "Spam" = "Spam"; /* No comment provided by engineer. */ @@ -4939,13 +4915,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"; @@ -5125,14 +5101,11 @@ "The old database was not removed during the migration, it can be deleted." = "Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; +"Your profile is stored on your device and only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; -/* No comment provided by engineer. */ -"The 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!"; @@ -5167,7 +5140,7 @@ "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." = "Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden!"; +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Es werden alle herunter- und hochgeladenen Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "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!"; @@ -5205,6 +5178,9 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Das ist Ihre eigene SimpleX-Adresse!"; +/* No comment provided by engineer. */ +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden."; + /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App."; @@ -5397,6 +5373,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."; @@ -5412,6 +5391,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"; @@ -5487,6 +5469,9 @@ /* No comment provided by engineer. */ "Use servers" = "Verwende Server"; +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Kurze Links verwenden (BETA)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Verwenden Sie SimpleX-Chat-Server?"; @@ -5496,6 +5481,9 @@ /* 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."; @@ -5569,10 +5557,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"; @@ -5715,9 +5703,6 @@ /* pref value */ "yes" = "Ja"; -/* No comment provided by engineer. */ -"Yes" = "Ja"; - /* No comment provided by engineer. */ "you" = "Profil"; @@ -5784,9 +5769,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."; @@ -6033,6 +6015,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 b70bf16ea8..28ba0f0642 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (puede copiarse)"; @@ -25,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)"; @@ -58,12 +34,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Estrella en GitHub](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"@'%@'" = "@'%@'"; - -/* 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."; @@ -86,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."; @@ -299,7 +269,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "un dia"; /* time interval */ @@ -309,15 +279,15 @@ "1 minute" = "1 minuto"; /* delete after time - time interval */ +time interval */ "1 month" = "un mes"; /* delete after time - time interval */ +time interval */ "1 week" = "una semana"; /* delete after time */ -"1 year" = "Un año"; +"1 year" = "1 año"; /* No comment provided by engineer. */ "1-time link" = "Enlace de un uso"; @@ -371,8 +341,8 @@ "Accent" = "Color"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Aceptar"; /* No comment provided by engineer. */ @@ -385,7 +355,7 @@ "Accept contact request from %@?" = "¿Aceptar solicitud de contacto de %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Aceptar incógnito"; /* call status */ @@ -500,10 +470,10 @@ "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 (text), and the list deleted." = "Todos los chats serán quitados de la lista (text) y esta será eliminada."; +"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."; @@ -521,10 +491,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!"; @@ -535,6 +505,9 @@ /* 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."; @@ -581,7 +554,7 @@ "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." = "Se permite informar de mensajes a los moderadores."; +"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."; @@ -659,7 +632,7 @@ "App group:" = "Grupo app:"; /* No comment provided by engineer. */ -"App icon" = "Icono aplicación"; +"App icon" = "Icono de la aplicación"; /* No comment provided by engineer. */ "App passcode" = "Código de acceso de la aplicación"; @@ -718,9 +691,6 @@ /* No comment provided by engineer. */ "Archiving database" = "Archivando base de datos"; -/* No comment provided by engineer. */ -"Ask" = "Preguntar"; - /* No comment provided by engineer. */ "Attach" = "Adjuntar"; @@ -797,7 +767,7 @@ "Better groups" = "Grupos mejorados"; /* No comment provided by engineer. */ -"Better groups performance" = "Mejor rendimiento de los grupos"; +"Better groups performance" = "Rendimiento de grupos mejorado"; /* No comment provided by engineer. */ "Better message dates." = "Sistema de fechas mejorado."; @@ -812,7 +782,7 @@ "Better notifications" = "Notificaciones mejoradas"; /* No comment provided by engineer. */ -"Better privacy and security" = "Mejor privacidad y seguridad"; +"Better privacy and security" = "Privacidad y seguridad mejoradas"; /* No comment provided by engineer. */ "Better security ✅" = "Seguridad mejorada ✅"; @@ -847,7 +817,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. */ @@ -892,6 +863,9 @@ /* 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"; @@ -932,7 +906,7 @@ "Can't message member" = "No se pueden enviar mensajes al miembro"; /* alert action - alert button */ +alert button */ "Cancel" = "Cancelar"; /* No comment provided by engineer. */ @@ -990,7 +964,7 @@ "Change self-destruct mode" = "Cambiar el modo de autodestrucción"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Cambiar código autodestrucción"; /* chat item text */ @@ -1063,10 +1037,10 @@ "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!"; /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -1158,12 +1132,6 @@ /* No comment provided by engineer. */ "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): **%@**."; @@ -1176,6 +1144,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"; @@ -1387,7 +1358,7 @@ "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 will be deleted - this cannot be undone!" = "El contacto será eliminado. ¡No puede deshacerse!"; /* No comment provided by engineer. */ "Contacts" = "Contactos"; @@ -1585,7 +1556,7 @@ "decryption errors" = "errores de descifrado"; /* delete after time - pref value */ +pref value */ "default (%@)" = "predeterminado (%@)"; /* No comment provided by engineer. */ @@ -1595,7 +1566,7 @@ "default (yes)" = "predeterminado (sí)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Eliminar"; /* No comment provided by engineer. */ @@ -1701,7 +1672,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?"; @@ -1713,7 +1684,7 @@ "Delete queue" = "Eliminar cola"; /* No comment provided by engineer. */ -"Delete report" = "Borrar informe"; +"Delete report" = "Eliminar informe"; /* No comment provided by engineer. */ "Delete up to 20 messages at once." = "Elimina hasta 20 mensajes a la vez."; @@ -1911,7 +1882,7 @@ "Downgrade and open chat" = "Degradar y abrir Chat"; /* alert button - chat item action */ +chat item action */ "Download" = "Descargar"; /* No comment provided by engineer. */ @@ -1975,7 +1946,7 @@ "Enable camera access" = "Permitir acceso a la cámara"; /* 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. */ "Enable for all" = "Activar para todos"; @@ -2355,7 +2326,9 @@ /* No comment provided by engineer. */ "Error: " = "Error: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Error: %@"; /* No comment provided by engineer. */ @@ -2373,9 +2346,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"; @@ -2434,10 +2404,10 @@ "File errors:\n%@" = "Error(es) de archivo\n%@"; /* file error text */ -"File is blocked by server operator:\n(info.reason.text)." = "Archivo bloqueado por el operador del servidor\n(info.reason.text)."; +"File is blocked by server operator:\n%@." = "Archivo bloqueado por el operador del servidor\n%@."; /* file error text */ -"File not found - most likely file was deleted or cancelled." = "Archivo no encontrado, probablemente haya sido borrado o cancelado."; +"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: %@"; @@ -2514,9 +2484,6 @@ /* No comment provided by engineer. */ "For all moderators" = "Para todos los moderadores"; -/* No comment provided by engineer. */ -"for better metadata privacy." = "para mejorar la privacidad de los metadatos."; - /* servers error */ "For chat profile %@:" = "Para el perfil de chat %@:"; @@ -2602,7 +2569,7 @@ "Further reduced battery usage" = "Reducción consumo de batería"; /* No comment provided by engineer. */ -"Get notified when mentioned." = "Las menciones ahora notifican."; +"Get notified when mentioned." = "Las menciones ahora se notifican."; /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs y stickers"; @@ -2671,10 +2638,10 @@ "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"; @@ -2683,7 +2650,7 @@ "Help" = "Ayuda"; /* No comment provided by engineer. */ -"Help admins moderating their groups." = "Ayuda a los admins moderar sus grupos."; +"Help admins moderating their groups." = "Ayuda a los admins a moderar sus grupos."; /* No comment provided by engineer. */ "Hidden" = "Oculto"; @@ -3178,7 +3145,7 @@ "Member inactive" = "Miembro inactivo"; /* chat feature */ -"Member reports" = "Informes de miembro"; +"Member reports" = "Informes de miembros"; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\" y todos serán notificados."; @@ -3190,10 +3157,10 @@ "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!"; /* No comment provided by engineer. */ "Members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; @@ -3220,7 +3187,7 @@ "Members can send voice messages." = "Los miembros del grupo pueden enviar mensajes de voz."; /* No comment provided by engineer. */ -"Mention members 👋" = "Mencionar miembros 👋"; +"Mention members 👋" = "Menciona a miembros 👋"; /* No comment provided by engineer. */ "Menus" = "Menus"; @@ -3295,7 +3262,7 @@ "Messages from %@ will be shown!" = "¡Los mensajes de %@ serán mostrados!"; /* alert message */ -"Messages in this chat will never be deleted." = "Los mensajes de esta conversación nunca se borran."; +"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"; @@ -3304,7 +3271,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."; @@ -3619,8 +3586,8 @@ "observer" = "observador"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "desactivado"; /* blur media */ @@ -3710,7 +3677,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Sólo tu contacto puede enviar mensajes de voz."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Abrir"; /* No comment provided by engineer. */ @@ -3728,21 +3695,12 @@ /* No comment provided by engineer. */ "Open group" = "Grupo abierto"; -/* No comment provided by engineer. */ -"Open link" = "Abrir enlace"; - -/* No comment provided by engineer. */ -"Open links from chat list" = "Abrir enlaces desde listado de chats"; - /* authentication reason */ "Open migration to another device" = "Abrir menú migración a otro dispositivo"; /* No comment provided by engineer. */ "Open Settings" = "Abrir Configuración"; -/* No comment provided by engineer. */ -"Open web link?" = "¿Abrir enlace web?"; - /* No comment provided by engineer. */ "Opening app…" = "Iniciando aplicación…"; @@ -3771,7 +3729,7 @@ "Or to share privately" = "O para compartir en privado"; /* No comment provided by engineer. */ -"Organize chats into lists" = "Organiza los chats en listas"; +"Organize chats into lists" = "Organiza tus chats en listas"; /* No comment provided by engineer. */ "other" = "otros"; @@ -3830,9 +3788,15 @@ /* 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. */ "Periodic" = "Periódicamente"; @@ -3938,14 +3902,20 @@ /* 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 de archivos de media."; +"Private media file names." = "Nombres privados en archivos de media."; /* No comment provided by engineer. */ "Private message routing" = "Enrutamiento privado de mensajes"; @@ -4053,7 +4023,7 @@ "Rate the app" = "Valora la aplicación"; /* No comment provided by engineer. */ -"Reachable chat toolbar" = "Barra de chat accesible"; +"Reachable chat toolbar" = "Barra de menú accesible"; /* chat item menu */ "React…" = "Reacciona…"; @@ -4167,7 +4137,7 @@ "Registered" = "Registrado"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Rechazar"; /* No comment provided by engineer. */ @@ -4176,6 +4146,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Rechazar solicitud de contacto"; +/* No comment provided by engineer. */ +"rejected" = "rechazado"; + /* call status */ "rejected call" = "llamada rechazada"; @@ -4267,7 +4240,7 @@ "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: (text.isEmpty ? reason.text : text)" = "Informe: (text.isEmpty ? reason.text : text)"; +"Report: %@" = "Informe: %@"; /* No comment provided by engineer. */ "Reporting messages to moderators is prohibited." = "No se permite informar de mensajes a los moderadores."; @@ -4332,9 +4305,6 @@ /* No comment provided by engineer. */ "Review conditions" = "Revisar condiciones"; -/* No comment provided by engineer. */ -"Review later" = "Revisar más tarde"; - /* No comment provided by engineer. */ "Revoke" = "Revocar"; @@ -4357,7 +4327,7 @@ "Safer groups" = "Grupos más seguros"; /* alert button - chat item action */ +chat item action */ "Save" = "Guardar"; /* alert button */ @@ -4676,7 +4646,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"; @@ -4700,7 +4670,7 @@ "Set it instead of system authentication." = "Úsalo en lugar de la autenticación del sistema."; /* No comment provided by engineer. */ -"Set message expiration in chats." = "Establece vencimiento de los mensajes en chats."; +"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"; @@ -4733,7 +4703,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. */ @@ -4772,6 +4742,9 @@ /* No comment provided by engineer. */ "Share with contacts" = "Compartir con contactos"; +/* No comment provided by engineer. */ +"Short link" = "Enlace corto"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Mostrar → en mensajes con enrutamiento privado."; @@ -4809,11 +4782,14 @@ "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 link type */ +"SimpleX channel link" = "Enlace de canal SimpleX"; + /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "Simplex Chat y Flux han acordado incluir en la aplicación servidores operados por Flux."; @@ -4899,7 +4875,7 @@ "Somebody" = "Alguien"; /* blocking reason - report reason */ +report reason */ "Spam" = "Spam"; /* No comment provided by engineer. */ @@ -5083,7 +5059,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."; @@ -5125,14 +5101,11 @@ "The old database was not removed during the migration, it can be deleted." = "La base de datos antigua no se eliminó durante la migración, puede eliminarse."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; +"Your profile is stored on your device and only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; -/* No comment provided by engineer. */ -"The 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!"; @@ -5149,7 +5122,7 @@ "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."; @@ -5173,7 +5146,7 @@ "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 podrá deshacerse!"; +"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."; @@ -5205,11 +5178,14 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "¡Esta es tu propia dirección SimpleX!"; +/* No comment provided by engineer. */ +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Este enlace requiere una versión más reciente de la aplicación. Por favor, actualiza la aplicación o pide a tu contacto un enlace compatible."; + /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador."; /* No comment provided by engineer. */ -"This message was deleted or not received yet." = "El mensaje ha sido borrado o aún no se ha recibido."; +"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 **%@**."; @@ -5242,7 +5218,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"; @@ -5320,7 +5296,7 @@ "Unblock member" = "Desbloquear miembro"; /* No comment provided by engineer. */ -"Unblock member for all?" = "¿Desbloquear miembro para todos?"; +"Unblock member for all?" = "¿Desbloquear el miembro para todos?"; /* No comment provided by engineer. */ "Unblock member?" = "¿Desbloquear miembro?"; @@ -5397,6 +5373,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."; @@ -5412,6 +5391,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"; @@ -5487,6 +5469,9 @@ /* No comment provided by engineer. */ "Use servers" = "Usar servidores"; +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Usar enlaces cortos (BETA)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "¿Usar servidores SimpleX Chat?"; @@ -5496,6 +5481,9 @@ /* 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."; @@ -5715,9 +5703,6 @@ /* pref value */ "yes" = "sí"; -/* No comment provided by engineer. */ -"Yes" = "Si"; - /* No comment provided by engineer. */ "you" = "tu"; @@ -5784,9 +5769,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."; @@ -6033,6 +6015,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 e860e56426..4891c7fb26 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)"; @@ -203,7 +182,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 päivä"; /* time interval */ @@ -213,11 +192,11 @@ "1 minute" = "1 minuutti"; /* delete after time - time interval */ +time interval */ "1 month" = "1 kuukausi"; /* delete after time - time interval */ +time interval */ "1 week" = "1 viikko"; /* No comment provided by engineer. */ @@ -260,8 +239,8 @@ "above, then choose:" = "edellä, valitse sitten:"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Hyväksy"; /* No comment provided by engineer. */ @@ -271,7 +250,7 @@ "Accept contact request from %@?" = "Hyväksy kontaktipyyntö %@:ltä?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Hyväksy tuntematon"; /* call status */ @@ -530,7 +509,7 @@ "Can't invite contacts!" = "Kontakteja ei voi kutsua!"; /* alert action - alert button */ +alert button */ "Cancel" = "Peruuta"; /* feature offered item */ @@ -570,7 +549,7 @@ "Change self-destruct mode" = "Vaihda itsetuhotilaa"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Vaihda itsetuhoutuva pääsykoodi"; /* chat item text */ @@ -789,6 +768,9 @@ /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Luo uusi profiili [työpöytäsovelluksessa](https://simplex.chat/downloads/). 💻"; +/* No comment provided by engineer. */ +"Create profile" = "Luo profiilisi"; + /* server test step */ "Create queue" = "Luo jono"; @@ -889,7 +871,7 @@ "Decryption error" = "Salauksen purkuvirhe"; /* delete after time - pref value */ +pref value */ "default (%@)" = "oletusarvo (%@)"; /* No comment provided by engineer. */ @@ -899,7 +881,7 @@ "default (yes)" = "oletusarvo (kyllä)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Poista"; /* No comment provided by engineer. */ @@ -1379,7 +1361,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 +1375,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Jopa kun ei käytössä keskustelussa."; -/* No comment provided by engineer. */ -"event happened" = "tapahtuma tapahtui"; - /* No comment provided by engineer. */ "Exit without saving" = "Poistu tallentamatta"; @@ -2097,8 +2078,8 @@ "observer" = "tarkkailija"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "pois"; /* blur media */ @@ -2402,7 +2383,7 @@ "Reduced battery usage" = "Pienempi akun käyttö"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Hylkää"; /* No comment provided by engineer. */ @@ -2502,7 +2483,7 @@ "Run chat" = "Käynnistä chat"; /* alert button - chat item action */ +chat item action */ "Save" = "Tallenna"; /* alert button */ @@ -2710,7 +2691,7 @@ "Settings" = "Asetukset"; /* alert action - chat item action */ +chat item action */ "Share" = "Jaa"; /* No comment provided by engineer. */ @@ -2930,7 +2911,7 @@ "The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; +"Your profile is stored on your device and only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Toinen kuittaus, joka uupui! ✅"; @@ -3427,6 +3408,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 05dcfa9cd3..4dd75039dc 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)"; @@ -199,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"; @@ -287,7 +269,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 jour"; /* time interval */ @@ -297,13 +279,16 @@ "1 minute" = "1 minute"; /* delete after time - time interval */ +time interval */ "1 month" = "1 mois"; /* delete after time - time interval */ +time interval */ "1 week" = "1 semaine"; +/* delete after time */ +"1 year" = "1 an"; + /* No comment provided by engineer. */ "1-time link" = "Lien unique"; @@ -356,8 +341,8 @@ "Accent" = "Principale"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Accepter"; /* No comment provided by engineer. */ @@ -370,7 +355,7 @@ "Accept contact request from %@?" = "Accepter la demande de contact de %@ ?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Accepter en incognito"; /* call status */ @@ -388,6 +373,9 @@ /* 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"; @@ -397,6 +385,9 @@ /* No comment provided by engineer. */ "Add friends" = "Ajouter des amis"; +/* No comment provided by engineer. */ +"Add list" = "Ajouter une liste"; + /* No comment provided by engineer. */ "Add profile" = "Ajouter un profil"; @@ -412,6 +403,9 @@ /* 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"; @@ -469,12 +463,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."; @@ -502,6 +502,9 @@ /* profile dropdown */ "All profiles" = "Tous les profiles"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Tous les rapports seront archivés pour vous."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tous vos contacts resteront connectés."; @@ -547,6 +550,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."; @@ -601,6 +607,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"; @@ -643,12 +652,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"; @@ -730,6 +757,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."; @@ -742,6 +772,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 ✅"; @@ -775,7 +808,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. */ @@ -814,9 +848,15 @@ /* 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"; @@ -857,7 +897,7 @@ "Can't message member" = "Impossible d'envoyer un message à ce membre"; /* alert action - alert button */ +alert button */ "Cancel" = "Annuler"; /* No comment provided by engineer. */ @@ -884,6 +924,9 @@ /* 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"; @@ -912,7 +955,7 @@ "Change self-destruct mode" = "Modifier le mode d'autodestruction"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Modifier le code d'autodestruction"; /* chat item text */ @@ -1032,6 +1075,12 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Effacer la conversation ?"; +/* No comment provided by engineer. */ +"Clear group?" = "Vider le groupe ?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Vider ou supprimer le groupe ?"; + /* No comment provided by engineer. */ "Clear private notes?" = "Effacer les notes privées ?"; @@ -1047,6 +1096,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"; @@ -1071,12 +1123,6 @@ /* No comment provided by engineer. */ "Conditions of use" = "Conditions d'utilisation"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Les conditions seront acceptées pour les opérateurs activés après 30 jours."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for operator(s): **%@**." = "Les conditions seront acceptées pour le(s) opérateur(s) : **%@**."; - /* No comment provided by engineer. */ "Conditions will be accepted for the operator(s): **%@**." = "Les conditions seront acceptées pour le(s) opérateur(s) : **%@**."; @@ -1089,6 +1135,9 @@ /* 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"; @@ -1119,6 +1168,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Confirmer la transmission"; +/* token status text */ +"Confirmed" = "Confirmé"; + /* server test step */ "Connect" = "Se connecter"; @@ -1218,6 +1270,9 @@ /* No comment provided by engineer. */ "Connection and servers status." = "État de la connexion et des serveurs."; +/* No comment provided by engineer. */ +"Connection blocked" = "Connexion bloquée"; + /* No comment provided by engineer. */ "Connection error" = "Erreur de connexion"; @@ -1227,12 +1282,21 @@ /* chat list item title (it should not be shown */ "connection established" = "connexion établie"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "La connexion est bloquée par l'opérateur du serveur :\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "La connexion n'est pas prête."; + /* No comment provided by engineer. */ "Connection notifications" = "Notifications de connexion"; /* No comment provided by engineer. */ "Connection request sent!" = "Demande de connexion envoyée !"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "La connexion nécessite une renégociation du cryptage."; + /* No comment provided by engineer. */ "Connection security" = "Sécurité des connexions"; @@ -1293,6 +1357,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"; @@ -1335,6 +1402,9 @@ /* No comment provided by engineer. */ "Create link" = "Créer un lien"; +/* No comment provided by engineer. */ +"Create list" = "Créer une liste"; + /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Créer un nouveau profil sur [l'application de bureau](https://simplex.chat/downloads/). 💻"; @@ -1477,7 +1547,7 @@ "decryption errors" = "Erreurs de déchiffrement"; /* delete after time - pref value */ +pref value */ "default (%@)" = "défaut (%@)"; /* No comment provided by engineer. */ @@ -1487,7 +1557,7 @@ "default (yes)" = "par défaut (oui)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Supprimer"; /* No comment provided by engineer. */ @@ -1514,6 +1584,9 @@ /* 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"; @@ -1568,6 +1641,9 @@ /* 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 ?"; @@ -1598,6 +1674,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."; @@ -1706,6 +1785,12 @@ /* 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"; @@ -1766,20 +1851,29 @@ /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "N'utilisez PAS SimpleX pour les appels d'urgence."; +/* No comment provided by engineer. */ +"Documents:" = "Documents:"; + /* No comment provided by engineer. */ "Don't create address" = "Ne pas créer d'adresse"; /* No comment provided by engineer. */ "Don't enable" = "Ne pas activer"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "Ne manquez pas les messages importants."; + /* No comment provided by engineer. */ "Don't show again" = "Ne plus afficher"; +/* No comment provided by engineer. */ +"Done" = "Terminé"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Rétrograder et ouvrir le chat"; /* alert button - chat item action */ +chat item action */ "Download" = "Télécharger"; /* No comment provided by engineer. */ @@ -1843,7 +1937,7 @@ "Enable camera access" = "Autoriser l'accès à la caméra"; /* No comment provided by engineer. */ -"Enable Flux" = "Activer 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. */ "Enable for all" = "Activer pour tous"; @@ -1956,6 +2050,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "renégociation de chiffrement requise pour %@"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Renégociation du chiffrement en cours."; + /* No comment provided by engineer. */ "ended" = "terminé"; @@ -2031,6 +2128,9 @@ /* No comment provided by engineer. */ "Error changing to incognito!" = "Erreur lors du passage en mode incognito !"; +/* No comment provided by engineer. */ +"Error checking token status" = "Erreur lors de la vérification de l'état du jeton (token)"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard."; @@ -2043,6 +2143,9 @@ /* No comment provided by engineer. */ "Error creating group link" = "Erreur lors de la création du lien du groupe"; +/* alert title */ +"Error creating list" = "Erreur lors de la création de la liste"; + /* No comment provided by engineer. */ "Error creating member contact" = "Erreur lors de la création du contact du membre"; @@ -2052,6 +2155,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Erreur lors de la création du profil !"; +/* No comment provided by engineer. */ +"Error creating report" = "Erreur lors de la création du rapport"; + /* No comment provided by engineer. */ "Error decrypting file" = "Erreur lors du déchiffrement du fichier"; @@ -2118,12 +2224,21 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Erreur de reconnexion des serveurs"; +/* alert title */ +"Error registering for notifications" = "Erreur lors de l'inscription aux notifications"; + /* No comment provided by engineer. */ "Error removing member" = "Erreur lors de la suppression d'un membre"; +/* alert title */ +"Error reordering lists" = "Erreur lors de la réorganisation des listes"; + /* No comment provided by engineer. */ "Error 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"; @@ -2175,6 +2290,9 @@ /* 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"; @@ -2199,7 +2317,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. */ @@ -2217,9 +2337,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Même s'il est désactivé dans la conversation."; -/* No comment provided by engineer. */ -"event happened" = "event happened"; - /* No comment provided by engineer. */ "Exit without saving" = "Quitter sans enregistrer"; @@ -2229,6 +2346,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"; @@ -2253,18 +2373,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é."; @@ -2340,9 +2472,6 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Correction non prise en charge par un membre du groupe"; -/* No comment provided by engineer. */ -"for better metadata privacy." = "pour une meilleure protection des métadonnées."; - /* servers error */ "For chat profile %@:" = "Pour le profil de discussion %@ :"; @@ -3349,8 +3478,8 @@ "observer" = "observateur"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "off"; /* blur media */ @@ -3434,7 +3563,7 @@ /* 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. */ @@ -3855,7 +3984,7 @@ "Reduced battery usage" = "Réduction de la consommation de batterie"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Rejeter"; /* No comment provided by engineer. */ @@ -3990,9 +4119,6 @@ /* No comment provided by engineer. */ "Review conditions" = "Vérifier les conditions"; -/* No comment provided by engineer. */ -"Review later" = "Vérifier plus tard"; - /* No comment provided by engineer. */ "Revoke" = "Révoquer"; @@ -4015,7 +4141,7 @@ "Safer groups" = "Groupes plus sûrs"; /* alert button - chat item action */ +chat item action */ "Save" = "Enregistrer"; /* alert button */ @@ -4379,7 +4505,7 @@ "Shape profile images" = "Images de profil modelable"; /* alert action - chat item action */ +chat item action */ "Share" = "Partager"; /* No comment provided by engineer. */ @@ -4758,14 +4884,11 @@ "The old database was not removed during the migration, it can be deleted." = "L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Le profil n'est partagé qu'avec vos contacts."; +"Your profile is stored on your device and only shared with your contacts." = "Le profil n'est partagé qu'avec vos contacts."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Les mêmes conditions s'appliquent à l'opérateur **%@**."; -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Les mêmes conditions s'appliquent à(aux) l'opérateur(s) : **%@**."; - /* No comment provided by engineer. */ "The second preset operator in the app!" = "Le deuxième opérateur prédéfini de l'application !"; @@ -5399,9 +5522,6 @@ /* 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 operators in Network & servers settings." = "Vous pouvez définir les opérateurs dans les paramètres Réseau et serveurs."; - /* No comment provided by engineer. */ "You can configure servers via settings." = "Vous pouvez configurer les serveurs via les paramètres."; @@ -5645,6 +5765,3 @@ /* 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 5a01b41d2e..5a9b6b4e38 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (másolható)"; @@ -25,21 +16,9 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* 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)"; @@ -47,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)"; @@ -58,12 +34,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"@'%@'" = "@'%@'"; - -/* No comment provided by engineer. */ -"@%@" = "@%@"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Partner hozzáadása:** új meghívási hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; @@ -185,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."; @@ -278,7 +248,7 @@ "%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."; @@ -299,7 +269,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 nap"; /* time interval */ @@ -309,21 +279,21 @@ "1 minute" = "1 perc"; /* delete after time - time interval */ +time interval */ "1 month" = "1 hónap"; /* delete after time - time interval */ +time interval */ "1 week" = "1 hét"; /* delete after time */ "1 year" = "1 év"; /* No comment provided by engineer. */ -"1-time link" = "Egyszer használható meghívási hivatkozás"; +"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ási hivatkozás csak *egyetlen partnerrel használható* – személyesen vagy bármilyen üzenetváltón keresztül megosztható."; +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Az egyszer használható meghívó egy hivatkozás és *csak egyetlen partnerrel használható* – személyesen vagy bármilyen üzenetváltó-alkalmazáson keresztül megosztható."; /* No comment provided by engineer. */ "5 minutes" = "5 perc"; @@ -356,7 +326,7 @@ "Abort changing address" = "Cím módosításának megszakítása"; /* No comment provided by engineer. */ -"Abort changing address?" = "Cím módosításának 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"; @@ -371,21 +341,21 @@ "Accent" = "Kiemelőszín"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Elfogadás"; /* No comment provided by engineer. */ "Accept conditions" = "Feltételek elfogadása"; /* No comment provided by engineer. */ -"Accept connection request?" = "Kapcsolatkérés elfogadása?"; +"Accept connection request?" = "Elfogadja a meghívási kérést?"; /* notification body */ -"Accept contact request from %@?" = "Elfogadja %@ kapcsolatkérését?"; +"Accept contact request from %@?" = "Elfogadja %@ meghívási kérését?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Elfogadás inkognitóban"; /* call status */ @@ -410,7 +380,7 @@ "Active connections" = "Aktív kapcsolatok száma"; /* No comment provided by engineer. */ -"Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Cím hozzáadása a profilhoz, hogy a partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve az Ön partnerei 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"; @@ -464,7 +434,7 @@ "Address change will be aborted. Old receiving address will be used." = "A cím módosítása meg fog szakadni. A régi fogadási cím lesz használva."; /* No comment provided by engineer. */ -"Address or 1-time link?" = "Cím vagy egyszer használható meghívási 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"; @@ -503,13 +473,13 @@ "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 (text), and the list deleted." = "Az összes csevegés el lesz távolítva a(z) (text) nevű listáról, és a lista is törölve lesz."; +"All chats will be removed from the list %@, and the list deleted." = "Az összes csevegés el lesz távolítva a(z) %@ nevű listáról, és a lista is törölve lesz."; /* No comment provided by engineer. */ "All data is erased when it is entered." = "A jelkód megadása után az összes adat törölve lesz."; /* No comment provided by engineer. */ -"All data is kept private on your device." = "Az összes adat privát módon van tárolva az Ön 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."; @@ -518,7 +488,7 @@ "all members" = "összes tag"; /* No comment provided by engineer. */ -"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Az összes üzenet és fájl **végpontok közötti titkosítással**, a közvetlen üzenetek továbbá kvantumálló titkosítással is rendelkeznek."; +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Az összes üzenet és fájl **végpontok közötti titkosítással**, a közvetlen üzenetek továbbá kvantumbiztos titkosítással is rendelkeznek."; /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Az összes üzenet törölve lesz – ez a művelet nem vonható vissza!"; @@ -527,7 +497,7 @@ "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 %@ által írt új üzenet el lesz rejtve!"; +"All new messages from %@ will be hidden!" = "%@ összes új üzenete el lesz rejtve!"; /* profile dropdown */ "All profiles" = "Összes profil"; @@ -535,11 +505,14 @@ /* No comment provided by engineer. */ "All reports will be archived for you." = "Az összes jelentés archiválva lesz az Ön számára."; +/* No comment provided by engineer. */ +"All servers" = "Összes kiszolgáló"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Az összes partnerével kapcsolatban marad."; /* No comment provided by engineer. */ -"All your contacts will remain connected. Profile update will be sent to your contacts." = "A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve az Ön partnerei számára."; +"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."; @@ -551,7 +524,7 @@ "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 a partnere is engedélyezi az Ön számára."; @@ -596,7 +569,7 @@ "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." = "A reakciók hozzáadása az üzenetekhez engedélyezve van a partnerei számára."; @@ -689,10 +662,10 @@ "Archive" = "Archívum"; /* No comment provided by engineer. */ -"Archive %lld reports?" = "%lld jelentés archiválása?"; +"Archive %lld reports?" = "Archivál %lld jelentést?"; /* No comment provided by engineer. */ -"Archive all reports?" = "Az összes jelentés archiválása?"; +"Archive all reports?" = "Archiválja az összes jelentést?"; /* No comment provided by engineer. */ "Archive and upload" = "Archiválás és feltöltés"; @@ -704,7 +677,7 @@ "Archive report" = "Jelentés archiválása"; /* No comment provided by engineer. */ -"Archive report?" = "Jelentés archiválása?"; +"Archive report?" = "Archiválja a jelentést?"; /* swipe action */ "Archive reports" = "Jelentések archiválása"; @@ -719,10 +692,7 @@ "Archiving database" = "Adatbázis archiválása"; /* No comment provided by engineer. */ -"Ask" = "Mindig kérdezzen rá"; - -/* No comment provided by engineer. */ -"Attach" = "Csatolás"; +"Attach" = "Mellékelés"; /* No comment provided by engineer. */ "attempts" = "próbálkozások"; @@ -761,7 +731,7 @@ "Auto-accept" = "Automatikus elfogadás"; /* No comment provided by engineer. */ -"Auto-accept contact requests" = "Kapcsolatkérések automatikus elfogadása"; +"Auto-accept contact requests" = "Meghívási kérések automatikus elfogadása"; /* No comment provided by engineer. */ "Auto-accept images" = "Képek automatikus elfogadása"; @@ -836,10 +806,10 @@ "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"; @@ -847,7 +817,8 @@ /* rcv group event chat item */ "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. */ @@ -892,6 +863,9 @@ /* 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"; @@ -932,7 +906,7 @@ "Can't message member" = "Nem lehet üzenetet küldeni a tagnak"; /* alert action - alert button */ +alert button */ "Cancel" = "Mégse"; /* No comment provided by engineer. */ @@ -960,19 +934,19 @@ "Change" = "Módosítás"; /* alert title */ -"Change automatic message deletion?" = "Automatikus üzenettörlés módosítása?"; +"Change automatic message deletion?" = "Módosítja az automatikus üzenettörlést?"; /* authentication reason */ "Change chat profiles" = "Csevegési profilok módosítása"; /* No comment provided by engineer. */ -"Change database passphrase?" = "Az adatbázis jelmondatának módosítása?"; +"Change database passphrase?" = "Módosítja az adatbázis jelmondatát?"; /* authentication reason */ "Change lock mode" = "Zárolási mód módosítása"; /* No comment provided by engineer. */ -"Change member role?" = "Tag szerepkörének módosítása?"; +"Change member role?" = "Módosítja a tag szerepkörét?"; /* authentication reason */ "Change passcode" = "Jelkód módosítása"; @@ -990,7 +964,7 @@ "Change self-destruct mode" = "Önmegsemmisítő-mód módosítása"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Önmegsemmisítő-jelkód módosítása"; /* chat item text */ @@ -1108,16 +1082,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 group?" = "Csoport 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?" = "Privát jegyzetek kiürítése?"; +"Clear private notes?" = "Kiüríti a privát jegyzeteket?"; /* No comment provided by engineer. */ "Clear verification" = "Hitelesítés törlése"; @@ -1147,7 +1121,7 @@ "Completed" = "Elkészült"; /* No comment provided by engineer. */ -"Conditions accepted on: %@." = "Feltételek elfogadva: %@."; +"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: **%@**."; @@ -1158,24 +1132,21 @@ /* No comment provided by engineer. */ "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ők 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 el lesznek fogadva az engedélyezett üzemeltetők 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"; @@ -1228,13 +1199,13 @@ "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?"; +"Connect to yourself?" = "Kapcsolódik saját magához?"; /* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az Ön egyszer használható meghívási hivatkozása!"; +"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódik saját magához?\nEz a saját egyszer használható meghívója!"; /* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódás saját magához?\nEz az Ön SimpleX-címe!"; +"Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódik saját magához?\nEz a saját SimpleX-címe!"; /* No comment provided by engineer. */ "Connect via contact address" = "Kapcsolódás a kapcsolattartási címen keresztül"; @@ -1243,7 +1214,7 @@ "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ási hivatkozáson keresztül"; +"Connect via one-time link" = "Kapcsolódás egyszer használható meghívón keresztül"; /* No comment provided by engineer. */ "Connect with %@" = "Kapcsolódás a következővel: %@"; @@ -1279,7 +1250,7 @@ "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ó)"; @@ -1330,7 +1301,7 @@ "Connection notifications" = "Kapcsolódási értesítések"; /* No comment provided by engineer. */ -"Connection request sent!" = "Kapcsolatkérés elküldve!"; +"Connection request sent!" = "Meghívási kérés elküldve!"; /* No comment provided by engineer. */ "Connection requires encryption renegotiation." = "A kapcsolat titkosítása újraegyeztetést igényel."; @@ -1417,13 +1388,13 @@ "Corner" = "Sarok"; /* No comment provided by engineer. */ -"Correct name to %@?" = "Név helyesbítése a következőre: %@?"; +"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ási 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ű profillal."; @@ -1501,7 +1472,7 @@ "Custom time" = "Egyéni időköz"; /* No comment provided by engineer. */ -"Customizable message shape." = "Testre szabható üzenetbuborékok."; +"Customizable message shape." = "Személyre szabható üzenetbuborékok."; /* No comment provided by engineer. */ "Customize theme" = "Téma személyre szabása"; @@ -1585,7 +1556,7 @@ "decryption errors" = "visszafejtési hibák"; /* delete after time - pref value */ +pref value */ "default (%@)" = "alapértelmezett (%@)"; /* No comment provided by engineer. */ @@ -1595,20 +1566,20 @@ "default (yes)" = "alapértelmezett (igen)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Törlés"; /* No comment provided by engineer. */ "Delete %lld messages of members?" = "Törli a tagok %lld üzenetét?"; /* No comment provided by engineer. */ -"Delete %lld messages?" = "%lld üzenet törlése?"; +"Delete %lld messages?" = "Töröl %lld üzenetet?"; /* No comment provided by engineer. */ "Delete address" = "Cím törlése"; /* No comment provided by engineer. */ -"Delete address?" = "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"; @@ -1623,16 +1594,16 @@ "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 Ön eszközéről."; +"Delete chat messages from your device." = "Csevegési üzenetek törlése a saját eszközéről."; /* No comment provided by engineer. */ "Delete chat profile" = "Csevegési profil törlése"; /* No comment provided by engineer. */ -"Delete chat profile?" = "Csevegési profil törlése?"; +"Delete chat profile?" = "Törli a csevegési profilt?"; /* No comment provided by engineer. */ -"Delete chat?" = "Csevegés törlése?"; +"Delete chat?" = "Törli a csevegést?"; /* No comment provided by engineer. */ "Delete connection" = "Kapcsolat törlése"; @@ -1641,7 +1612,7 @@ "Delete contact" = "Partner törlése"; /* No comment provided by engineer. */ -"Delete contact?" = "Partner törlése?"; +"Delete contact?" = "Törli a partnert?"; /* No comment provided by engineer. */ "Delete database" = "Adatbázis törlése"; @@ -1653,7 +1624,7 @@ "Delete file" = "Fájl törlése"; /* No comment provided by engineer. */ -"Delete files and media?" = "A fájlok- és a médiatartalmak törlése?"; +"Delete files and media?" = "Törli a fájl- és a médiatartalmakat?"; /* No comment provided by engineer. */ "Delete files for all chat profiles" = "Fájlok törlése az összes csevegési profilból"; @@ -1662,13 +1633,13 @@ "Delete for everyone" = "Törlés az összes tagnál"; /* No comment provided by engineer. */ -"Delete for me" = "Csak számomra"; +"Delete for me" = "Csak nálam"; /* No comment provided by engineer. */ "Delete group" = "Csoport törlése"; /* No comment provided by engineer. */ -"Delete group?" = "Csoport törlése?"; +"Delete group?" = "Törli a csoportot?"; /* No comment provided by engineer. */ "Delete invitation" = "Meghívó törlése"; @@ -1677,16 +1648,16 @@ "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?" = "Lista törlése?"; +"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?"; /* alert button */ "Delete messages" = "Üzenetek törlése"; @@ -1698,13 +1669,13 @@ "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ő partnerkérelem törlése?"; +"Delete pending connection?" = "Törli a függőben lévő meghívót?"; /* No comment provided by engineer. */ "Delete profile" = "Profil törlése"; @@ -1719,7 +1690,7 @@ "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"; @@ -1824,7 +1795,7 @@ "Disable (keep overrides)" = "Letiltás (felülírások megtartásával)"; /* alert title */ -"Disable automatic message deletion?" = "Automatikus üzenettörlés letiltása?"; +"Disable automatic message deletion?" = "Letiltja az automatikus üzenettörlést?"; /* alert button */ "Disable delete messages" = "Üzenetek törlésének letiltása"; @@ -1863,7 +1834,7 @@ "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" = "Csoportok felfedezése és csatlakozás"; @@ -1878,7 +1849,7 @@ "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."; @@ -1911,7 +1882,7 @@ "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. */ @@ -1939,7 +1910,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"; @@ -1969,13 +1940,13 @@ "Enable (keep overrides)" = "Engedélyezés (felülírások megtartásával)"; /* alert title */ -"Enable automatic message deletion?" = "Automatikus üzenet törlés engedélyezése?"; +"Enable automatic message deletion?" = "Engedélyezi az automatikus üzenettörlést?"; /* No comment provided by engineer. */ "Enable camera access" = "Kamera hozzáférés engedélyezése"; /* No comment provided by engineer. */ -"Enable Flux" = "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"; @@ -1984,7 +1955,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"; @@ -1993,7 +1964,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"; @@ -2026,7 +1997,7 @@ "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"; @@ -2077,7 +2048,7 @@ "encryption re-negotiation allowed for %@" = "a titkosítás újraegyeztetése engedélyezve van %@ számára"; /* message decrypt error item */ -"Encryption re-negotiation error" = "Hiba a titkosítás újraegyeztetésekor"; +"Encryption re-negotiation error" = "Hiba történt a titkosítás újraegyeztetésekor"; /* No comment provided by engineer. */ "Encryption re-negotiation failed." = "Nem sikerült a titkosítást újraegyeztetni."; @@ -2137,225 +2108,227 @@ "Error" = "Hiba"; /* No comment provided by engineer. */ -"Error aborting address change" = "Hiba a cím módosítá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"; +"Error accepting contact request" = "Hiba történt a meghívási kérés elfogadásakor"; /* No comment provided by engineer. */ -"Error adding member(s)" = "Hiba a tag(ok) hozzáadásakor"; +"Error adding member(s)" = "Hiba történt a tag(ok) hozzáadásakor"; /* alert title */ -"Error adding server" = "Hiba a kiszolgáló hozzáadásakor"; +"Error adding server" = "Hiba történt a kiszolgáló hozzáadásakor"; /* No comment provided by engineer. */ -"Error changing address" = "Hiba a cím módosításakor"; +"Error changing address" = "Hiba történt a cím módosításakor"; /* No comment provided by engineer. */ -"Error changing connection profile" = "Hiba a kapcsolati profilra való váltáskor"; +"Error changing connection profile" = "Hiba történt a kapcsolati profilra való váltáskor"; /* No comment provided by engineer. */ -"Error changing role" = "Hiba a szerepkör módosításakor"; +"Error changing role" = "Hiba történt a szerepkör módosításakor"; /* No comment provided by engineer. */ -"Error changing setting" = "Hiba a beállítás módosításakor"; +"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 az inkognitóprofilra való váltáskor!"; +"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 a token állapotának ellenőrzésekor"; +"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."; +"Error connecting to forwarding server %@. Please try later." = "Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később."; /* No comment provided by engineer. */ -"Error creating address" = "Hiba a cím létrehozásakor"; +"Error creating address" = "Hiba történt a cím létrehozásakor"; /* No comment provided by engineer. */ -"Error creating group" = "Hiba a csoport létrehozásakor"; +"Error creating group" = "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"; +"Error creating group link" = "Hiba történt a csoporthivatkozás létrehozásakor"; /* alert title */ -"Error creating list" = "Hiba a lista létrehozásakor"; +"Error creating list" = "Hiba történt a lista létrehozásakor"; /* No comment provided by engineer. */ -"Error creating member contact" = "Hiba a partnerrel történő kapcsolat létrehozásában"; +"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 az üzenet létrehozásakor"; +"Error creating message" = "Hiba történt az üzenet létrehozásakor"; /* No comment provided by engineer. */ -"Error creating profile!" = "Hiba a profil létrehozásakor!"; +"Error creating profile!" = "Hiba történt a profil létrehozásakor!"; /* No comment provided by engineer. */ -"Error creating report" = "Hiba a jelentés létrehozásakor"; +"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"; +"Error decrypting file" = "Hiba történt a fájl visszafejtésekor"; /* No comment provided by engineer. */ -"Error deleting chat database" = "Hiba a csevegési adatbázis törlésekor"; +"Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor"; /* No comment provided by engineer. */ -"Error deleting chat!" = "Hiba a csevegés törlésekor!"; +"Error deleting chat!" = "Hiba történt a csevegés törlésekor!"; /* No comment provided by engineer. */ -"Error deleting connection" = "Hiba a kapcsolat törlésekor"; +"Error deleting connection" = "Hiba történt a kapcsolat törlésekor"; /* No comment provided by engineer. */ -"Error deleting database" = "Hiba az adatbázis törlésekor"; +"Error deleting database" = "Hiba történt az adatbázis törlésekor"; /* No comment provided by engineer. */ -"Error deleting old database" = "Hiba a régi adatbázis törlésekor"; +"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 a token törlésekor"; +"Error deleting token" = "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"; +"Error deleting user profile" = "Hiba történt a felhasználó-profil törlésekor"; /* No comment provided by engineer. */ -"Error downloading the archive" = "Hiba az archívum letöltésekor"; +"Error downloading the archive" = "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!"; +"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 az értesítések engedélyezésekor"; +"Error enabling notifications" = "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"; +"Error encrypting database" = "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"; +"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 a téma exportálásakor: %@"; +"Error exporting theme: %@" = "Hiba történt a téma exportálásakor: %@"; /* No comment provided by engineer. */ -"Error importing chat database" = "Hiba a csevegési adatbázis importálásakor"; +"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 a csoporthoz való csatlakozáskor"; +"Error joining group" = "Hiba történt a csoporthoz való csatlakozáskor"; /* alert title */ -"Error loading servers" = "Hiba a kiszolgálók betöltésekor"; +"Error loading servers" = "Hiba történt a kiszolgálók betöltésekor"; /* No comment provided by engineer. */ -"Error migrating settings" = "Hiba a beállítások átköltöztetésekor"; +"Error migrating settings" = "Hiba történt a beállítások átköltöztetésekor"; /* No comment provided by engineer. */ -"Error opening chat" = "Hiba a csevegés megnyitásakor"; +"Error opening chat" = "Hiba történt a csevegés megnyitásakor"; /* alert title */ -"Error receiving file" = "Hiba a fájl fogadásakor"; +"Error receiving file" = "Hiba történt a fájl fogadásakor"; /* No comment provided by engineer. */ -"Error reconnecting server" = "Hiba a kiszolgálóhoz való újrakapcsolódáskor"; +"Error reconnecting server" = "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"; +"Error reconnecting servers" = "Hiba történt a kiszolgálókhoz való újrakapcsolódáskor"; /* alert title */ -"Error registering for notifications" = "Hiba az értesítések regisztrálásakor"; +"Error registering for notifications" = "Hiba történt az értesítések regisztrálásakor"; /* No comment provided by engineer. */ -"Error removing member" = "Hiba a tag eltávolításakor"; +"Error removing member" = "Hiba történt a tag eltávolításakor"; /* alert title */ -"Error reordering lists" = "Hiba a listák újrarendezésekor"; +"Error reordering lists" = "Hiba történt a listák újrarendezésekor"; /* No comment provided by engineer. */ -"Error resetting statistics" = "Hiba a statisztikák visszaállításakor"; +"Error resetting statistics" = "Hiba történt a statisztikák visszaállításakor"; /* alert title */ -"Error saving chat list" = "Hiba a csevegési lista mentésekor"; +"Error saving chat list" = "Hiba történt a csevegési lista mentésekor"; /* No comment provided by engineer. */ -"Error saving group profile" = "Hiba a csoportprofil mentésekor"; +"Error saving group profile" = "Hiba történt a csoportprofil mentésekor"; /* No comment provided by engineer. */ -"Error saving ICE servers" = "Hiba az ICE-kiszolgálók mentésekor"; +"Error saving ICE servers" = "Hiba történt az ICE-kiszolgálók mentésekor"; /* No comment provided by engineer. */ -"Error saving passcode" = "Hiba a jelkód mentésekor"; +"Error saving passcode" = "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"; +"Error saving passphrase to keychain" = "Hiba történt a jelmondat kulcstartóba történő mentésekor"; /* alert title */ -"Error saving servers" = "Hiba a kiszolgálók mentésekor"; +"Error saving servers" = "Hiba történt a kiszolgálók mentésekor"; /* when migrating */ -"Error saving settings" = "Hiba a beállítások mentésekor"; +"Error saving settings" = "Hiba történt a beállítások mentésekor"; /* No comment provided by engineer. */ -"Error saving user password" = "Hiba a felhasználó jelszavának mentésekor"; +"Error saving user password" = "Hiba történt a felhasználó jelszavának mentésekor"; /* No comment provided by engineer. */ -"Error scanning code: %@" = "Hiba a kód beolvasá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 delivery receipts!" = "Hiba történt a kézbesítési jelentések beállításakor!"; /* No comment provided by engineer. */ -"Error starting chat" = "Hiba a csevegés elindításakor"; +"Error starting chat" = "Hiba történt a csevegés elindításakor"; /* No comment provided by engineer. */ -"Error stopping chat" = "Hiba a csevegés megállításakor"; +"Error stopping chat" = "Hiba történt a csevegés megállításakor"; /* No comment provided by engineer. */ -"Error switching profile" = "Hiba a profilváltáskor"; +"Error switching profile" = "Hiba történt a profilváltáskor"; /* alertTitle */ -"Error switching profile!" = "Hiba a profilváltáskor!"; +"Error switching profile!" = "Hiba történt a profilváltáskor!"; /* No comment provided by engineer. */ -"Error synchronizing connection" = "Hiba a kapcsolat szinkronizálásakor"; +"Error synchronizing connection" = "Hiba történt a kapcsolat szinkronizálásakor"; /* No comment provided by engineer. */ -"Error testing server connection" = "Hiba a kiszolgáló kapcsolatának tesztelésekor"; +"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"; +"Error updating group link" = "Hiba történt a csoporthivatkozás frissítésekor"; /* No comment provided by engineer. */ -"Error updating message" = "Hiba az üzenet frissítésekor"; +"Error updating message" = "Hiba történt az üzenet frissítésekor"; /* alert title */ -"Error updating server" = "Hiba a kiszolgáló frissítésekor"; +"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: %@"; /* No comment provided by engineer. */ @@ -2373,9 +2346,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"; @@ -2434,7 +2404,7 @@ "File errors:\n%@" = "Fájlhiba:\n%@"; /* file error text */ -"File is blocked by server operator:\n(info.reason.text)." = "A kiszolgáló üzemeltetője letiltotta a fájlt:\n(info.reason.text)."; +"File is blocked by server operator:\n%@." = "A kiszolgáló üzemeltetője letiltotta a fájlt:\n%@."; /* file error text */ "File not found - most likely file was deleted or cancelled." = "A fájl nem található – valószínűleg a fájlt törölték vagy visszavonták."; @@ -2514,9 +2484,6 @@ /* No comment provided by engineer. */ "For all moderators" = "Az összes moderátor számára"; -/* No comment provided by engineer. */ -"for better metadata privacy." = "a metaadatok jobb védelme érdekében."; - /* servers error */ "For chat profile %@:" = "A(z) %@ nevű csevegési profilhoz:"; @@ -2524,7 +2491,7 @@ "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 partnere 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"; @@ -2539,7 +2506,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"; @@ -2548,7 +2515,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."; @@ -2566,7 +2533,7 @@ "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."; +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "A(z) %@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %@ célkiszolgálóhoz. Próbálja meg később."; /* No comment provided by engineer. */ "Forwarding server address is incompatible with network settings: %@." = "A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; @@ -2704,7 +2671,7 @@ "Hide profile" = "Profil elrejtése"; /* No comment provided by engineer. */ -"Hide:" = "Elrejtés:"; +"Hide:" = "Elrejtve:"; /* No comment provided by engineer. */ "History" = "Előzmények"; @@ -2773,7 +2740,7 @@ "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"; @@ -2836,7 +2803,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ási 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"; @@ -2962,10 +2929,10 @@ "invited %@" = "meghívta őt: %@"; /* chat list item title */ -"invited to connect" = "meghívta, hogy csatlakozzon"; +"invited to connect" = "Függőben lévő meghívó"; /* rcv group event chat item */ -"invited via your group link" = "meghí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."; @@ -2992,7 +2959,7 @@ "It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt."; /* No comment provided by engineer. */ -"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Az üzenet visszafejtése sikertelen volt, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; /* No comment provided by engineer. */ "It protects your IP address and connections." = "Védi az IP-címét és a kapcsolatait."; @@ -3031,7 +2998,7 @@ "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!"; +"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"; @@ -3046,7 +3013,7 @@ "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 connections" = "Kapcsolatok megtartása"; @@ -3070,13 +3037,13 @@ "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"; @@ -3106,7 +3073,7 @@ "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..." = "Listanév…"; +"List name..." = "Lista neve…"; /* No comment provided by engineer. */ "LIVE" = "ÉLŐ"; @@ -3193,7 +3160,7 @@ "Member will be removed from chat - this cannot be undone!" = "A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ -"Member will be removed from group - this cannot be undone!" = "A tag eltávolítása a csoportból – ez a művelet nem vonható vissza!"; +"Member will be removed from group - this cannot be undone!" = "A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ "Members can add message reactions." = "A tagok reakciókat adhatnak hozzá az üzenetekhez."; @@ -3292,7 +3259,7 @@ "Messages & files" = "Üzenetek és fájlok"; /* No comment provided by engineer. */ -"Messages from %@ will be shown!" = "Az összes %@ által írt üzenet meg fog jelenni!"; +"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."; @@ -3307,10 +3274,10 @@ "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**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve."; /* No comment provided by engineer. */ -"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, fájlok és hívások **végpontok közötti kvantumálló titkosítással** sérülés utáni titkosságvédelemmel, visszautasítással és feltörés utáni helyreállítással vannak védve."; +"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve."; /* No comment provided by engineer. */ "Migrate device" = "Eszköz átköltöztetése"; @@ -3337,7 +3304,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)."; @@ -3409,7 +3376,7 @@ "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ó"; @@ -3421,7 +3388,7 @@ "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"; @@ -3442,7 +3409,7 @@ "New chat experience 🎉" = "Új csevegési élmény 🎉"; /* notification */ -"New contact request" = "Új kapcsolatkérés"; +"New contact request" = "Új meghívási kérés"; /* notification */ "New contact:" = "Új kapcsolat:"; @@ -3451,7 +3418,7 @@ "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"; @@ -3619,8 +3586,8 @@ "observer" = "megfigyelő"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "kikapcsolva"; /* blur media */ @@ -3645,7 +3612,7 @@ "on" = "bekapcsolva"; /* No comment provided by engineer. */ -"One-time invitation link" = "Egyszer használható meghívási 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."; @@ -3710,7 +3677,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Csak a partnere tud hangüzeneteket küldeni."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Megnyitás"; /* No comment provided by engineer. */ @@ -3728,21 +3695,12 @@ /* No comment provided by engineer. */ "Open group" = "Csoport megnyitása"; -/* No comment provided by engineer. */ -"Open link" = "Hivatkozás megnyitása"; - -/* No comment provided by engineer. */ -"Open links from chat list" = "Hivatkozás megnyitása a csevegési listából"; - /* authentication reason */ "Open migration to another device" = "Átköltöztetés indítása egy másik eszközre"; /* No comment provided by engineer. */ "Open Settings" = "Beállítások megnyitása"; -/* No comment provided by engineer. */ -"Open web link?" = "Webhivatkozás megnyitása?"; - /* No comment provided by engineer. */ "Opening app…" = "Az alkalmazás megnyitása…"; @@ -3830,9 +3788,15 @@ /* No comment provided by engineer. */ "peer-to-peer" = "egyenrangú"; +/* No comment provided by engineer. */ +"pending" = "függőben"; + /* No comment provided by engineer. */ "Pending" = "Függőben"; +/* No comment provided by engineer. */ +"pending approval" = "jóváhagyásra vár"; + /* No comment provided by engineer. */ "Periodic" = "Időszakos"; @@ -3864,7 +3828,7 @@ "Please check that you used the correct link or ask your contact to send you another one." = "Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg a partnerét, hogy küldjön egy másikat."; /* No comment provided by engineer. */ -"Please check your network connection with %@ and try again." = "Ellenőrizze a hálózati kapcsolatát a következővel: %@, és próbálja újra."; +"Please check your network connection with %@ and try again." = "Ellenőrizze a hálózati kapcsolatát a vele: %@, és próbálja újra."; /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Ellenőrizze a saját- és a partnere beállításait."; @@ -3936,10 +3900,16 @@ "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 szerver üzemeltetői számára."; /* No comment provided by engineer. */ "Private filenames" = "Privát fájlnevek"; @@ -3978,7 +3948,7 @@ "Profile theme" = "Profiltéma"; /* alert message */ -"Profile update will be sent to your contacts." = "A profilfrissítés el lesz küldve az Ön partnerei 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."; @@ -4026,7 +3996,7 @@ "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"; @@ -4044,10 +4014,10 @@ "Push server" = "Push-kiszolgáló"; /* chat item text */ -"quantum resistant e2e encryption" = "végpontok közötti kvantumálló titkosítás"; +"quantum resistant e2e encryption" = "végpontok közötti kvantumbiztos titkosítás"; /* No comment provided by engineer. */ -"Quantum resistant encryption" = "Kvantumálló titkosítás"; +"Quantum resistant encryption" = "Kvantumbiztos titkosítás"; /* No comment provided by engineer. */ "Rate the app" = "Értékelje az alkalmazást"; @@ -4137,16 +4107,16 @@ "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" = "Bejegyzés frissítve"; @@ -4161,20 +4131,23 @@ "Register" = "Regisztrálás"; /* token info */ -"Register notification token?" = "Értesítési token regisztrálása?"; +"Register notification token?" = "Regisztrálja az értesítési tokent?"; /* token status text */ "Registered" = "Regisztrálva"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Elutasítás"; /* No comment provided by engineer. */ "Reject (sender NOT notified)" = "Elutasítás (a feladó NEM kap értesítést)"; /* No comment provided by engineer. */ -"Reject contact request" = "Kapcsolatkérés elutasítása"; +"Reject contact request" = "Meghívási kérés elutasítása"; + +/* No comment provided by engineer. */ +"rejected" = "elutasítva"; /* call status */ "rejected call" = "elutasított hívás"; @@ -4189,7 +4162,7 @@ "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"; @@ -4198,10 +4171,10 @@ "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"; @@ -4225,10 +4198,10 @@ "Renegotiate encryption" = "Titkosítás újraegyeztetése"; /* No comment provided by engineer. */ -"Renegotiate encryption?" = "Titkosítás újraegyeztetése?"; +"Renegotiate encryption?" = "Újraegyezteti a titkosítást?"; /* No comment provided by engineer. */ -"Repeat connection request?" = "Kapcsolatkérés megismétlése?"; +"Repeat connection request?" = "Megismétli a meghívási kérést?"; /* No comment provided by engineer. */ "Repeat download" = "Letöltés ismét"; @@ -4237,7 +4210,7 @@ "Repeat import" = "Importálás ismét"; /* No comment provided by engineer. */ -"Repeat join request?" = "Csatlakozáskérés megismétlése?"; +"Repeat join request?" = "Megismétli a meghívási kérést?"; /* No comment provided by engineer. */ "Repeat upload" = "Feltöltés ismét"; @@ -4267,7 +4240,7 @@ "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: (text.isEmpty ? reason.text : text)" = "Jelentés: (text.isEmpty ? reason.text : text)"; +"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."; @@ -4276,7 +4249,7 @@ "Reports" = "Jelentések"; /* chat list item title */ -"requested to connect" = "kérelmezve a kapcsolódáshoz"; +"requested to connect" = "Függőben lévő meghívási kérelem"; /* No comment provided by engineer. */ "Required" = "Szükséges"; @@ -4291,7 +4264,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"; @@ -4318,10 +4291,10 @@ "Restore database backup" = "Adatbázismentés visszaállítása"; /* No comment provided by engineer. */ -"Restore database backup?" = "Adatbázismentés visszaállítása?"; +"Restore database backup?" = "Visszaállítja az adatbázismentést?"; /* No comment provided by engineer. */ -"Restore database error" = "Hiba az adatbázis visszaállításakor"; +"Restore database error" = "Hiba történt az adatbázis visszaállításakor"; /* No comment provided by engineer. */ "Retry" = "Újrapróbálkozás"; @@ -4332,9 +4305,6 @@ /* 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"; - /* No comment provided by engineer. */ "Revoke" = "Visszavonás"; @@ -4342,7 +4312,7 @@ "Revoke file" = "Fájl visszavonása"; /* No comment provided by engineer. */ -"Revoke file?" = "Fájl visszavonása?"; +"Revoke file?" = "Visszavonja a fájlt?"; /* No comment provided by engineer. */ "Role" = "Szerepkör"; @@ -4357,7 +4327,7 @@ "Safer groups" = "Biztonságosabb csoportok"; /* alert button - chat item action */ +chat item action */ "Save" = "Mentés"; /* alert button */ @@ -4388,7 +4358,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"; @@ -4397,13 +4367,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"; @@ -4439,7 +4409,7 @@ "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 a partnere alkalmazásából."; @@ -4541,10 +4511,10 @@ "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"; @@ -4559,7 +4529,7 @@ "Send receipts" = "Kézbesítési jelentések küldése"; /* No comment provided by engineer. */ -"Send them from gallery or custom keyboards." = "Küldje el őket galériából vagy egyedi billentyűzetekről."; +"Send them from gallery or custom keyboards." = "Küldje el őket a galériából vagy az egyéni billentyűzetekről."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Legfeljebb az utolsó 100 üzenet elküldése az új tagok számára."; @@ -4568,7 +4538,7 @@ "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üldője törölhette a meghívási kérést."; /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "A kézbesítési jelentések küldése engedélyezve lesz az összes látható csevegési profilban lévő összes partnere számára."; @@ -4643,7 +4613,7 @@ "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 módosult."; @@ -4733,14 +4703,14 @@ "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ó meghívási 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ási 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"; @@ -4764,7 +4734,7 @@ "Share SimpleX address on social media." = "SimpleX-cím megosztása a közösségi médiában."; /* No comment provided by engineer. */ -"Share this 1-time invite link" = "Ennek az egyszer használható meghívási hivatkozásnak a 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 SimpleXben"; @@ -4772,6 +4742,9 @@ /* No comment provided by engineer. */ "Share with contacts" = "Megosztás a partnerekkel"; +/* No comment provided by engineer. */ +"Short link" = "Rövid hivatkozás"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Egy „→” jel megjelenítése a privát útválasztáson keresztül küldött üzeneteknél."; @@ -4797,7 +4770,7 @@ "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"; @@ -4809,10 +4782,13 @@ "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ási hivatkozás biztonságosan megosztható bármilyen üzenetváltó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ási hivatkozás?"; +"SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó?"; + +/* simplex link type */ +"SimpleX channel link" = "SimpleX-csatornahivatkozás"; /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba."; @@ -4851,7 +4827,7 @@ "SimpleX Lock turned on" = "SimpleX-zár bekapcsolva"; /* simplex link type */ -"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívási 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."; @@ -4899,7 +4875,7 @@ "Somebody" = "Valaki"; /* blocking reason - report reason */ +report reason */ "Spam" = "Kéretlen tartalom"; /* No comment provided by engineer. */ @@ -4912,7 +4888,7 @@ "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"; @@ -4936,22 +4912,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"; @@ -4987,7 +4963,7 @@ "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ási 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"; @@ -5026,10 +5002,10 @@ "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 timeout" = "TCP-kapcsolat időtúllépése"; /* No comment provided by engineer. */ "TCP port for messaging" = "TCP-port az üzenetváltáshoz"; @@ -5065,13 +5041,13 @@ "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!"; /* 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 meghívási kéréseket kap – ezt a beállítások menüben engedélyezheti."; /* No comment provided by engineer. */ "The app protects your privacy by using different operators in each conversation." = "Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ."; @@ -5086,7 +5062,7 @@ "The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás."; /* No comment provided by engineer. */ -"The connection reached the limit of undelivered messages, your contact may be offline." = "A kapcsolat elérte a kézbesítetlen üzenetek számának határát, az Ön partnere 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!"; @@ -5125,14 +5101,11 @@ "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 a partnereivel van megosztva."; +"Your profile is stored on your device and only shared with your contacts." = "A profilja csak a partnereivel van megosztva."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**."; -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k) 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!"; @@ -5182,7 +5155,7 @@ "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 kvantumálló titkosí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 a partner"; @@ -5200,10 +5173,13 @@ "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ási hivatkozása!"; +"This is your own one-time link!" = "Ez a saját egyszer használható meghívója!"; /* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Ez az Ön SimpleX-címe!"; +"This is your own SimpleX address!" = "Ez a saját SimpleX-címe!"; + +/* No comment provided by engineer. */ +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Ez a hivatkozás újabb alkalmazásverziót igényel. Frissítse az alkalmazást vagy kérjen egy kompatibilis hivatkozást a partnerétől."; /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén."; @@ -5242,7 +5218,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 Ön adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához."; /* No comment provided by engineer. */ "To receive" = "A fogadáshoz"; @@ -5323,7 +5299,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"; @@ -5380,7 +5356,7 @@ "Unlink" = "Szétkapcsolás"; /* No comment provided by engineer. */ -"Unlink desktop?" = "Számítógép leválasztása?"; +"Unlink desktop?" = "Leválasztja a számítógépet?"; /* No comment provided by engineer. */ "Unlock" = "Feloldás"; @@ -5397,6 +5373,9 @@ /* swipe action */ "Unread" = "Olvasatlan"; +/* No comment provided by engineer. */ +"Unsupported connection link" = "Nem támogatott kapcsolattartási hivatkozás"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára."; @@ -5407,10 +5386,13 @@ "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 módosítá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"; @@ -5487,6 +5469,9 @@ /* No comment provided by engineer. */ "Use servers" = "Kiszolgálók használata"; +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Rövid hivatkozások használata (béta)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "SimpleX Chat-kiszolgálók használata?"; @@ -5496,6 +5481,9 @@ /* No comment provided by engineer. */ "Use TCP port %@ when no port is specified." = "A következő TCP-port használata, amikor nincs port megadva: %@."; +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "A 443-as TCP-port használata kizárólag az előre beállított kiszolgálokhoz."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Használja az alkalmazást hívás közben."; @@ -5551,13 +5539,13 @@ "via group link" = "a csoporthivatkozáson keresztül"; /* chat list item description */ -"via one-time link" = "egy egyszer használható meghívási hivatkozáson keresztül"; +"via one-time link" = "egy egyszer használható meghívón keresztül"; /* No comment provided by engineer. */ "via relay" = "egy továbbítókiszolgálón keresztül"; /* No comment provided by engineer. */ -"Via secure quantum resistant protocol." = "Biztonságos kvantumálló-protokollon keresztül."; +"Via secure quantum resistant protocol." = "Biztonságos kvantumbiztos protokollon keresztül."; /* No comment provided by engineer. */ "video" = "videó"; @@ -5715,9 +5703,6 @@ /* pref value */ "yes" = "igen"; -/* No comment provided by engineer. */ -"Yes" = "Igen"; - /* No comment provided by engineer. */ "you" = "Ön"; @@ -5731,7 +5716,7 @@ "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ódott a következőhöz: %@."; @@ -5743,7 +5728,7 @@ "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ási hivatkozáson keresztül!"; +"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."; @@ -5758,7 +5743,7 @@ "You are already joining the group via this link." = "A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül."; /* No comment provided by engineer. */ -"You are already joining the group!\nRepeat join request?" = "A csatlakozás már folyamatban van a csoporthoz!\nCsatlakozáskérés megismétlése?"; +"You are already joining the group!\nRepeat join request?" = "A csatlakozás már folyamatban van a csoporthoz!\nMegismétli a meghívási kérést?"; /* No comment provided by engineer. */ "You are connected to the server used to receive messages from this contact." = "Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál."; @@ -5784,9 +5769,6 @@ /* 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” menüben 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."; @@ -5863,10 +5845,10 @@ "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!"; +"You have already requested connection via this address!" = "Már küldött egy meghívási kérést ezen a címen keresztül!"; /* No comment provided by engineer. */ -"You have already requested connection!\nRepeat connection request?" = "Már küldött egy kapcsolódási kérelmet!\nKapcsolatkérés megismétlése?"; +"You have already requested connection!\nRepeat connection request?" = "Ön már küldött egy meghívási kérést!\nMegismétli a meghívási kérést?"; /* No comment provided by engineer. */ "You have 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."; @@ -5908,10 +5890,10 @@ "You sent group invitation" = "Csoportmeghívó elküldve"; /* chat list item description */ -"you shared one-time link" = "Ön egy egyszer használható meghívási 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ási 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."; @@ -5926,7 +5908,7 @@ "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 el lesz fogadva, várjon, vagy ellenőrizze később!"; +"You will be connected when your connection request is accepted, please wait or check later!" = "Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később!"; /* No comment provided by engineer. */ "You will be connected when your contact's device is online, please wait or check later!" = "Akkor lesz kapcsolódva, amikor a partnerének az eszköze online lesz, várjon, vagy ellenőrizze később!"; @@ -5953,7 +5935,7 @@ "you: " = "Ön: "; /* No comment provided by engineer. */ -"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Egy olyan partnerét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a 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"; @@ -6013,10 +5995,10 @@ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját."; /* alert message */ -"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja módosult. Ha elmenti, a profilfrissítés el lesz küldve az Ön partnerei számára."; +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja módosult. Ha elmenti, a profilfrissítés el lesz küldve a partnerei számára."; /* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "A profilja, a partnerei és az elküldött üzenetei az Ön eszközén vannak tárolva."; +"Your profile, contacts and delivered messages are stored on your device." = "A profilja, a partnerei és az elküldött üzenetei a saját eszközén vannak tárolva."; /* No comment provided by engineer. */ "Your random profile" = "Véletlenszerű profil"; @@ -6025,7 +6007,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"; @@ -6033,6 +6015,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/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 586ddd1ffa..b914a06079 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (può essere copiato)"; @@ -25,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)"; @@ -58,12 +34,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Dai una stella su GitHub](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"@'%@'" = "@'%@'"; - -/* 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."; @@ -299,7 +269,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 giorno"; /* time interval */ @@ -309,11 +279,11 @@ "1 minute" = "1 minuto"; /* delete after time - time interval */ +time interval */ "1 month" = "1 mese"; /* delete after time - time interval */ +time interval */ "1 week" = "1 settimana"; /* delete after time */ @@ -371,8 +341,8 @@ "Accent" = "Principale"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Accetta"; /* No comment provided by engineer. */ @@ -385,7 +355,7 @@ "Accept contact request from %@?" = "Accettare la richiesta di contatto da %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Accetta in incognito"; /* call status */ @@ -503,7 +473,7 @@ "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 (text), and the list deleted." = "Tutte le chat verranno rimosse dall'elenco (testo) e l'elenco eliminato."; +"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."; @@ -535,6 +505,9 @@ /* 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."; @@ -718,9 +691,6 @@ /* No comment provided by engineer. */ "Archiving database" = "Archiviazione del database"; -/* No comment provided by engineer. */ -"Ask" = "Chiedi"; - /* No comment provided by engineer. */ "Attach" = "Allega"; @@ -812,7 +782,7 @@ "Better notifications" = "Notifiche migliorate"; /* No comment provided by engineer. */ -"Better privacy and security" = "Privacy e sicurezza migliorate"; +"Better privacy and security" = "Privacy e sicurezza migliori"; /* No comment provided by engineer. */ "Better security ✅" = "Sicurezza migliorata ✅"; @@ -847,7 +817,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. */ @@ -892,6 +863,9 @@ /* 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"; @@ -932,7 +906,7 @@ "Can't message member" = "Impossibile inviare un messaggio al membro"; /* alert action - alert button */ +alert button */ "Cancel" = "Annulla"; /* No comment provided by engineer. */ @@ -990,7 +964,7 @@ "Change self-destruct mode" = "Cambia modalità di autodistruzione"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Cambia codice di autodistruzione"; /* chat item text */ @@ -1158,12 +1132,6 @@ /* No comment provided by engineer. */ "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: **%@**."; @@ -1176,6 +1144,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"; @@ -1585,7 +1556,7 @@ "decryption errors" = "errori di decifrazione"; /* delete after time - pref value */ +pref value */ "default (%@)" = "predefinito (%@)"; /* No comment provided by engineer. */ @@ -1595,7 +1566,7 @@ "default (yes)" = "predefinito (sì)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Elimina"; /* No comment provided by engineer. */ @@ -1911,7 +1882,7 @@ "Downgrade and open chat" = "Esegui downgrade e apri chat"; /* alert button - chat item action */ +chat item action */ "Download" = "Scarica"; /* No comment provided by engineer. */ @@ -1975,7 +1946,7 @@ "Enable camera access" = "Attiva l'accesso alla fotocamera"; /* 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. */ "Enable for all" = "Attiva per tutti"; @@ -2355,7 +2326,9 @@ /* No comment provided by engineer. */ "Error: " = "Errore: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Errore: %@"; /* No comment provided by engineer. */ @@ -2373,9 +2346,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"; @@ -2434,7 +2404,7 @@ "File errors:\n%@" = "Errori di file:\n%@"; /* file error text */ -"File is blocked by server operator:\n(info.reason.text)." = "Il file è bloccato dall'operatore del server:\n(info.reason.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."; @@ -2514,9 +2484,6 @@ /* No comment provided by engineer. */ "For all moderators" = "Per tutti i moderatori"; -/* No comment provided by engineer. */ -"for better metadata privacy." = "per una migliore privacy dei metadati."; - /* servers error */ "For chat profile %@:" = "Per il profilo di chat %@:"; @@ -3169,7 +3136,7 @@ "Member" = "Membro"; /* profile update event chat item */ -"member %@ changed to %@" = "membro %1$@ cambiato in %2$@"; +"member %@ changed to %@" = "il membro %1$@ è diventato %2$@"; /* rcv group event chat item */ "member connected" = "si è connesso/a"; @@ -3619,8 +3586,8 @@ "observer" = "osservatore"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "off"; /* blur media */ @@ -3710,7 +3677,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Solo il tuo contatto può inviare messaggi vocali."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Apri"; /* No comment provided by engineer. */ @@ -3728,21 +3695,12 @@ /* No comment provided by engineer. */ "Open group" = "Apri gruppo"; -/* No comment provided by engineer. */ -"Open link" = "Apri link"; - -/* No comment provided by engineer. */ -"Open links from chat list" = "Apri i link dall'elenco delle chat"; - /* authentication reason */ "Open migration to another device" = "Apri migrazione ad un altro dispositivo"; /* No comment provided by engineer. */ "Open Settings" = "Apri le impostazioni"; -/* No comment provided by engineer. */ -"Open web link?" = "Aprire il link?"; - /* No comment provided by engineer. */ "Opening app…" = "Apertura dell'app…"; @@ -3830,9 +3788,15 @@ /* 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. */ "Periodic" = "Periodicamente"; @@ -3938,9 +3902,15 @@ /* 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"; @@ -4167,7 +4137,7 @@ "Registered" = "Registrato"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Rifiuta"; /* No comment provided by engineer. */ @@ -4176,6 +4146,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Rifiuta la richiesta di contatto"; +/* No comment provided by engineer. */ +"rejected" = "rifiutato"; + /* call status */ "rejected call" = "chiamata rifiutata"; @@ -4267,7 +4240,7 @@ "Report violation: only group moderators will see it." = "Segnala violazione: solo i moderatori del gruppo lo vedranno."; /* report in notification */ -"Report: (text.isEmpty ? reason.text : text)" = "Segnalazione: (text.isEmpty ? reason.text : text)"; +"Report: %@" = "Segnalazione: %@"; /* No comment provided by engineer. */ "Reporting messages to moderators is prohibited." = "È vietato segnalare messaggi ai moderatori."; @@ -4332,9 +4305,6 @@ /* No comment provided by engineer. */ "Review conditions" = "Leggi le condizioni"; -/* No comment provided by engineer. */ -"Review later" = "Leggi più tardi"; - /* No comment provided by engineer. */ "Revoke" = "Revoca"; @@ -4357,7 +4327,7 @@ "Safer groups" = "Gruppi più sicuri"; /* alert button - chat item action */ +chat item action */ "Save" = "Salva"; /* alert button */ @@ -4733,7 +4703,7 @@ "Shape profile images" = "Forma delle immagini del profilo"; /* alert action - chat item action */ +chat item action */ "Share" = "Condividi"; /* No comment provided by engineer. */ @@ -4772,6 +4742,9 @@ /* No comment provided by engineer. */ "Share with contacts" = "Condividi con i contatti"; +/* No comment provided by engineer. */ +"Short link" = "Link breve"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Mostra → nei messaggi inviati via instradamento privato."; @@ -4814,6 +4787,9 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "Indirizzo SimpleX o link una tantum?"; +/* simplex link type */ +"SimpleX channel link" = "Link del canale SimpleX"; + /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell'app."; @@ -4899,7 +4875,7 @@ "Somebody" = "Qualcuno"; /* blocking reason - report reason */ +report reason */ "Spam" = "Spam"; /* No comment provided by engineer. */ @@ -5125,14 +5101,11 @@ "The old database was not removed during the migration, it can be deleted." = "Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; +"Your profile is stored on your device and only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; -/* No comment provided by engineer. */ -"The 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!"; @@ -5205,6 +5178,9 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Questo è il tuo indirizzo SimpleX!"; +/* No comment provided by engineer. */ +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Questo link richiede una versione più recente dell'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile."; + /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop."; @@ -5397,6 +5373,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."; @@ -5412,6 +5391,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"; @@ -5487,6 +5469,9 @@ /* No comment provided by engineer. */ "Use servers" = "Usa i server"; +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Usa link brevi (BETA)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Usare i server di SimpleX Chat?"; @@ -5496,6 +5481,9 @@ /* 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."; @@ -5715,9 +5703,6 @@ /* pref value */ "yes" = "sì"; -/* No comment provided by engineer. */ -"Yes" = "Sì"; - /* No comment provided by engineer. */ "you" = "tu"; @@ -5784,9 +5769,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."; @@ -6033,6 +6015,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Il tuo indirizzo SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "I tuoi server SMP"; - diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index b97da471b2..d214f88e1c 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.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)"; @@ -287,7 +266,7 @@ "0s" = "0秒"; /* delete after time - time interval */ +time interval */ "1 day" = "1日"; /* time interval */ @@ -297,11 +276,11 @@ "1 minute" = "1分"; /* delete after time - time interval */ +time interval */ "1 month" = "1ヶ月"; /* delete after time - time interval */ +time interval */ "1 week" = "1週間"; /* No comment provided by engineer. */ @@ -350,8 +329,8 @@ "above, then choose:" = "上で選んでください:"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "承諾"; /* No comment provided by engineer. */ @@ -361,7 +340,7 @@ "Accept contact request from %@?" = "%@ からの連絡要求を受け入れますか?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "シークレットモードで承諾"; /* call status */ @@ -668,7 +647,7 @@ "Can't invite contacts!" = "連絡先を招待できません!"; /* alert action - alert button */ +alert button */ "Cancel" = "中止"; /* feature offered item */ @@ -708,7 +687,7 @@ "Change self-destruct mode" = "自己破壊モードの変更"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "自己破壊パスコードを変更する"; /* chat item text */ @@ -978,6 +957,9 @@ /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "[デスクトップアプリ](https://simplex.chat/downloads/)で新しいプロファイルを作成します。 💻"; +/* No comment provided by engineer. */ +"Create profile" = "プロフィールを作成する"; + /* server test step */ "Create queue" = "キューの作成"; @@ -1087,7 +1069,7 @@ "Decryption error" = "復号化エラー"; /* delete after time - pref value */ +pref value */ "default (%@)" = "デフォルト (%@)"; /* No comment provided by engineer. */ @@ -1097,7 +1079,7 @@ "default (yes)" = "デフォルト(はい)"; /* alert action - swipe action */ +swipe action */ "Delete" = "削除"; /* No comment provided by engineer. */ @@ -1586,7 +1568,9 @@ /* No comment provided by engineer. */ "Error: " = "エラー : "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "エラー : %@"; /* No comment provided by engineer. */ @@ -1598,9 +1582,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "会話中に無効になっている場合でも。"; -/* No comment provided by engineer. */ -"event happened" = "イベント発生"; - /* No comment provided by engineer. */ "Exit without saving" = "保存せずに閉じる"; @@ -2313,8 +2294,8 @@ "observer" = "オブザーバー"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "オフ"; /* blur media */ @@ -2389,7 +2370,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "音声メッセージを送れるのはあなたの連絡相手だけです。"; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "開く"; /* No comment provided by engineer. */ @@ -2621,7 +2602,7 @@ "Reduced battery usage" = "電池使用量低減"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "拒否"; /* No comment provided by engineer. */ @@ -2721,7 +2702,7 @@ "Run chat" = "チャット起動"; /* alert button - chat item action */ +chat item action */ "Save" = "保存"; /* alert button */ @@ -2908,7 +2889,7 @@ "Settings" = "設定"; /* alert action - chat item action */ +chat item action */ "Share" = "共有する"; /* No comment provided by engineer. */ @@ -3131,7 +3112,7 @@ "The old database was not removed during the migration, it can be deleted." = "古いデータベースは移行時に削除されなかったので、削除することができます。"; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "プロフィールは連絡先にしか共有されません。"; +"Your profile is stored on your device and only shared with your contacts." = "プロフィールは連絡先にしか共有されません。"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "長らくお待たせしました! ✅"; @@ -3628,6 +3609,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 1b2ab30025..232de56641 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1,12 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (kan gekopieerd worden)"; @@ -25,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)"; @@ -58,12 +34,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"@'%@'" = "@'%@'"; - -/* 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."; @@ -299,7 +269,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 dag"; /* time interval */ @@ -309,11 +279,11 @@ "1 minute" = "1 minuut"; /* delete after time - time interval */ +time interval */ "1 month" = "1 maand"; /* delete after time - time interval */ +time interval */ "1 week" = "1 week"; /* delete after time */ @@ -371,8 +341,8 @@ "Accent" = "Accent"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Accepteer"; /* No comment provided by engineer. */ @@ -385,7 +355,7 @@ "Accept contact request from %@?" = "Accepteer contactverzoek van %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Accepteer incognito"; /* call status */ @@ -503,7 +473,7 @@ "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 (text), and the list deleted." = "Alle chats worden uit de lijst (tekst) verwijderd en de lijst wordt verwijderd."; +"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."; @@ -718,9 +688,6 @@ /* No comment provided by engineer. */ "Archiving database" = "Database archiveren"; -/* No comment provided by engineer. */ -"Ask" = "Vragen"; - /* No comment provided by engineer. */ "Attach" = "Bijvoegen"; @@ -847,7 +814,8 @@ /* rcv group event chat item */ "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. */ @@ -892,6 +860,9 @@ /* 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"; @@ -932,7 +903,7 @@ "Can't message member" = "Kan geen bericht sturen naar lid"; /* alert action - alert button */ +alert button */ "Cancel" = "Annuleren"; /* No comment provided by engineer. */ @@ -990,7 +961,7 @@ "Change self-destruct mode" = "Zelfvernietigings modus wijzigen"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Zelfvernietigings code wijzigen"; /* chat item text */ @@ -1158,12 +1129,6 @@ /* No comment provided by engineer. */ "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): **%@**."; @@ -1176,6 +1141,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"; @@ -1585,7 +1553,7 @@ "decryption errors" = "decoderingsfouten"; /* delete after time - pref value */ +pref value */ "default (%@)" = "standaard (%@)"; /* No comment provided by engineer. */ @@ -1595,7 +1563,7 @@ "default (yes)" = "standaard (ja)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Verwijderen"; /* No comment provided by engineer. */ @@ -1899,7 +1867,7 @@ "Don't enable" = "Niet inschakelen"; /* No comment provided by engineer. */ -"Don't miss important messages." = "‐Mis geen belangrijke berichten."; +"Don't miss important messages." = "Mis geen belangrijke berichten."; /* No comment provided by engineer. */ "Don't show again" = "Niet meer weergeven"; @@ -1911,7 +1879,7 @@ "Downgrade and open chat" = "Downgraden en chat openen"; /* alert button - chat item action */ +chat item action */ "Download" = "Downloaden"; /* No comment provided by engineer. */ @@ -1975,7 +1943,7 @@ "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"; @@ -2194,7 +2162,7 @@ "Error creating profile!" = "Fout bij aanmaken van profiel!"; /* No comment provided by engineer. */ -"Error creating report" = "Fout bij maken van rapport"; +"Error creating report" = "Fout bij het rapporteren"; /* No comment provided by engineer. */ "Error decrypting file" = "Fout bij het ontsleutelen van bestand"; @@ -2355,7 +2323,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. */ @@ -2373,9 +2343,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"; @@ -2434,7 +2401,7 @@ "File errors:\n%@" = "Bestandsfouten:\n%@"; /* file error text */ -"File is blocked by server operator:\n(info.reason.text)." = "Bestand is geblokkeerd door serveroperator:\n(info.reason.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."; @@ -2514,9 +2481,6 @@ /* No comment provided by engineer. */ "For all moderators" = "Voor alle moderators"; -/* No comment provided by engineer. */ -"for better metadata privacy." = "voor betere privacy van metagegevens."; - /* servers error */ "For chat profile %@:" = "Voor chatprofiel %@:"; @@ -3619,8 +3583,8 @@ "observer" = "Waarnemer"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "uit"; /* blur media */ @@ -3710,7 +3674,7 @@ /* 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. */ @@ -3728,21 +3692,12 @@ /* No comment provided by engineer. */ "Open group" = "Open groep"; -/* No comment provided by engineer. */ -"Open link" = "Link openen"; - -/* No comment provided by engineer. */ -"Open links from chat list" = "Open links van chatlijst"; - /* authentication reason */ "Open migration to another device" = "Open de migratie naar een ander apparaat"; /* No comment provided by engineer. */ "Open Settings" = "Open instellingen"; -/* No comment provided by engineer. */ -"Open web link?" = "Weblink openen?"; - /* No comment provided by engineer. */ "Opening app…" = "App openen…"; @@ -3830,9 +3785,15 @@ /* 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. */ "Periodic" = "Periodiek"; @@ -3938,9 +3899,15 @@ /* 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"; @@ -4167,7 +4134,7 @@ "Registered" = "Geregistreerd"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Afwijzen"; /* No comment provided by engineer. */ @@ -4176,6 +4143,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Contactverzoek afwijzen"; +/* No comment provided by engineer. */ +"rejected" = "afgewezen"; + /* call status */ "rejected call" = "geweigerde oproep"; @@ -4246,7 +4216,7 @@ "Reply" = "Antwoord"; /* chat item action */ -"Report" = "Rapport"; +"Report" = "rapporteren"; /* report reason */ "Report content: only group moderators will see it." = "Inhoud melden: alleen groepsmoderators kunnen dit zien."; @@ -4267,7 +4237,7 @@ "Report violation: only group moderators will see it." = "Rapporteer overtreding: alleen groepsmoderators kunnen dit zien."; /* report in notification */ -"Report: (text.isEmpty ? reason.text : text)" = "Rapport: (text.isEmpty ? reason.text : text)"; +"Report: %@" = "rapporteer: %@"; /* No comment provided by engineer. */ "Reporting messages to moderators is prohibited." = "Het is niet toegestaan om berichten aan moderators te melden."; @@ -4332,9 +4302,6 @@ /* No comment provided by engineer. */ "Review conditions" = "Voorwaarden bekijken"; -/* No comment provided by engineer. */ -"Review later" = "Later beoordelen"; - /* No comment provided by engineer. */ "Revoke" = "Intrekken"; @@ -4357,7 +4324,7 @@ "Safer groups" = "Veiligere groepen"; /* alert button - chat item action */ +chat item action */ "Save" = "Opslaan"; /* alert button */ @@ -4550,7 +4517,7 @@ "Send notifications" = "Meldingen verzenden"; /* No comment provided by engineer. */ -"Send private reports" = "Verstuur rapporten privé"; +"Send private reports" = "Rapporteer privé"; /* No comment provided by engineer. */ "Send questions and ideas" = "Stuur vragen en ideeën"; @@ -4733,7 +4700,7 @@ "Shape profile images" = "Vorm profiel afbeeldingen"; /* alert action - chat item action */ +chat item action */ "Share" = "Deel"; /* No comment provided by engineer. */ @@ -4899,7 +4866,7 @@ "Somebody" = "Iemand"; /* blocking reason - report reason */ +report reason */ "Spam" = "Spam"; /* No comment provided by engineer. */ @@ -5125,14 +5092,11 @@ "The old database was not removed during the migration, it can be deleted." = "De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; +"Your profile is stored on your device and only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; -/* No comment provided by engineer. */ -"The 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!"; @@ -5412,6 +5376,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"; @@ -5715,9 +5682,6 @@ /* pref value */ "yes" = "Ja"; -/* No comment provided by engineer. */ -"Yes" = "Ja"; - /* No comment provided by engineer. */ "you" = "jij"; @@ -5784,9 +5748,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."; @@ -6033,6 +5994,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 4c88e3dcaf..31a9b87662 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)"; @@ -55,12 +34,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Daj gwiazdkę na GitHub](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"@'%@'" = "@'%@'"; - -/* 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."; @@ -296,7 +269,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 dzień"; /* time interval */ @@ -306,11 +279,11 @@ "1 minute" = "1 minuta"; /* delete after time - time interval */ +time interval */ "1 month" = "1 miesiąc"; /* delete after time - time interval */ +time interval */ "1 week" = "1 tydzień"; /* delete after time */ @@ -368,8 +341,8 @@ "Accent" = "Akcent"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Akceptuj"; /* No comment provided by engineer. */ @@ -382,7 +355,7 @@ "Accept contact request from %@?" = "Zaakceptuj prośbę o kontakt od %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Akceptuj incognito"; /* call status */ @@ -497,7 +470,7 @@ "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 (text), and the list deleted." = "Wszystkie rozmowy zostaną usunięte z listy (text), a lista usunięta."; +"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."; @@ -709,9 +682,6 @@ /* No comment provided by engineer. */ "Archiving database" = "Archiwizowanie bazy danych"; -/* No comment provided by engineer. */ -"Ask" = "Zapytaj"; - /* No comment provided by engineer. */ "Attach" = "Dołącz"; @@ -832,7 +802,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. */ @@ -917,7 +888,7 @@ "Can't message member" = "Nie można wysłać wiadomości do członka"; /* alert action - alert button */ +alert button */ "Cancel" = "Anuluj"; /* No comment provided by engineer. */ @@ -972,7 +943,7 @@ "Change self-destruct mode" = "Zmień tryb samozniszczenia"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Zmień pin samozniszczenia"; /* chat item text */ @@ -1131,9 +1102,6 @@ /* No comment provided by engineer. */ "Conditions of use" = "Warunki użytkowania"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Warunki zostaną zaakceptowane dla aktywowanych operatorów po 30 dniach."; - /* No comment provided by engineer. */ "Configure ICE servers" = "Skonfiguruj serwery ICE"; @@ -1513,7 +1481,7 @@ "decryption errors" = "błąd odszyfrowywania"; /* delete after time - pref value */ +pref value */ "default (%@)" = "domyślne (%@)"; /* No comment provided by engineer. */ @@ -1523,7 +1491,7 @@ "default (yes)" = "domyślnie (tak)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Usuń"; /* No comment provided by engineer. */ @@ -1800,7 +1768,7 @@ "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. */ @@ -2199,7 +2167,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. */ @@ -2214,9 +2184,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"; @@ -3268,8 +3235,8 @@ "observer" = "obserwator"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "wyłączony"; /* blur media */ @@ -3350,7 +3317,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Tylko Twój kontakt może wysyłać wiadomości głosowe."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Otwórz"; /* No comment provided by engineer. */ @@ -3747,7 +3714,7 @@ "Reduced battery usage" = "Zmniejszone zużycie baterii"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Odrzuć"; /* No comment provided by engineer. */ @@ -3898,7 +3865,7 @@ "Safer groups" = "Bezpieczniejsze grupy"; /* alert button - chat item action */ +chat item action */ "Save" = "Zapisz"; /* alert button */ @@ -4250,7 +4217,7 @@ "Shape profile images" = "Kształtuj obrazy profilowe"; /* alert action - chat item action */ +chat item action */ "Share" = "Udostępnij"; /* No comment provided by engineer. */ @@ -4590,7 +4557,7 @@ "The old database was not removed during the migration, it can be deleted." = "Stara baza danych nie została usunięta podczas migracji, można ją usunąć."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Profil jest udostępniany tylko Twoim kontaktom."; +"Your profile is stored on your device and only shared with your contacts." = "Profil jest udostępniany tylko Twoim kontaktom."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Drugi tik, który przegapiliśmy! ✅"; @@ -5411,6 +5378,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Twój adres SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Twoje serwery SMP"; - diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 5d78aaa35b..cb837836ff 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 */ "%@:" = "%@:"; @@ -224,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 сообщений заблокировано"; @@ -290,7 +269,7 @@ "0s" = "0с"; /* delete after time - time interval */ +time interval */ "1 day" = "1 день"; /* time interval */ @@ -300,11 +279,11 @@ "1 minute" = "1 минута"; /* delete after time - time interval */ +time interval */ "1 month" = "1 месяц"; /* delete after time - time interval */ +time interval */ "1 week" = "1 неделю"; /* delete after time */ @@ -338,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" = "Прекратить"; @@ -362,8 +341,8 @@ "Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Принять"; /* No comment provided by engineer. */ @@ -376,7 +355,7 @@ "Accept contact request from %@?" = "Принять запрос на соединение от %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Принять инкогнито"; /* call status */ @@ -394,6 +373,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Ошибки подтверждения"; +/* token status text */ +"Active" = "Активный"; + /* No comment provided by engineer. */ "Active connections" = "Активные соединения"; @@ -490,6 +472,9 @@ /* 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." = "Все данные удаляются при его вводе."; @@ -497,10 +482,7 @@ "All data is kept private on your device." = "Все данные хранятся только на вашем устройстве."; /* No comment provided by engineer. */ -"All group members will remain connected." = "Все члены группы, которые соединились через эту ссылку, останутся в группе."; - -/* feature role */ -"all members" = "все члены"; +"All group members will remain connected." = "Все члены группы останутся соединены."; /* No comment provided by engineer. */ "All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах."; @@ -517,6 +499,9 @@ /* profile dropdown */ "All profiles" = "Все профили"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Все сообщения о нарушениях будут заархивированы для вас."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Все контакты, которые соединились через этот адрес, сохранятся."; @@ -551,7 +536,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." = "Разрешить посылать исчезающие сообщения."; @@ -562,6 +547,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." = "Разрешить посылать файлы и медиа."; @@ -634,6 +622,9 @@ /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Приложение шифрует новые локальные файлы (кроме видео)."; +/* No comment provided by engineer. */ +"App group:" = "Группа приложения:"; + /* No comment provided by engineer. */ "App icon" = "Иконка"; @@ -664,6 +655,12 @@ /* 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" = "Архивировать и загрузить"; @@ -671,14 +668,20 @@ "Archive contacts to chat later." = "Архивируйте контакты чтобы продолжить переписку."; /* No comment provided by engineer. */ -"Archive report" = "Архивировать отчет"; +"Archive report" = "Архивировать сообщение о нарушении"; /* No comment provided by engineer. */ -"Archive report?" = "Архивировать отчет?"; +"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" = "Подготовка архива"; @@ -757,6 +760,9 @@ /* No comment provided by engineer. */ "Better groups" = "Улучшенные группы"; +/* No comment provided by engineer. */ +"Better groups performance" = "Улучшенная производительность групп"; + /* No comment provided by engineer. */ "Better message dates." = "Улучшенные даты сообщений."; @@ -769,6 +775,9 @@ /* No comment provided by engineer. */ "Better notifications" = "Улучшенные уведомления"; +/* No comment provided by engineer. */ +"Better privacy and security" = "Улучшенная конфиденциальность и безопасность"; + /* No comment provided by engineer. */ "Better security ✅" = "Улучшенная безопасность ✅"; @@ -785,13 +794,13 @@ "Block for all" = "Заблокировать для всех"; /* No comment provided by engineer. */ -"Block group members" = "Блокируйте членов группы"; +"Block group members" = "Заблокировать членов группы"; /* No comment provided by engineer. */ "Block member" = "Заблокировать члена группы"; /* No comment provided by engineer. */ -"Block member for all?" = "Заблокировать члена для всех?"; +"Block member for all?" = "Заблокировать для всех?"; /* No comment provided by engineer. */ "Block member?" = "Заблокировать члена группы?"; @@ -802,7 +811,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. */ @@ -841,9 +851,15 @@ /* No comment provided by engineer. */ "Business chats" = "Бизнес разговоры"; +/* No comment provided by engineer. */ +"Businesses" = "Бизнесы"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА)."; +/* No comment provided by engineer. */ +"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Используя SimpleX Chat, Вы согласны:\n- отправлять только законные сообщения в публичных группах.\n- уважать других пользователей – не отправлять спам."; + /* No comment provided by engineer. */ "call" = "звонок"; @@ -872,7 +888,7 @@ "Can't call contact" = "Не удается позвонить контакту"; /* No comment provided by engineer. */ -"Can't call member" = "Не удается позвонить члену группы"; +"Can't call member" = "Не удаётся позвонить члену группы"; /* No comment provided by engineer. */ "Can't invite contact!" = "Нельзя пригласить контакт!"; @@ -881,10 +897,10 @@ "Can't invite contacts!" = "Нельзя пригласить контакты!"; /* No comment provided by engineer. */ -"Can't message member" = "Не удается написать члену группы"; +"Can't message member" = "Не удаётся отправить сообщение члену группы"; /* alert action - alert button */ +alert button */ "Cancel" = "Отменить"; /* No comment provided by engineer. */ @@ -911,6 +927,9 @@ /* No comment provided by engineer. */ "Change" = "Поменять"; +/* alert title */ +"Change automatic message deletion?" = "Измененить автоматическое удаление сообщений?"; + /* authentication reason */ "Change chat profiles" = "Поменять профили"; @@ -939,7 +958,7 @@ "Change self-destruct mode" = "Изменить режим самоуничтожения"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Изменить код самоуничтожения"; /* chat item text */ @@ -1059,6 +1078,12 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Очистить разговор?"; +/* No comment provided by engineer. */ +"Clear group?" = "Очистить группу?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Очистить или удалить группу?"; + /* No comment provided by engineer. */ "Clear private notes?" = "Очистить личные заметки?"; @@ -1074,6 +1099,9 @@ /* No comment provided by engineer. */ "colored" = "цвет"; +/* report reason */ +"Community guidelines violation" = "Нарушение правил группы"; + /* server test step */ "Compare file" = "Сравнение файла"; @@ -1098,12 +1126,6 @@ /* No comment provided by engineer. */ "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): **%@**." = "Условия будут приняты для оператора(ов): **%@**."; @@ -1116,6 +1138,9 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Настройка ICE серверов"; +/* No comment provided by engineer. */ +"Configure server operators" = "Настроить операторов серверов"; + /* No comment provided by engineer. */ "Confirm" = "Подтвердить"; @@ -1146,6 +1171,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Подтвердить загрузку"; +/* token status text */ +"Confirmed" = "Подтвержденный"; + /* server test step */ "Connect" = "Соединиться"; @@ -1245,6 +1273,9 @@ /* No comment provided by engineer. */ "Connection and servers status." = "Состояние соединения и серверов."; +/* No comment provided by engineer. */ +"Connection blocked" = "Соединение заблокировано"; + /* No comment provided by engineer. */ "Connection error" = "Ошибка соединения"; @@ -1254,12 +1285,21 @@ /* 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" = "Безопасность соединения"; @@ -1320,6 +1360,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" = "Продолжить"; @@ -1362,6 +1405,9 @@ /* No comment provided by engineer. */ "Create link" = "Создать ссылку"; +/* No comment provided by engineer. */ +"Create list" = "Создать список"; + /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Создайте новый профиль в [приложении для компьютера](https://simplex.chat/downloads/). 💻"; @@ -1504,7 +1550,7 @@ "decryption errors" = "ошибки расшифровки"; /* delete after time - pref value */ +pref value */ "default (%@)" = "по умолчанию (%@)"; /* No comment provided by engineer. */ @@ -1514,7 +1560,7 @@ "default (yes)" = "по умолчанию (да)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Удалить"; /* No comment provided by engineer. */ @@ -1541,6 +1587,9 @@ /* 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" = "Удалить профиль чата"; @@ -1595,6 +1644,9 @@ /* No comment provided by engineer. */ "Delete link?" = "Удалить ссылку?"; +/* alert title */ +"Delete list?" = "Удалить список?"; + /* No comment provided by engineer. */ "Delete member message?" = "Удалить сообщение участника?"; @@ -1625,6 +1677,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 сообщений за раз."; @@ -1725,14 +1780,17 @@ "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." = "Прямые сообщения между членами группы запрещены."; +"Direct messages between members are prohibited in this chat." = "Личные сообщения запрещены в этой группе."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Выключить (кроме исключений)"; +/* alert title */ +"Disable automatic message deletion?" = "Отключить автоматическое удаление сообщений?"; + +/* alert button */ +"Disable delete messages" = "Отключить удаление сообщений"; + /* No comment provided by engineer. */ "Disable for all" = "Выключить для всех"; @@ -1778,9 +1836,6 @@ /* No comment provided by engineer. */ "Do it later" = "Отложить"; -/* No comment provided by engineer. */ -"Do not send history to new members." = "Не отправлять историю новым членам."; - /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку."; @@ -1793,20 +1848,29 @@ /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "Не используйте SimpleX для экстренных звонков."; +/* No comment provided by engineer. */ +"Documents:" = "Документы:"; + /* No comment provided by engineer. */ "Don't create address" = "Не создавать адрес"; /* No comment provided by engineer. */ "Don't enable" = "Не включать"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "Не пропустите важные сообщения."; + /* No comment provided by engineer. */ "Don't show again" = "Не показывать"; +/* No comment provided by engineer. */ +"Done" = "Готово"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Откатить версию и открыть чат"; /* alert button - chat item action */ +chat item action */ "Download" = "Загрузить"; /* No comment provided by engineer. */ @@ -1869,9 +1933,6 @@ /* No comment provided by engineer. */ "Enable camera access" = "Включить доступ к камере"; -/* No comment provided by engineer. */ -"Enable Flux" = "Включить Flux"; - /* No comment provided by engineer. */ "Enable for all" = "Включить для всех"; @@ -1983,6 +2044,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "требуется новое соглашение о шифровании для %@"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Выполняется повторное согласование шифрования."; + /* No comment provided by engineer. */ "ended" = "завершён"; @@ -2037,9 +2101,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Ошибка при принятии запроса на соединение"; -/* No comment provided by engineer. */ -"Error adding member(s)" = "Ошибка при добавлении членов группы"; - /* alert title */ "Error adding server" = "Ошибка добавления сервера"; @@ -2058,6 +2119,9 @@ /* No comment provided by engineer. */ "Error changing to incognito!" = "Ошибка при смене на Инкогнито!"; +/* No comment provided by engineer. */ +"Error checking token status" = "Ошибка проверки статуса токена"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Ошибка подключения к пересылающему серверу %@. Попробуйте позже."; @@ -2070,8 +2134,8 @@ /* No comment provided by engineer. */ "Error creating group link" = "Ошибка при создании ссылки группы"; -/* No comment provided by engineer. */ -"Error creating member contact" = "Ошибка создания контакта с членом группы"; +/* alert title */ +"Error creating list" = "Ошибка создания списка"; /* No comment provided by engineer. */ "Error creating message" = "Ошибка создания сообщения"; @@ -2079,6 +2143,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Ошибка создания профиля!"; +/* No comment provided by engineer. */ +"Error creating report" = "Ошибка создания сообщения о нарушении"; + /* No comment provided by engineer. */ "Error decrypting file" = "Ошибка расшифровки файла"; @@ -2145,12 +2212,18 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "Ошибка переподключения к серверам"; -/* No comment provided by engineer. */ -"Error removing member" = "Ошибка при удалении члена группы"; +/* alert title */ +"Error registering for notifications" = "Ошибка регистрации для уведомлений"; + +/* alert title */ +"Error reordering lists" = "Ошибка сортировки списков"; /* No comment provided by engineer. */ "Error resetting statistics" = "Ошибка сброса статистики"; +/* alert title */ +"Error saving chat list" = "Ошибка сохранения списка чатов"; + /* No comment provided by engineer. */ "Error saving group profile" = "Ошибка при сохранении профиля группы"; @@ -2178,9 +2251,6 @@ /* No comment provided by engineer. */ "Error sending email" = "Ошибка отправки email"; -/* No comment provided by engineer. */ -"Error sending member contact invitation" = "Ошибка отправки приглашения члену группы"; - /* No comment provided by engineer. */ "Error sending message" = "Ошибка при отправке сообщения"; @@ -2202,6 +2272,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" = "Ошибка обновления ссылки группы"; @@ -2226,7 +2299,9 @@ /* No comment provided by engineer. */ "Error: " = "Ошибка: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Ошибка: %@"; /* No comment provided by engineer. */ @@ -2244,9 +2319,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" = "Выйти без сохранения"; @@ -2256,6 +2328,9 @@ /* No comment provided by engineer. */ "expired" = "истекло"; +/* token status text */ +"Expired" = "Истекший"; + /* No comment provided by engineer. */ "Export database" = "Экспорт архива чата"; @@ -2280,18 +2355,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." = "Файл не найден - скорее всего, файл был удален или отменен."; @@ -2365,10 +2452,7 @@ "Fix not supported by contact" = "Починка не поддерживается контактом"; /* No comment provided by engineer. */ -"Fix not supported by group member" = "Починка не поддерживается членом группы"; - -/* No comment provided by engineer. */ -"for better metadata privacy." = "для лучшей конфиденциальности метаданных."; +"For all moderators" = "Для всех модераторов"; /* servers error */ "For chat profile %@:" = "Для профиля чата %@:"; @@ -2379,6 +2463,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" = "Для доставки сообщений"; @@ -2442,15 +2529,15 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Полное имя (не обязательно)"; -/* No comment provided by engineer. */ -"Fully decentralized – visible only to members." = "Группа полностью децентрализована – она видна только членам."; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Полностью обновлены - работают в фоне!"; /* No comment provided by engineer. */ "Further reduced battery usage" = "Уменьшенное потребление батареи"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Уведомления, когда Вас упомянули."; + /* No comment provided by engineer. */ "GIFs and stickers" = "ГИФ файлы и стикеры"; @@ -2508,24 +2595,24 @@ /* No comment provided by engineer. */ "Group profile" = "Профиль группы"; -/* No comment provided by engineer. */ -"Group profile is stored on members' devices, not on the servers." = "Профиль группы хранится на устройствах членов, а не на серверах."; - /* snd group event chat item */ "group profile updated" = "профиль группы обновлен"; /* No comment provided by engineer. */ "Group welcome message" = "Приветственное сообщение группы"; -/* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "Группа будет удалена для всех членов - это действие нельзя отменить!"; - /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "Группа будет удалена для Вас - это действие нельзя отменить!"; +/* No comment provided by engineer. */ +"Groups" = "Группы"; + /* No comment provided by engineer. */ "Help" = "Помощь"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Помогайте администраторам модерировать их группы."; + /* No comment provided by engineer. */ "Hidden" = "Скрытое"; @@ -2550,9 +2637,6 @@ /* No comment provided by engineer. */ "History" = "История"; -/* No comment provided by engineer. */ -"History is not sent to new members." = "История не отправляется новым членам."; - /* time unit */ "hours" = "часов"; @@ -2562,6 +2646,9 @@ /* No comment provided by engineer. */ "How it helps privacy" = "Как это улучшает конфиденциальность"; +/* alert button */ +"How it works" = "Как это работает"; + /* No comment provided by engineer. */ "How SimpleX works" = "Как SimpleX работает"; @@ -2649,6 +2736,12 @@ /* No comment provided by engineer. */ "inactive" = "неактивен"; +/* report reason */ +"Inappropriate content" = "Неприемлемый контент"; + +/* report reason */ +"Inappropriate profile" = "Неприемлемый профиль"; + /* No comment provided by engineer. */ "Incognito" = "Инкогнито"; @@ -2715,6 +2808,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" = "ошибка чата"; @@ -2763,9 +2871,6 @@ /* No comment provided by engineer. */ "Invite friends" = "Пригласить друзей"; -/* No comment provided by engineer. */ -"Invite members" = "Пригласить членов группы"; - /* No comment provided by engineer. */ "Invite to chat" = "Пригласить в разговор"; @@ -2916,6 +3021,15 @@ /* No comment provided by engineer. */ "Linked desktops" = "Связанные компьютеры"; +/* swipe action */ +"List" = "Список"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Название списка и эмодзи должны быть разными для всех списков."; + +/* No comment provided by engineer. */ +"List name..." = "Имя списка..."; + /* No comment provided by engineer. */ "LIVE" = "LIVE"; @@ -2970,57 +3084,15 @@ /* blur media */ "Medium" = "Среднее"; -/* member role */ -"member" = "член группы"; - -/* No comment provided by engineer. */ -"Member" = "Член группы"; - -/* profile update event chat item */ -"member %@ changed to %@" = "член %1$@ изменился на %2$@"; - /* rcv group event chat item */ "member connected" = "соединен(а)"; -/* item status text */ -"Member inactive" = "Член неактивен"; +/* chat feature */ +"Member reports" = "Сообщения о нарушениях"; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All chat members will be notified." = "Роль участника будет изменена на \"%@\". Все участники разговора получат уведомление."; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Роль члена группы будет изменена на \"%@\". Все члены группы получат сообщение."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена группы будет изменена на \"%@\". Будет отправлено новое приглашение."; - -/* No comment provided by engineer. */ -"Member will be removed from chat - this cannot be undone!" = "Член будет удален из разговора - это действие нельзя отменить!"; - -/* No comment provided by engineer. */ -"Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!"; - -/* No comment provided by engineer. */ -"Members can add message reactions." = "Члены группы могут добавлять реакции на сообщения."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Члены группы могут необратимо удалять отправленные сообщения. (24 часа)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Члены группы могут посылать прямые сообщения."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Члены группы могут посылать исчезающие сообщения."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Члены группы могут слать файлы и медиа."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Члены группы могут отправлять ссылки SimpleX."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; - /* No comment provided by engineer. */ "Menus" = "Меню"; @@ -3042,9 +3114,6 @@ /* item status text */ "Message forwarded" = "Сообщение переслано"; -/* item status description */ -"Message may be delivered later if member becomes active." = "Сообщение может быть доставлено позже, если член группы станет активным."; - /* No comment provided by engineer. */ "Message queue info" = "Информация об очереди сообщений"; @@ -3093,6 +3162,9 @@ /* 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" = "Получено сообщений"; @@ -3165,9 +3237,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!" = "Дополнительные улучшения скоро!"; @@ -3186,6 +3264,9 @@ /* notification label action */ "Mute" = "Без звука"; +/* notification label action */ +"Mute all" = "Все без звука"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Без звука, когда не активный!"; @@ -3219,6 +3300,9 @@ /* delete after time */ "never" = "никогда"; +/* token status text */ +"New" = "Новый"; + /* No comment provided by engineer. */ "New chat" = "Новый чат"; @@ -3246,9 +3330,6 @@ /* No comment provided by engineer. */ "New media options" = "Новые медиа-опции"; -/* No comment provided by engineer. */ -"New member role" = "Роль члена группы"; - /* notification */ "new message" = "новое сообщение"; @@ -3279,6 +3360,15 @@ /* Authentication unavailable */ "No app password" = "Нет кода доступа"; +/* No comment provided by engineer. */ +"No chats" = "Нет чатов"; + +/* No comment provided by engineer. */ +"No chats found" = "Чаты не найдены"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Нет чатов в списке %@"; + /* No comment provided by engineer. */ "No contacts selected" = "Контакты не выбраны"; @@ -3312,6 +3402,9 @@ /* servers error */ "No media & file servers." = "Нет серверов файлов и медиа."; +/* No comment provided by engineer. */ +"No message" = "Нет сообщения"; + /* servers error */ "No message servers." = "Нет серверов сообщений."; @@ -3348,12 +3441,21 @@ /* copied message info in history */ "no text" = "нет текста"; +/* alert title */ +"No token!" = "Нет токена!"; + +/* No comment provided by engineer. */ +"No unread chats" = "Нет непрочитанных чатов"; + /* No comment provided by engineer. */ "No user identifiers." = "Без идентификаторов пользователей."; /* No comment provided by engineer. */ "Not compatible!" = "Несовместимая версия!"; +/* No comment provided by engineer. */ +"Notes" = "Заметки"; + /* No comment provided by engineer. */ "Nothing selected" = "Ничего не выбрано"; @@ -3366,18 +3468,21 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Уведомления выключены"; +/* alert title */ +"Notifications error" = "Ошибка уведомлений"; + /* No comment provided by engineer. */ "Notifications privacy" = "Конфиденциальность уведомлений"; -/* No comment provided by engineer. */ -"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Теперь админы могут:\n- удалять сообщения членов.\n- приостанавливать членов (роль \"наблюдатель\")"; +/* alert title */ +"Notifications status" = "Статус уведомлений"; /* member role */ "observer" = "читатель"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "нет"; /* blur media */ @@ -3431,6 +3536,12 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "Только владельцы группы могут разрешить голосовые сообщения."; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Только отправитель и модераторы видят это"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Только вы и модераторы видят это"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "Только Вы можете добавлять реакции на сообщения."; @@ -3461,7 +3572,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Только Ваш контакт может отправлять голосовые сообщения."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Открыть"; /* No comment provided by engineer. */ @@ -3512,6 +3623,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" = "другое"; @@ -3551,9 +3665,6 @@ /* No comment provided by engineer. */ "Password to show" = "Пароль чтобы раскрыть"; -/* past/unknown group member */ -"Past member %@" = "Бывший член %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Вставить адрес компьютера"; @@ -3570,7 +3681,13 @@ "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. */ "Periodic" = "Периодически"; @@ -3638,6 +3755,15 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Пожалуйста, надежно сохраните пароль, Вы НЕ сможете его поменять, если потеряете."; +/* token info */ +"Please try to disable and re-enable notfications." = "Попробуйте выключить и снова включить уведомления."; + +/* token info */ +"Please wait for token activation to complete." = "Пожалуйста, дождитесь завершения активации токена."; + +/* token info */ +"Please wait for token to be registered." = "Пожалуйста, дождитесь регистрации токена."; + /* No comment provided by engineer. */ "Polish interface" = "Польский интерфейс"; @@ -3668,12 +3794,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" = "Конфиденциальная доставка сообщений"; @@ -3720,7 +3855,7 @@ "Prohibit messages reactions." = "Запретить реакции на сообщения."; /* No comment provided by engineer. */ -"Prohibit sending direct messages to members." = "Запретить посылать прямые сообщения членам группы."; +"Prohibit reporting messages to moderators." = "Запретить жаловаться модераторам группы."; /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "Запретить посылать исчезающие сообщения."; @@ -3881,8 +4016,17 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Уменьшенное потребление батареи"; +/* No comment provided by engineer. */ +"Register" = "Зарегистрировать"; + +/* token info */ +"Register notification token?" = "Зарегистрировать токен уведомлений?"; + +/* token status text */ +"Registered" = "Зарегистрирован"; + /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Отклонить"; /* No comment provided by engineer. */ @@ -3891,6 +4035,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Отклонить запрос"; +/* No comment provided by engineer. */ +"rejected" = "отклонён"; + /* call status */ "rejected call" = "отклонённый звонок"; @@ -3909,12 +4056,6 @@ /* No comment provided by engineer. */ "Remove image" = "Удалить изображение"; -/* No comment provided by engineer. */ -"Remove member" = "Удалить члена группы"; - -/* No comment provided by engineer. */ -"Remove member?" = "Удалить члена группы?"; - /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Удалить пароль из Keychain?"; @@ -3960,6 +4101,36 @@ /* chat item action */ "Reply" = "Ответить"; +/* chat item action */ +"Report" = "Пожаловаться"; + +/* report reason */ +"Report content: only group moderators will see it." = "Пожаловаться на сообщение: увидят только модераторы группы."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Пожаловаться на профиль: увидят только модераторы группы."; + +/* report reason */ +"Report other: only group moderators will see it." = "Пожаловаться: увидят только модераторы группы."; + +/* No comment provided by engineer. */ +"Report reason?" = "Причина сообщения?"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Пожаловаться на спам: увидят только модераторы группы."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Пожаловаться на нарушение: увидят только модераторы группы."; + +/* report in notification */ +"Report: %@" = "Сообщение о нарушении: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Сообщения о нарушениях запрещены в этой группе."; + +/* No comment provided by engineer. */ +"Reports" = "Сообщения о нарушениях"; + /* chat list item title */ "requested to connect" = "запрошено соединение"; @@ -4017,9 +4188,6 @@ /* No comment provided by engineer. */ "Review conditions" = "Посмотреть условия"; -/* No comment provided by engineer. */ -"Review later" = "Посмотреть позже"; - /* No comment provided by engineer. */ "Revoke" = "Отозвать"; @@ -4042,7 +4210,7 @@ "Safer groups" = "Более безопасные группы"; /* alert button - chat item action */ +chat item action */ "Save" = "Сохранить"; /* alert button */ @@ -4051,9 +4219,6 @@ /* alert button */ "Save and notify contact" = "Сохранить и уведомить контакт"; -/* No comment provided by engineer. */ -"Save and notify group members" = "Сохранить и уведомить членов группы"; - /* No comment provided by engineer. */ "Save and reconnect" = "Сохранить и переподключиться"; @@ -4063,6 +4228,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" = "Сохранить пароль и открыть чат"; @@ -4231,6 +4399,9 @@ /* No comment provided by engineer. */ "Send notifications" = "Отправлять уведомления"; +/* No comment provided by engineer. */ +"Send private reports" = "Вы можете сообщить о нарушениях"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Отправьте вопросы и идеи"; @@ -4240,9 +4411,6 @@ /* 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 последних сообщений новым членам."; - /* alert message */ "Sender cancelled file transfer." = "Отправитель отменил передачу файла."; @@ -4363,6 +4531,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…" = "Имя контакта…"; @@ -4375,6 +4546,9 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Установите код вместо системной аутентификации."; +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Установите срок хранения сообщений в чатах."; + /* profile update event chat item */ "set new contact address" = "установлен новый адрес контакта"; @@ -4390,9 +4564,6 @@ /* No comment provided by engineer. */ "Set passphrase to export" = "Установите пароль"; -/* No comment provided by engineer. */ -"Set the message shown to new members!" = "Установить сообщение для новых членов группы!"; - /* No comment provided by engineer. */ "Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN"; @@ -4406,7 +4577,7 @@ "Shape profile images" = "Форма картинок профилей"; /* alert action - chat item action */ +chat item action */ "Share" = "Поделиться"; /* No comment provided by engineer. */ @@ -4571,6 +4742,10 @@ /* notification title */ "Somebody" = "Контакт"; +/* blocking reason +report reason */ +"Spam" = "Спам"; + /* No comment provided by engineer. */ "Square, circle, or anything in between." = "Квадрат, круг и все, что между ними."; @@ -4628,6 +4803,9 @@ /* No comment provided by engineer. */ "Stopping chat" = "Остановка чата"; +/* No comment provided by engineer. */ +"Storage" = "Хранилище"; + /* No comment provided by engineer. */ "strike" = "зачеркнуть"; @@ -4697,6 +4875,9 @@ /* 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"; @@ -4712,6 +4893,9 @@ /* server test failure */ "Test failed at step %@." = "Ошибка теста на шаге %@."; +/* No comment provided by engineer. */ +"Test notifications" = "Протестировать уведомления"; + /* No comment provided by engineer. */ "Test server" = "Тестировать сервер"; @@ -4769,30 +4953,15 @@ /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Неправильный ID предыдущего сообщения (меньше или равен предыдущему).\nЭто может произойти из-за ошибки программы, или когда соединение компроментировано."; -/* No comment provided by engineer. */ -"The message will be deleted for all members." = "Сообщение будет удалено для всех членов группы."; - -/* No comment provided by engineer. */ -"The message will be marked as moderated for all members." = "Сообщение будет помечено как удаленное для всех членов группы."; - -/* No comment provided by engineer. */ -"The messages will be deleted for all members." = "Сообщения будут удалены для всех членов группы."; - -/* No comment provided by engineer. */ -"The messages will be marked as moderated for all members." = "Сообщения будут помечены как удаленные для всех членов группы."; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Профиль отправляется только Вашим контактам."; +"Your profile is stored on your device and only shared with your contacts." = "Ваш профиль храниться на Вашем устройстве и отправляется только контактам."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**."; -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Те же самые условия будут приняты для оператора(ов): **%@**."; - /* No comment provided by engineer. */ "The second preset operator in the app!" = "Второй оператор серверов в приложении!"; @@ -4832,6 +5001,9 @@ /* No comment provided by engineer. */ "This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Это действие нельзя отменить — все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут."; +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Это действие нельзя отменить - сообщения в этом чате, отправленные или полученные раньше чем выбрано, будут удалены."; + /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; @@ -4850,9 +5022,6 @@ /* No comment provided by engineer. */ "This display name is invalid. Please choose another name." = "Ошибка имени профиля. Пожалуйста, выберите другое имя."; -/* No comment provided by engineer. */ -"This group has over %lld members, delivery receipts are not sent." = "В группе более %lld членов, отчёты о доставке выключены."; - /* No comment provided by engineer. */ "This group no longer exists." = "Эта группа больше не существует."; @@ -4865,6 +5034,9 @@ /* 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 **%@**." = "Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**."; @@ -4931,6 +5103,9 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Установите режим Инкогнито при соединении."; +/* token status */ +"Token status: %@." = "Статус токена: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Прозрачность тулбара"; @@ -4967,15 +5142,6 @@ /* No comment provided by engineer. */ "Unblock for all" = "Разблокировать для всех"; -/* No comment provided by engineer. */ -"Unblock member" = "Разблокировать члена группы"; - -/* No comment provided by engineer. */ -"Unblock member for all?" = "Разблокировать члена для всех?"; - -/* No comment provided by engineer. */ -"Unblock member?" = "Разблокировать члена группы?"; - /* rcv group event chat item */ "unblocked %@" = "%@ разблокирован"; @@ -5048,9 +5214,6 @@ /* swipe action */ "Unread" = "Не прочитано"; -/* No comment provided by engineer. */ -"Up to 100 last messages are sent to new members." = "До 100 последних сообщений отправляются новым членам."; - /* No comment provided by engineer. */ "Update" = "Обновить"; @@ -5063,6 +5226,9 @@ /* No comment provided by engineer. */ "Update settings?" = "Обновить настройки?"; +/* No comment provided by engineer. */ +"Updated conditions" = "Обновленные условия"; + /* rcv group event chat item */ "updated group profile" = "обновил(а) профиль группы"; @@ -5144,12 +5310,18 @@ /* No comment provided by engineer. */ "Use SOCKS proxy" = "Использовать SOCKS прокси"; +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "Использовать TCP-порт %@, когда порт не указан."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Используйте приложение во время звонка."; /* No comment provided by engineer. */ "Use the app with one hand." = "Используйте приложение одной рукой."; +/* No comment provided by engineer. */ +"Use web port" = "Использовать веб-порт"; + /* No comment provided by engineer. */ "User selection" = "Выбор пользователя"; @@ -5426,9 +5598,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,9 +5631,6 @@ /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках."; -/* No comment provided by engineer. */ -"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились."; - /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с **%@**."; @@ -5519,9 +5685,6 @@ /* No comment provided by engineer. */ "You joined this group" = "Вы вступили в эту группу"; -/* No comment provided by engineer. */ -"You joined this group. Connecting to inviting group member." = "Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы."; - /* snd group event chat item */ "you left" = "Вы покинули группу"; @@ -5555,6 +5718,9 @@ /* chat list item description */ "you shared one-time link incognito" = "Вы создали ссылку инкогнито"; +/* token info */ +"You should receive notifications." = "Вы должны получать уведомления."; + /* snd group event chat item */ "you unblocked %@" = "Вы разблокировали %@"; @@ -5573,9 +5739,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." = "Вы все равно получите звонки и уведомления в профилях без звука, когда они активные."; @@ -5672,6 +5835,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 0a3564fe91..57c0466eb9 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)"; @@ -185,7 +164,7 @@ "0s" = "0s"; /* delete after time - time interval */ +time interval */ "1 day" = "1 วัน"; /* time interval */ @@ -195,11 +174,11 @@ "1 minute" = "1 นาที"; /* delete after time - time interval */ +time interval */ "1 month" = "1 เดือน"; /* delete after time - time interval */ +time interval */ "1 week" = "1 สัปดาห์"; /* No comment provided by engineer. */ @@ -239,15 +218,15 @@ "above, then choose:" = "ด้านบน จากนั้นเลือก:"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "รับ"; /* notification body */ "Accept contact request from %@?" = "รับการขอติดต่อจาก %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "ยอมรับโหมดไม่ระบุตัวตน"; /* call status */ @@ -506,7 +485,7 @@ "Can't invite contacts!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; /* alert action - alert button */ +alert button */ "Cancel" = "ยกเลิก"; /* feature offered item */ @@ -546,7 +525,7 @@ "Change self-destruct mode" = "เปลี่ยนโหมดทําลายตัวเอง"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "เปลี่ยนรหัสผ่านแบบทำลายตัวเอง"; /* chat item text */ @@ -856,7 +835,7 @@ "Decryption error" = "ข้อผิดพลาดในการ decrypt"; /* delete after time - pref value */ +pref value */ "default (%@)" = "ค่าเริ่มต้น (%@)"; /* No comment provided by engineer. */ @@ -866,7 +845,7 @@ "default (yes)" = "ค่าเริ่มต้น (ใช่)"; /* alert action - swipe action */ +swipe action */ "Delete" = "ลบ"; /* No comment provided by engineer. */ @@ -1331,7 +1310,9 @@ /* No comment provided by engineer. */ "Error: " = "ผิดพลาด: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "ข้อผิดพลาด: % @"; /* No comment provided by engineer. */ @@ -2034,8 +2015,8 @@ "observer" = "ผู้สังเกตการณ์"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "ปิด"; /* blur media */ @@ -2336,7 +2317,7 @@ "Reduced battery usage" = "ลดการใช้แบตเตอรี่"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "ปฏิเสธ"; /* No comment provided by engineer. */ @@ -2433,7 +2414,7 @@ "Run chat" = "เรียกใช้แชท"; /* alert button - chat item action */ +chat item action */ "Save" = "บันทึก"; /* alert button */ @@ -2635,7 +2616,7 @@ "Settings" = "การตั้งค่า"; /* alert action - chat item action */ +chat item action */ "Share" = "แชร์"; /* No comment provided by engineer. */ @@ -2849,7 +2830,7 @@ "The old database was not removed during the migration, it can be deleted." = "ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้"; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น"; +"Your profile is stored on your device and only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "ขีดที่สองที่เราพลาด! ✅"; @@ -3328,6 +3309,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 dd0d7c6217..e3bb11d1cc 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)"; @@ -287,7 +266,7 @@ "0s" = "0sn"; /* delete after time - time interval */ +time interval */ "1 day" = "1 gün"; /* time interval */ @@ -297,11 +276,11 @@ "1 minute" = "1 dakika"; /* delete after time - time interval */ +time interval */ "1 month" = "1 ay"; /* delete after time - time interval */ +time interval */ "1 week" = "1 hafta"; /* No comment provided by engineer. */ @@ -356,8 +335,8 @@ "Accent" = "Ana renk"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Kabul et"; /* No comment provided by engineer. */ @@ -370,7 +349,7 @@ "Accept contact request from %@?" = "%@ 'den gelen iletişim isteği kabul edilsin mi?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Takma adla kabul et"; /* call status */ @@ -772,7 +751,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. */ @@ -854,7 +834,7 @@ "Can't message member" = "Üyeye mesaj gönderilemiyor"; /* alert action - alert button */ +alert button */ "Cancel" = "İptal et"; /* No comment provided by engineer. */ @@ -909,7 +889,7 @@ "Change self-destruct mode" = "Kendini yok etme modunu değiştir"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Kendini yok eden parolayı değiştir"; /* chat item text */ @@ -1068,12 +1048,6 @@ /* No comment provided by engineer. */ "Conditions of use" = "Kullanım koşulları"; -/* No comment provided by engineer. */ -"Conditions will be accepted for enabled operators after 30 days." = "Koşullar 30 gün sonra etkin operatörler tarafından kabul edilecektir."; - -/* No comment provided by engineer. */ -"Conditions will be accepted for operator(s): **%@**." = "Koşullar operatör(ler) için kabul edilecektir: **%@**."; - /* No comment provided by engineer. */ "Conditions will be accepted for the operator(s): **%@**." = "Koşullar bu operatör(ler) için kabul edilecektir: **%@**."; @@ -1474,7 +1448,7 @@ "decryption errors" = "Şifre çözme hataları"; /* delete after time - pref value */ +pref value */ "default (%@)" = "varsayılan (%@)"; /* No comment provided by engineer. */ @@ -1484,7 +1458,7 @@ "default (yes)" = "varsayılan (evet)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Sil"; /* No comment provided by engineer. */ @@ -1773,7 +1747,7 @@ "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. */ @@ -1836,9 +1810,6 @@ /* No comment provided by engineer. */ "Enable camera access" = "Kamera erişimini etkinleştir"; -/* No comment provided by engineer. */ -"Enable Flux" = "Flux'u Etkinleştir"; - /* No comment provided by engineer. */ "Enable for all" = "Herkes için etkinleştir"; @@ -2193,7 +2164,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. */ @@ -2211,9 +2184,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Konuşma sırasında devre dışı bırakılsa bile."; -/* No comment provided by engineer. */ -"event happened" = "etkinlik yaşandı"; - /* No comment provided by engineer. */ "Exit without saving" = "Kaydetmeden çık"; @@ -3301,8 +3271,8 @@ "observer" = "gözlemci"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "kapalı"; /* blur media */ @@ -3383,7 +3353,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Sadece karşıdaki kişi sesli mesajlar gönderebilir."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Aç"; /* No comment provided by engineer. */ @@ -3780,7 +3750,7 @@ "Reduced battery usage" = "Azaltılmış pil kullanımı"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Reddet"; /* No comment provided by engineer. */ @@ -3931,7 +3901,7 @@ "Safer groups" = "Daha güvenli gruplar"; /* alert button - chat item action */ +chat item action */ "Save" = "Kaydet"; /* alert button */ @@ -4283,7 +4253,7 @@ "Shape profile images" = "Profil resimlerini şekillendir"; /* alert action - chat item action */ +chat item action */ "Share" = "Paylaş"; /* No comment provided by engineer. */ @@ -4632,7 +4602,7 @@ "The old database was not removed during the migration, it can be deleted." = "Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak."; +"Your profile is stored on your device and only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Özlediğimiz ikinci tik! ✅"; @@ -5450,6 +5420,3 @@ /* 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 f750d1ac8b..734b8dda82 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)"; @@ -287,7 +266,7 @@ "0s" = "0с"; /* delete after time - time interval */ +time interval */ "1 day" = "1 день"; /* time interval */ @@ -297,11 +276,11 @@ "1 minute" = "1 хвилина"; /* delete after time - time interval */ +time interval */ "1 month" = "1 місяць"; /* delete after time - time interval */ +time interval */ "1 week" = "1 тиждень"; /* No comment provided by engineer. */ @@ -356,8 +335,8 @@ "Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "Прийняти"; /* No comment provided by engineer. */ @@ -370,7 +349,7 @@ "Accept contact request from %@?" = "Прийняти запит на контакт від %@?"; /* accept contact request via notification - swipe action */ +swipe action */ "Accept incognito" = "Прийняти інкогніто"; /* call status */ @@ -775,7 +754,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. */ @@ -857,7 +837,7 @@ "Can't message member" = "Не можу надіслати повідомлення користувачеві"; /* alert action - alert button */ +alert button */ "Cancel" = "Скасувати"; /* No comment provided by engineer. */ @@ -912,7 +892,7 @@ "Change self-destruct mode" = "Змінити режим самознищення"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Змінити пароль самознищення"; /* chat item text */ @@ -1071,12 +1051,6 @@ /* No comment provided by engineer. */ "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): **%@**." = "Для оператора(ів) приймаються умови: **%@**."; @@ -1477,7 +1451,7 @@ "decryption errors" = "помилки розшифровки"; /* delete after time - pref value */ +pref value */ "default (%@)" = "за замовчуванням (%@)"; /* No comment provided by engineer. */ @@ -1487,7 +1461,7 @@ "default (yes)" = "за замовчуванням (так)"; /* alert action - swipe action */ +swipe action */ "Delete" = "Видалити"; /* No comment provided by engineer. */ @@ -1779,7 +1753,7 @@ "Downgrade and open chat" = "Пониження та відкритий чат"; /* alert button - chat item action */ +chat item action */ "Download" = "Завантажити"; /* No comment provided by engineer. */ @@ -1842,9 +1816,6 @@ /* No comment provided by engineer. */ "Enable camera access" = "Увімкніть доступ до камери"; -/* No comment provided by engineer. */ -"Enable Flux" = "Увімкнути Flux"; - /* No comment provided by engineer. */ "Enable for all" = "Увімкнути для всіх"; @@ -2199,7 +2170,9 @@ /* No comment provided by engineer. */ "Error: " = "Помилка: "; -/* alert message */ +/* alert message +file error text +snd error text */ "Error: %@" = "Помилка: %@"; /* No comment provided by engineer. */ @@ -2217,9 +2190,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" = "Вихід без збереження"; @@ -2340,9 +2310,6 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Виправлення не підтримується учасником групи"; -/* No comment provided by engineer. */ -"for better metadata privacy." = "для кращої конфіденційності метаданих."; - /* servers error */ "For chat profile %@:" = "Для профілю чату %@:"; @@ -3349,8 +3316,8 @@ "observer" = "спостерігач"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "вимкнено"; /* blur media */ @@ -3434,7 +3401,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Тільки ваш контакт може надсилати голосові повідомлення."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Відкрито"; /* No comment provided by engineer. */ @@ -3855,7 +3822,7 @@ "Reduced battery usage" = "Зменшення використання акумулятора"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "Відхилити"; /* No comment provided by engineer. */ @@ -3990,9 +3957,6 @@ /* No comment provided by engineer. */ "Review conditions" = "Умови перегляду"; -/* No comment provided by engineer. */ -"Review later" = "Перегляньте пізніше"; - /* No comment provided by engineer. */ "Revoke" = "Відкликати"; @@ -4015,7 +3979,7 @@ "Safer groups" = "Безпечніші групи"; /* alert button - chat item action */ +chat item action */ "Save" = "Зберегти"; /* alert button */ @@ -4379,7 +4343,7 @@ "Shape profile images" = "Сформуйте зображення профілю"; /* alert action - chat item action */ +chat item action */ "Share" = "Поділіться"; /* No comment provided by engineer. */ @@ -4758,14 +4722,11 @@ "The old database was not removed during the migration, it can be deleted." = "Стара база даних не була видалена під час міграції, її можна видалити."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Профіль доступний лише вашим контактам."; +"Your profile is stored on your device and only shared with your contacts." = "Профіль доступний лише вашим контактам."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Такі ж умови будуть застосовуватися до оператора(ів): **%@**."; - /* No comment provided by engineer. */ "The second preset operator in the app!" = "Другий попередньо встановлений оператор у застосунку!"; @@ -5399,9 +5360,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." = "Ви можете налаштувати сервери за допомогою налаштувань."; @@ -5645,6 +5603,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 68ca7a839f..e3f9669d9f 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 星期"; @@ -260,7 +269,7 @@ "0s" = "0秒"; /* delete after time - time interval */ +time interval */ "1 day" = "1天"; /* time interval */ @@ -270,13 +279,22 @@ "1 minute" = "1分钟"; /* delete after time - time interval */ +time interval */ "1 month" = "1月"; /* delete after time - time interval */ +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,10 +341,13 @@ "Accent" = "强调"; /* accept contact request via notification - accept incoming call via notification - swipe action */ +accept incoming call via notification +swipe action */ "Accept" = "接受"; +/* No comment provided by engineer. */ +"Accept conditions" = "接受条款"; + /* No comment provided by engineer. */ "Accept connection request?" = "接受联系人?"; @@ -331,24 +355,36 @@ "Accept contact request from %@?" = "接受来自 %@ 的联系人请求?"; /* accept contact request via notification - swipe 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 +394,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 +430,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 +460,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 +484,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 +499,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 +550,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." = "允许发送文件和媒体。"; @@ -532,6 +607,9 @@ /* No comment provided by engineer. */ "and %lld other events" = "和 %lld 其他事件"; +/* report reason */ +"Another reason" = "另一个理由"; + /* No comment provided by engineer. */ "Answer call" = "接听来电"; @@ -547,6 +625,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 +637,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "应用程序密码被替换为自毁密码。"; +/* No comment provided by engineer. */ +"App session" = "应用会话"; + /* No comment provided by engineer. */ "App version" = "应用程序版本"; @@ -571,12 +655,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" = "已存档的联系人"; @@ -628,6 +730,9 @@ /* No comment provided by engineer. */ "Auto-accept images" = "自动接受图片"; +/* alert title */ +"Auto-accept settings" = "自动接受设置"; + /* No comment provided by engineer. */ "Back" = "返回"; @@ -649,15 +754,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 +811,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 +845,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 +900,7 @@ "Can't message member" = "无法向成员发送消息"; /* alert action - alert button */ +alert button */ "Cancel" = "取消"; /* No comment provided by engineer. */ @@ -788,6 +927,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 +958,7 @@ "Change self-destruct mode" = "更改自毁模式"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "更改自毁密码"; /* chat item text */ @@ -831,6 +976,15 @@ /* chat item text */ "changing address…" = "更改地址…"; +/* No comment provided by engineer. */ +"Chat" = "聊天"; + +/* No comment provided by engineer. */ +"Chat already exists" = "聊天已存在"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "聊天已存在!"; + /* No comment provided by engineer. */ "Chat colors" = "聊天颜色"; @@ -867,15 +1021,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 +1078,12 @@ /* No comment provided by engineer. */ "Clear conversation?" = "清除对话吗?"; +/* No comment provided by engineer. */ +"Clear group?" = "清除群?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "清除还是删除群?"; + /* No comment provided by engineer. */ "Clear private notes?" = "清除私密笔记?"; @@ -924,6 +1099,9 @@ /* No comment provided by engineer. */ "colored" = "彩色"; +/* report reason */ +"Community guidelines violation" = "违反社区指导方针"; + /* server test step */ "Compare file" = "对比文件"; @@ -936,9 +1114,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): **%@**." = "已经接受下列运营方的条款:**%@**。"; + +/* No comment provided by engineer. */ +"Conditions of use" = "使用条款"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "将接受下列运营方的条款:**%@**。"; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "将于 %@ 接受条款。"; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "将在 %@ 自动接受启用的运营方的条款。"; + /* No comment provided by engineer. */ "Configure ICE servers" = "配置 ICE 服务器"; +/* No comment provided by engineer. */ +"Configure server operators" = "配置服务器运营方"; + /* No comment provided by engineer. */ "Confirm" = "确认"; @@ -969,6 +1171,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "确认上传"; +/* token status text */ +"Confirmed" = "已确定"; + /* server test step */ "Connect" = "连接"; @@ -1068,6 +1273,9 @@ /* No comment provided by engineer. */ "Connection and servers status." = "连接和服务器状态。"; +/* No comment provided by engineer. */ +"Connection blocked" = "连接被阻止"; + /* No comment provided by engineer. */ "Connection error" = "连接错误"; @@ -1077,12 +1285,24 @@ /* chat list item title (it should not be shown */ "connection established" = "连接已建立"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "连接被运营方 %@ 阻止"; + +/* No comment provided by engineer. */ +"Connection not ready." = "连接未就绪。"; + /* No comment provided by engineer. */ "Connection notifications" = "连接通知"; /* No comment provided by engineer. */ "Connection request sent!" = "已发送连接请求!"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "连接需要加密重协商。"; + +/* No comment provided by engineer. */ +"Connection security" = "连接安全性"; + /* No comment provided by engineer. */ "Connection terminated" = "连接被终止"; @@ -1140,6 +1360,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 +1378,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 +1405,9 @@ /* No comment provided by engineer. */ "Create link" = "创建链接"; +/* No comment provided by engineer. */ +"Create list" = "创建列表"; + /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "在[桌面应用程序](https://simplex.chat/downloads/)中创建新的个人资料。 💻"; @@ -1212,6 +1444,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 +1465,9 @@ /* No comment provided by engineer. */ "Custom time" = "自定义时间"; +/* No comment provided by engineer. */ +"Customizable message shape." = "可自定义消息形状。"; + /* No comment provided by engineer. */ "Customize theme" = "自定义主题"; @@ -1312,7 +1550,7 @@ "decryption errors" = "解密错误"; /* delete after time - pref value */ +pref value */ "default (%@)" = "默认 (%@)"; /* No comment provided by engineer. */ @@ -1322,7 +1560,7 @@ "default (yes)" = "默认 (是)"; /* alert action - swipe action */ +swipe action */ "Delete" = "删除"; /* No comment provided by engineer. */ @@ -1346,12 +1584,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,6 +1644,9 @@ /* No comment provided by engineer. */ "Delete link?" = "删除链接?"; +/* alert title */ +"Delete list?" = "删除列表?"; + /* No comment provided by engineer. */ "Delete member message?" = "删除成员消息?"; @@ -1415,6 +1665,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 +1677,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 +1710,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 +1762,7 @@ "Device" = "设备"; /* No comment provided by engineer. */ -"Device authentication is disabled. Turning off SimpleX Lock." = "设备验证被禁用。关闭 SimpleX 锁定。"; +"Device authentication is disabled. Turning off SimpleX Lock." = "设备验证已禁用。 SimpleX 已解锁。"; /* No comment provided by engineer. */ "Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "没有启用设备验证。一旦启用设备验证,您可以通过设置打开 SimpleX 锁定。"; @@ -1521,11 +1780,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,26 +1845,38 @@ /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "请勿直接发送消息,即使您的服务器或目标服务器不支持私有路由。"; +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "代理不使用身份验证凭据。"; + /* No comment provided by engineer. */ "Do NOT use private routing." = "不要使用私有路由。"; /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "请勿使用 SimpleX 进行紧急通话。"; +/* No comment provided by engineer. */ +"Documents:" = "文档:"; + /* No comment provided by engineer. */ "Don't create address" = "不创建地址"; /* No comment provided by engineer. */ "Don't enable" = "不要启用"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "不错过重要消息。"; + /* No comment provided by engineer. */ "Don't show again" = "不再显示"; +/* No comment provided by engineer. */ +"Done" = "完成"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "降级并打开聊天"; /* alert button - chat item action */ +chat item action */ "Download" = "下载"; /* No comment provided by engineer. */ @@ -1608,6 +1888,9 @@ /* server test step */ "Download file" = "下载文件"; +/* alert action */ +"Download files" = "下载文件"; + /* No comment provided by engineer. */ "Downloaded" = "已下载"; @@ -1627,7 +1910,7 @@ "duplicate message" = "重复的消息"; /* No comment provided by engineer. */ -"duplicates" = "复本"; +"duplicates" = "副本"; /* No comment provided by engineer. */ "Duration" = "时长"; @@ -1635,6 +1918,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "端到端加密"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "端到端加密的通知。"; + /* chat item action */ "Edit" = "编辑"; @@ -1653,6 +1939,9 @@ /* 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 +2053,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "需要为 %@ 重新进行加密协商"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "正进行加密重协商。"; + /* No comment provided by engineer. */ "ended" = "已结束"; @@ -1812,21 +2104,33 @@ /* 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 connection profile" = "更改连接资料出错"; + /* No comment provided by engineer. */ "Error changing role" = "更改角色错误"; /* No comment provided by engineer. */ "Error changing setting" = "更改设置错误"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "切换至隐身聊天出错!"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "连接到转发服务器 %@ 时出错。请稍后尝试。"; @@ -1839,6 +2143,9 @@ /* No comment provided by engineer. */ "Error creating group link" = "创建群组链接错误"; +/* alert title */ +"Error creating list" = "创建列表出错"; + /* No comment provided by engineer. */ "Error creating member contact" = "创建成员联系人时出错"; @@ -1848,6 +2155,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "创建资料错误!"; +/* No comment provided by engineer. */ +"Error creating report" = "创建举报出错"; + /* No comment provided by engineer. */ "Error decrypting file" = "解密文件时出错"; @@ -1896,6 +2206,12 @@ /* 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 +2224,21 @@ /* No comment provided by engineer. */ "Error reconnecting servers" = "重新连接服务器时出错"; +/* alert title */ +"Error registering for notifications" = "注册消息推送出错"; + /* No comment provided by engineer. */ "Error removing member" = "删除成员错误"; +/* alert title */ +"Error reordering lists" = "重排列表出错"; + /* No comment provided by engineer. */ "Error resetting statistics" = "重置统计信息时出错"; +/* alert title */ +"Error saving chat list" = "保存聊天列表出错"; + /* No comment provided by engineer. */ "Error saving group profile" = "保存群组资料错误"; @@ -1926,6 +2251,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "保存密码到钥匙串错误"; +/* alert title */ +"Error saving servers" = "保存服务器出错"; + /* when migrating */ "Error saving settings" = "保存设置出错"; @@ -1953,18 +2281,27 @@ /* No comment provided by engineer. */ "Error stopping chat" = "停止聊天错误"; +/* No comment provided by engineer. */ +"Error switching profile" = "切换配置文件出错"; + /* alertTitle */ "Error switching profile!" = "切换资料错误!"; /* No comment provided by engineer. */ "Error synchronizing connection" = "同步连接错误"; +/* No comment provided by engineer. */ +"Error testing server connection" = "检验服务器连接出错"; + /* No comment provided by engineer. */ "Error updating group link" = "更新群组链接错误"; /* No comment provided by engineer. */ "Error updating message" = "更新消息错误"; +/* alert title */ +"Error updating server" = "更新服务器出错"; + /* No comment provided by engineer. */ "Error updating settings" = "更新设置错误"; @@ -1980,7 +2317,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 +2331,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 +2346,9 @@ /* No comment provided by engineer. */ "expired" = "过期"; +/* token status text */ +"Expired" = "已过期"; + /* No comment provided by engineer. */ "Export database" = "导出数据库"; @@ -2031,15 +2373,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." = "找不到文件 - 很可能文件已被删除或取消。"; @@ -2115,15 +2472,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" = "已转发"; @@ -2133,6 +2520,9 @@ /* No comment provided by engineer. */ "Forwarded from" = "转发自"; +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "正在转发 %lld 条消息"; + /* No comment provided by engineer. */ "Forwarding server %@ failed to connect to destination server %@. Please try later." = "转发服务器 %@ 无法连接到目标服务器 %@。请稍后尝试。"; @@ -2169,6 +2559,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 和贴纸"; @@ -2241,9 +2634,15 @@ /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "将为您删除群组——此操作无法撤消!"; +/* No comment provided by engineer. */ +"Groups" = "群"; + /* No comment provided by engineer. */ "Help" = "帮助"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "帮助管理员管理群组。"; + /* No comment provided by engineer. */ "Hidden" = "隐藏"; @@ -2274,6 +2673,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 +2745,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 +2769,12 @@ /* No comment provided by engineer. */ "inactive" = "无效"; +/* report reason */ +"Inappropriate content" = "不当内容"; + +/* report reason */ +"Inappropriate profile" = "不当个人资料"; + /* No comment provided by engineer. */ "Incognito" = "隐身聊天"; @@ -2424,6 +2841,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" = "无效聊天"; @@ -2475,6 +2907,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 +2931,9 @@ /* No comment provided by engineer. */ "iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "在您重启应用或改变密码后,iOS钥匙串将被用来安全地存储密码——它将允许接收推送通知。"; +/* No comment provided by engineer. */ +"IP address" = "IP 地址"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "不可撤回消息移除"; @@ -2586,6 +3024,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 +3057,15 @@ /* No comment provided by engineer. */ "Linked desktops" = "已链接桌面"; +/* swipe action */ +"List" = "列表"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "所有列表的名称和表情符号都应不同。"; + +/* No comment provided by engineer. */ +"List name..." = "列表名…"; + /* No comment provided by engineer. */ "LIVE" = "实时"; @@ -2682,12 +3135,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 +3159,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 +3177,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 +3225,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 +3252,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,12 +3330,18 @@ /* 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." = "此连接很可能已被删除。"; @@ -2868,6 +3351,9 @@ /* notification label action */ "Mute" = "静音"; +/* notification label action */ +"Mute all" = "全部静音"; + /* No comment provided by engineer. */ "Muted when inactive!" = "不活动时静音!"; @@ -2880,12 +3366,18 @@ /* 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" = "网络设置"; @@ -2895,6 +3387,9 @@ /* delete after time */ "never" = "从不"; +/* token status text */ +"New" = "新"; + /* No comment provided by engineer. */ "New chat" = "新聊天"; @@ -2913,6 +3408,9 @@ /* No comment provided by engineer. */ "New display name" = "新显示名"; +/* notification */ +"New events" = "新事件"; + /* No comment provided by engineer. */ "New in %@" = "%@ 的新内容"; @@ -2934,6 +3432,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "新密码……"; +/* No comment provided by engineer. */ +"New server" = "新服务器"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "每次启动应用都会使用新的 SOCKS 凭据。"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "每个服务器都会使用新的 SOCKS 凭据。"; + /* pref value */ "no" = "否"; @@ -2943,6 +3450,15 @@ /* Authentication unavailable */ "No app password" = "没有应用程序密码"; +/* No comment provided by engineer. */ +"No chats" = "无聊天"; + +/* No comment provided by engineer. */ +"No chats found" = "找不到聊天"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "列表 %@ 中无聊天"; + /* No comment provided by engineer. */ "No contacts selected" = "未选择联系人"; @@ -2973,9 +3489,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" = "没有录制语音消息的权限"; @@ -2985,24 +3516,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- 禁用成员(“观察员”角色)"; @@ -3010,8 +3574,8 @@ "observer" = "观察者"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "关闭"; /* blur media */ @@ -3047,6 +3611,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." = "只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。"; @@ -3062,6 +3629,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." = "只有您可以添加消息回应。"; @@ -3092,15 +3665,21 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "只有您的联系人可以发送语音消息。"; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "打开"; +/* No comment provided by engineer. */ +"Open changes" = "打开更改"; + /* No comment provided by engineer. */ "Open chat" = "打开聊天"; /* authentication reason */ "Open chat console" = "打开聊天控制台"; +/* No comment provided by engineer. */ +"Open conditions" = "打开条款"; + /* No comment provided by engineer. */ "Open group" = "打开群"; @@ -3113,6 +3692,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" = "或粘贴存档链接"; @@ -3125,6 +3713,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" = "其他"; @@ -3155,6 +3749,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "密码已设置!"; +/* No comment provided by engineer. */ +"Password" = "密码"; + /* No comment provided by engineer. */ "Password to show" = "显示密码"; @@ -3266,9 +3863,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" = "私密文件名"; @@ -3477,7 +4080,7 @@ "Reduced battery usage" = "减少电池使用量"; /* reject incoming call via notification - swipe action */ +swipe action */ "Reject" = "拒绝"; /* No comment provided by engineer. */ @@ -3604,19 +4207,22 @@ "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" = "安全接收文件"; @@ -3625,7 +4231,7 @@ "Safer groups" = "更安全的群组"; /* alert button - chat item action */ +chat item action */ "Save" = "保存"; /* alert button */ @@ -3646,6 +4252,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" = "保存密码并打开聊天"; @@ -3667,6 +4276,9 @@ /* No comment provided by engineer. */ "Save welcome message?" = "保存欢迎信息?"; +/* alert title */ +"Save your profile?" = "保存您的个人资料?"; + /* No comment provided by engineer. */ "saved" = "已保存"; @@ -3685,6 +4297,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" = "规模"; @@ -3962,7 +4577,7 @@ "Shape profile images" = "改变个人资料图形状"; /* alert action - chat item action */ +chat item action */ "Share" = "分享"; /* No comment provided by engineer. */ @@ -4025,6 +4640,18 @@ /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX 地址"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "可以通过任何消息应用安全分享 SimpleX 地址和一次性链接。"; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX 地址或一次性链接?"; + +/* simplex link type */ +"SimpleX channel link" = "SimpleX 频道链接"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat 与 Flux 达成了协议,将由 Flux 控制的服务器纳入 SimpleX 应用。"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "SimpleX Chat 的安全性 由 Trail of Bits 审核。"; @@ -4032,10 +4659,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 链接"; @@ -4061,6 +4688,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" = "简化的隐身模式"; @@ -4082,6 +4712,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:" = "某些文件未导出:"; @@ -4290,7 +4923,7 @@ "The old database was not removed during the migration, it can be deleted." = "旧数据库在迁移过程中没有被移除,可以删除。"; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "该资料仅与您的联系人共享。"; +"Your profile is stored on your device and only shared with your contacts." = "该资料仅与您的联系人共享。"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "我们错过的第二个\"√\"!✅"; @@ -5081,6 +5714,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..8fc9d104d2 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -85,6 +85,7 @@ android { "en", "ar", "bg", + "ca", "cs", "de", "es", diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index 48dfba11cc..0470977bcd 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -77,8 +77,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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 1c8209334d..5545595dc6 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -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 @@ -94,7 +92,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())) @@ -107,7 +105,7 @@ class SimplexApp: Application(), LifecycleEventObserver { /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */ if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats) } - updateChats(chats) + chatModel.chatsContext.updateChats(chats) } }.onFailure { Log.e(TAG, it.stackTraceToString()) } } diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 345a75b1e7..e2927e4aaf 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -155,6 +155,7 @@ buildConfig { 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"]}") } } 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 54e437afb1..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 @@ -42,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 @@ -51,10 +50,10 @@ 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: (ComposeMessage) -> Unit, @@ -197,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/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/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 2456463910..d88a450fd1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -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() } @@ -339,7 +339,7 @@ fun AndroidScreen(userPickerState: MutableStateFlow) { .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } ) Box2@{ currentChatId.value?.let { - ChatView(currentChatId, contentTag = null, onComposed = onComposed) + ChatView(chatsCtx = chatModel.chatsContext, currentChatId, onComposed = onComposed) } } } @@ -393,7 +393,7 @@ fun CenterPartOfScreen() { ModalManager.center.showInView() } } - else -> ChatView(currentChatId, contentTag = null) {} + 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 7afcd69487..61c20587bf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -38,6 +38,7 @@ 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.* @@ -67,7 +68,7 @@ object ChatModel { val chatId = 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 @@ -170,36 +171,6 @@ 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 getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value } else { @@ -324,31 +295,15 @@ 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 contentTag: MsgContentTag?) { 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 @@ -440,6 +395,26 @@ object ChatModel { addChat(chat) } } + + fun addToChatItems(index: Int, elem: ChatItem) { + chatItems.value = SnapshotStateList().apply { addAll(chatItems.value); add(index, elem); chatState.itemAdded(elem.id to elem.isRcvNew) } + } + + fun addToChatItems(elem: ChatItem) { + chatItems.value = SnapshotStateList().apply { addAll(chatItems.value); add(elem); chatState.itemAdded(elem.id to elem.isRcvNew) } + } + + fun removeLastChatItems() { + val removed: Triple + chatItems.value = SnapshotStateList().apply { + addAll(chatItems.value) + val remIndex = lastIndex + val rem = removeLast() + removed = Triple(rem.id, remIndex, rem.isRcvNew) + } + chatState.itemsRemoved(listOf(removed), chatItems.value) + } + suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { // mark chat non deleted if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) { @@ -493,9 +468,9 @@ object ChatModel { // 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) } } } @@ -540,7 +515,7 @@ object ChatModel { } else { cItem } - chatItems.addAndNotify(ci, contentTag) + addToChatItems(ci) true } } else { @@ -549,11 +524,11 @@ object ChatModel { } } - 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) { 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 } @@ -589,6 +564,48 @@ 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 + } + + // this should not happen, only another member can "remove" user, user can only "leave" (another event). + if (byMember.groupMemberId == groupInfo.membership.groupMemberId) { + Log.d(TAG, "exiting removeMemberItems") + return + } + val cInfo = ChatInfo.Group(groupInfo) + if (chatId.value == groupInfo.id) { + for (i in 0 until chatItems.value.size) { + val updatedItem = removedUpdatedItem(chatItems.value[i]) + if (updatedItem != null) { + updateChatItem(cInfo, updatedItem, atIndex = i) + } + } + } else { + val i = getChatIndex(rhId, groupInfo.id) + val chat = chats[i] + if (chat.chatItems.isNotEmpty()) { + val updatedItem = removedUpdatedItem(chat.chatItems[0]) + if (updatedItem != null) { + chats.value[i] = chat.copy(chatItems = listOf(updatedItem)) + } + } + } + } + fun clearChat(rhId: Long?, cInfo: ChatInfo) { // clear preview val i = getChatIndex(rhId, cInfo.id) @@ -605,9 +622,10 @@ object ChatModel { } } - 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 +635,8 @@ object ChatModel { subject .throttleLatest(2000) .collect { - withChats(contentTag) { - chats.replaceAll(popCollectedChats()) + withContext(Dispatchers.Main) { + chatsCtx.chats.replaceAll(popCollectedChats()) } } } @@ -706,7 +724,7 @@ object ChatModel { } i-- } - chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items) + chatState.itemsRead(if (itemIds != null) markedReadIds else null, items) } return markedRead to mentionsMarkedRead } @@ -747,6 +765,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 @@ -913,17 +936,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() } } } @@ -995,9 +1018,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 } } @@ -1008,9 +1031,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 @@ -1061,18 +1084,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 ) @@ -1190,6 +1204,7 @@ interface SomeChat { val apiId: Long val ready: Boolean val chatDeleted: Boolean + val userCantSendReason: Pair? val sendMsgEnabled: Boolean val incognito: Boolean fun featureEnabled(feature: ChatFeature): Boolean @@ -1214,14 +1229,6 @@ 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 @@ -1268,6 +1275,7 @@ 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 userCantSendReason get() = contact.userCantSendReason override val sendMsgEnabled get() = contact.sendMsgEnabled override val incognito get() = contact.incognito override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature) @@ -1293,6 +1301,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready override val chatDeleted get() = groupInfo.chatDeleted + override val userCantSendReason get() = groupInfo.userCantSendReason override val sendMsgEnabled get() = groupInfo.sendMsgEnabled override val incognito get() = groupInfo.incognito override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature) @@ -1317,6 +1326,7 @@ 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 userCantSendReason get() = noteFolder.userCantSendReason override val sendMsgEnabled get() = noteFolder.sendMsgEnabled override val incognito get() = noteFolder.incognito override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature) @@ -1341,6 +1351,7 @@ 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 userCantSendReason get() = contactRequest.userCantSendReason override val sendMsgEnabled get() = contactRequest.sendMsgEnabled override val incognito get() = contactRequest.incognito override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature) @@ -1365,6 +1376,7 @@ 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 userCantSendReason get() = contactConnection.userCantSendReason override val sendMsgEnabled get() = contactConnection.sendMsgEnabled override val incognito get() = contactConnection.incognito override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature) @@ -1383,19 +1395,22 @@ sealed class ChatInfo: SomeChat, NamedChat { } @Serializable @SerialName("invalidJSON") - class InvalidJSON(val json: String): ChatInfo() { + class InvalidJSON( + val json: String, + override val apiId: Long = -idGenerator.getAndIncrement(), + override val createdAt: Instant = Clock.System.now(), + override val updatedAt: Instant = Clock.System.now() + ): ChatInfo() { override val chatType get() = ChatType.Direct override val localDisplayName get() = invalidChatName - override val id get() = "" - override val apiId get() = 0L + override val id get() = "?$apiId" override val ready get() = false override val chatDeleted get() = false + override val userCantSendReason get() = generalGetString(MR.strings.cant_send_message_generic) to null override val sendMsgEnabled get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null - override val createdAt get() = Clock.System.now() - override val updatedAt get() = Clock.System.now() override val displayName get() = invalidChatName override val fullName get() = invalidChatName override val image get() = null @@ -1403,6 +1418,7 @@ sealed class ChatInfo: SomeChat, NamedChat { companion object { private val invalidChatName = generalGetString(MR.strings.invalid_chat) + private val idGenerator = AtomicLong(0) } } @@ -1433,14 +1449,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 @@ -1511,13 +1519,17 @@ 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 userCantSendReason: Pair? + get() { + // TODO [short links] this will have additional statuses for pending contact requests before they are accepted + if (nextSendGrpInv) return null + if (!active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null + if (!sndReady) return generalGetString(MR.strings.cant_send_message_contact_not_ready) to null + if (activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null + if (activeConn?.connDisabled == true) return generalGetString(MR.strings.cant_send_message_contact_disabled) to null + return null + } + override val sendMsgEnabled get() = userCantSendReason == null val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent override val incognito get() = contactConnIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { @@ -1751,7 +1763,23 @@ data class GroupInfo ( override val apiId get() = groupId override val ready get() = membership.memberActive override val chatDeleted get() = false - override val sendMsgEnabled get() = membership.memberActive + override val userCantSendReason: Pair? get() = + if (membership.memberActive) { + if (membership.memberRole == GroupMemberRole.Observer) { + generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc) + } else { + null + } + } else { + when (membership.memberStatus) { + GroupMemberStatus.MemRejected -> generalGetString(MR.strings.cant_send_message_rejected) to null + GroupMemberStatus.MemGroupDeleted -> generalGetString(MR.strings.cant_send_message_group_deleted) to null + GroupMemberStatus.MemRemoved -> generalGetString(MR.strings.cant_send_message_mem_removed) to null + GroupMemberStatus.MemLeft -> generalGetString(MR.strings.cant_send_message_you_left) to null + else -> generalGetString(MR.strings.cant_send_message_generic) to null + } + } + override val sendMsgEnabled get() = userCantSendReason == null override val incognito get() = membership.memberIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on @@ -1958,8 +1986,8 @@ data class GroupMember ( fun canBlockForAll(groupInfo: GroupInfo): Boolean { val userRole = groupInfo.membership.memberRole - return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Admin - && userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive + return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Moderator + && userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive } val memberIncognito = memberProfile.profileId != memberContactProfileId @@ -2127,6 +2155,7 @@ class NoteFolder( override val apiId get() = noteFolderId override val chatDeleted get() = false override val ready get() = true + override val userCantSendReason: Pair? = null override val sendMsgEnabled get() = true override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice @@ -2163,6 +2192,7 @@ class UserContactRequest ( override val apiId get() = contactRequestId override val chatDeleted get() = false override val ready get() = true + override val userCantSendReason = generalGetString(MR.strings.cant_send_message_generic) to null override val sendMsgEnabled get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false @@ -2192,7 +2222,7 @@ class PendingContactConnection( val viaContactUri: Boolean, val groupLinkId: String? = null, val customUserProfileId: Long? = null, - val connReqInv: String? = null, + val connLinkInv: CreatedConnLink? = null, override val localAlias: String, override val createdAt: Instant, override val updatedAt: Instant @@ -2202,6 +2232,7 @@ class PendingContactConnection( override val apiId get() = pccConnId override val chatDeleted get() = false override val ready get() = false + override val userCantSendReason = generalGetString(MR.strings.cant_send_message_generic) to null override val sendMsgEnabled get() = false override val incognito get() = customUserProfileId != null override fun featureEnabled(feature: ChatFeature) = false @@ -2439,14 +2470,14 @@ data class ChatItem ( 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 @@ -2691,10 +2722,6 @@ fun MutableState>.add(index: Int, elem: Chat) { value = SnapshotStateList().apply { addAll(value); add(index, elem) } } -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(elem: Chat) { value = SnapshotStateList().apply { addAll(value); add(elem) } } @@ -2702,11 +2729,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) } } @@ -2719,6 +2741,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>() @@ -2733,8 +2756,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) } } @@ -2746,17 +2769,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) } } @@ -2765,11 +2777,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() @@ -3259,6 +3272,7 @@ sealed class CIContent: ItemContent { when (role) { GroupMemberRole.Owner -> generalGetString(MR.strings.feature_roles_owners) GroupMemberRole.Admin -> generalGetString(MR.strings.feature_roles_admins) + GroupMemberRole.Moderator -> generalGetString(MR.strings.feature_roles_moderators) else -> generalGetString(MR.strings.feature_roles_all_members) } @@ -3340,14 +3354,16 @@ sealed class MsgReaction { } 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) } } @@ -3900,7 +3916,7 @@ enum class MsgContentTag { class FormattedText(val text: String, val format: Format? = null) { // TODO make it dependent on simplexLinkMode preference fun link(mode: SimplexLinkMode): String? = when (format) { - is Format.Uri -> text + is Format.Uri -> if (text.startsWith("http://", ignoreCase = true) || text.startsWith("https://", ignoreCase = true)) text else "https://$text" is Format.SimplexLink -> if (mode == SimplexLinkMode.BROWSER) text else format.simplexUri is Format.Email -> "mailto:$text" is Format.Phone -> "tel:$text" @@ -3958,12 +3974,14 @@ sealed class Format { enum class SimplexLinkType(val linkType: String) { contact("contact"), invitation("invitation"), - group("group"); + group("group"), + channel("channel"); val description: String get() = generalGetString(when (this) { contact -> MR.strings.simplex_link_contact invitation -> MR.strings.simplex_link_invitation group -> MR.strings.simplex_link_group + channel -> MR.strings.simplex_link_channel }) } 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 c221e3c15e..979d79c839 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -5,6 +5,8 @@ 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 @@ -17,9 +19,9 @@ 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.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.* @@ -45,6 +47,9 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant 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.util.Date @@ -86,18 +91,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) @@ -107,20 +101,10 @@ class AppPreferences { val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true) val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true) 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 privacyShortLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SHORT_LINKS, false) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) @@ -157,23 +141,12 @@ 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 networkSMPWebPort = mkBoolPreference(SHARED_PREFS_NETWORK_SMP_WEB_PORT, false) + val networkSMPWebPortServers: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS, SMPWebPortServers.default) val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout) val networkTCPTimeoutPerKb = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB, NetCfg.defaults.tcpTimeoutPerKb, NetCfg.proxyDefaults.tcpTimeoutPerKb) @@ -328,7 +301,19 @@ class AppPreferences { set = fun(value) = settings.putString(prefName, value.toString()) ) - // LALAL + private inline fun > mkSafeEnumPreference(key: String, default: T): SharedPreference = SharedPreference( + get = { + val value = settings.getString(key, "") + if (value == "") return@SharedPreference default + try { + enumValueOf(value) + } catch (e: IllegalArgumentException) { + default + } + }, + set = { value -> settings.putString(key, value.name) } + ) + private fun mkDatePreference(prefName: String, default: Instant?): SharedPreference = SharedPreference( get = { @@ -380,6 +365,7 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" + private const val SHARED_PREFS_PRIVACY_SHORT_LINKS = "PrivacyShortLinks" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" @@ -412,7 +398,7 @@ 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_SMP_WEB_PORT = "NetworkSMPWebPort" + private const val SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS = "NetworkSMPWebPortServers" private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" @@ -482,7 +468,7 @@ object ChatController { var ctrl: ChatCtrl? = -1 val appPrefs: AppPreferences by lazy { AppPreferences() } - val messagesChannel: Channel = Channel() + val messagesChannel: Channel = Channel() val chatModel = ChatModel private var receiverStarted = false @@ -495,8 +481,7 @@ object ChatController { 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 } @@ -505,8 +490,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 } @@ -544,9 +528,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") } @@ -627,9 +611,9 @@ object ChatController { val hasUser = chatModel.currentUser.value != null chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None - withChats { + withContext(Dispatchers.Main) { val chats = apiGetChats(rhId) - updateChats(chats) + chatModel.chatsContext.updateChats(chats) } chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList() chatModel.activeChatTagFilter.value = null @@ -658,11 +642,11 @@ object ChatController { messagesChannel.trySend(msg) } if (finishedWithoutTimeout == null) { - Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType) + Log.e(TAG, "Timeout reached while processing received message: " + msg.responseType) if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.possible_slow_function_title), - text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()), + text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()), shareText = true ) } @@ -678,7 +662,7 @@ object ChatController { } } - suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): CR { + suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): API { val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized") return withContext(Dispatchers.IO) { @@ -687,37 +671,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) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c) // coroutine was cancelled already, no need to process response (helps with apiListMembers - very heavy query in large groups) interruptIfCancelled() - val r = 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 @@ -727,14 +710,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 { @@ -746,8 +730,8 @@ object ChatController { suspend fun listUsers(rh: Long?): List { val r = sendCmd(rh, CC.ListUsers()) - if (r is CR.UsersList) { - val users = if (rh == null) r.users else r.users.map { it.copy(user = it.user.copy(remoteHostId = rh)) } + if (r is API.Result && r.res is CR.UsersList) { + val users = if (rh == null) r.res.users else r.res.users.map { it.copy(user = it.user.copy(remoteHostId = rh)) } return users.sortedBy { it.user.chatViewName } } Log.d(TAG, "listUsers: ${r.responseType} ${r.details}") @@ -756,26 +740,26 @@ object ChatController { suspend fun apiSetActiveUser(rh: Long?, userId: Long, viewPwd: String?): User { val r = sendCmd(rh, CC.ApiSetActiveUser(userId, viewPwd)) - if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh) + if (r is API.Result && r.res is CR.ActiveUser) return r.res.user.updateRemoteHostId(rh) Log.d(TAG, "apiSetActiveUser: ${r.responseType} ${r.details}") throw Exception("failed to set the user as active ${r.responseType} ${r.details}") } suspend fun apiSetAllContactReceipts(rh: Long?, enable: Boolean) { val r = sendCmd(rh, CC.SetAllContactReceipts(enable)) - if (r is CR.CmdOk) return + if (r.result is CR.CmdOk) return throw Exception("failed to set receipts for all users ${r.responseType} ${r.details}") } suspend fun apiSetUserContactReceipts(u: User, userMsgReceiptSettings: UserMsgReceiptSettings) { val r = sendCmd(u.remoteHostId, CC.ApiSetUserContactReceipts(u.userId, userMsgReceiptSettings)) - if (r is CR.CmdOk) return + if (r.result is CR.CmdOk) return throw Exception("failed to set receipts for user contacts ${r.responseType} ${r.details}") } suspend fun apiSetUserGroupReceipts(u: User, userMsgReceiptSettings: UserMsgReceiptSettings) { val r = sendCmd(u.remoteHostId, CC.ApiSetUserGroupReceipts(u.userId, userMsgReceiptSettings)) - if (r is CR.CmdOk) return + if (r.result is CR.CmdOk) return throw Exception("failed to set receipts for user groups ${r.responseType} ${r.details}") } @@ -793,20 +777,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}") @@ -815,7 +799,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}") @@ -824,15 +808,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}") } @@ -840,52 +822,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() @@ -894,8 +876,7 @@ 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 @@ -903,9 +884,10 @@ object ChatController { 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 + 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)) @@ -915,7 +897,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 @@ -923,7 +905,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 @@ -943,8 +925,8 @@ object ChatController { 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), @@ -956,7 +938,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) @@ -971,37 +953,27 @@ 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 - } - } + val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, itemId)) + if (r is API.Result && r.res is CR.ApiChatItemInfo) return r.res.chatItemInfo + apiErrorAlert("apiGetChatItemInfo", generalGetString(MR.strings.error_loading_details), r) + return null } suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemIds: List, ttl: Int?): List? { @@ -1010,21 +982,18 @@ object ChatController { } 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 - } - } + val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds)) + if (r is API.Result && r.res is CR.ForwardPlan) return r.res + apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r) + return null } suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? { val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, updatedMessage, live)) when { - r is 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), @@ -1044,7 +1013,7 @@ object ChatController { suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? { val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, itemId, add, reaction)) - if (r is CR.ChatItemReaction) return r.reaction.chatReaction.chatItem + if (r is API.Result && r.res is CR.ChatItemReaction) return r.res.reaction.chatReaction.chatItem Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}") return null } @@ -1052,35 +1021,35 @@ 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 + 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 CR.GroupChatItemsDeleted) return r + 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 CR.ChatItemsDeleted) return r.chatItemDeletions + 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 } @@ -1088,121 +1057,84 @@ object ChatController { 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 } @@ -1213,37 +1145,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)) } @@ -1251,13 +1178,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 = @@ -1268,207 +1191,199 @@ object ChatController { suspend fun apiContactInfo(rh: Long?, contactId: Long): Pair? { val r = sendCmd(rh, CC.APIContactInfo(contactId)) - if (r is CR.ContactInfo) return r.connectionStats_ to r.customUserProfile + if (r is API.Result && r.res is CR.ContactInfo) return r.res.connectionStats_ to r.res.customUserProfile Log.e(TAG, "apiContactInfo bad response: ${r.responseType} ${r.details}") return null } suspend fun apiGroupMemberInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { val r = sendCmd(rh, CC.APIGroupMemberInfo(groupId, groupMemberId)) - if (r is CR.GroupMemberInfo) return Pair(r.member, r.connectionStats_) + if (r is API.Result && r.res is CR.GroupMemberInfo) return r.res.member to r.res.connectionStats_ Log.e(TAG, "apiGroupMemberInfo bad response: ${r.responseType} ${r.details}") return null } suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair? { val r = sendCmd(rh, CC.APIContactQueueInfo(contactId)) - if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo) + if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) return null } suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) - if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo) + if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) return null } suspend fun apiSwitchContact(rh: Long?, contactId: Long): ConnectionStats? { val r = sendCmd(rh, CC.APISwitchContact(contactId)) - if (r is CR.ContactSwitchStarted) return r.connectionStats + if (r is API.Result && r.res is CR.ContactSwitchStarted) return r.res.connectionStats apiErrorAlert("apiSwitchContact", generalGetString(MR.strings.error_changing_address), r) return null } suspend fun apiSwitchGroupMember(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { val r = sendCmd(rh, CC.APISwitchGroupMember(groupId, groupMemberId)) - if (r is CR.GroupMemberSwitchStarted) return Pair(r.member, r.connectionStats) + if (r is API.Result && r.res is CR.GroupMemberSwitchStarted) return r.res.member to r.res.connectionStats apiErrorAlert("apiSwitchGroupMember", generalGetString(MR.strings.error_changing_address), r) return null } suspend fun apiAbortSwitchContact(rh: Long?, contactId: Long): ConnectionStats? { val r = sendCmd(rh, CC.APIAbortSwitchContact(contactId)) - if (r is CR.ContactSwitchAborted) return r.connectionStats + if (r is API.Result && r.res is CR.ContactSwitchAborted) return r.res.connectionStats apiErrorAlert("apiAbortSwitchContact", generalGetString(MR.strings.error_aborting_address_change), r) return null } suspend fun apiAbortSwitchGroupMember(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { val r = sendCmd(rh, CC.APIAbortSwitchGroupMember(groupId, groupMemberId)) - if (r is CR.GroupMemberSwitchAborted) return Pair(r.member, r.connectionStats) + if (r is API.Result && r.res is CR.GroupMemberSwitchAborted) return r.res.member to r.res.connectionStats apiErrorAlert("apiAbortSwitchGroupMember", generalGetString(MR.strings.error_aborting_address_change), r) return null } suspend fun apiSyncContactRatchet(rh: Long?, contactId: Long, force: Boolean): ConnectionStats? { val r = sendCmd(rh, CC.APISyncContactRatchet(contactId, force)) - if (r is CR.ContactRatchetSyncStarted) return r.connectionStats + if (r is API.Result && r.res is CR.ContactRatchetSyncStarted) return r.res.connectionStats apiErrorAlert("apiSyncContactRatchet", generalGetString(MR.strings.error_synchronizing_connection), r) return null } suspend fun apiSyncGroupMemberRatchet(rh: Long?, groupId: Long, groupMemberId: Long, force: Boolean): Pair? { val r = sendCmd(rh, CC.APISyncGroupMemberRatchet(groupId, groupMemberId, force)) - if (r is CR.GroupMemberRatchetSyncStarted) return Pair(r.member, r.connectionStats) + if (r is API.Result && r.res is CR.GroupMemberRatchetSyncStarted) return r.res.member to r.res.connectionStats apiErrorAlert("apiSyncGroupMemberRatchet", generalGetString(MR.strings.error_synchronizing_connection), r) return null } suspend fun apiGetContactCode(rh: Long?, contactId: Long): Pair? { val r = sendCmd(rh, CC.APIGetContactCode(contactId)) - if (r is CR.ContactCode) return r.contact to r.connectionCode + if (r is API.Result && r.res is CR.ContactCode) return r.res.contact to r.res.connectionCode Log.e(TAG,"failed to get contact code: ${r.responseType} ${r.details}") return null } suspend fun apiGetGroupMemberCode(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { val r = sendCmd(rh, CC.APIGetGroupMemberCode(groupId, groupMemberId)) - if (r is CR.GroupMemberCode) return r.member to r.connectionCode + if (r is API.Result && r.res is CR.GroupMemberCode) return r.res.member to r.res.connectionCode Log.e(TAG,"failed to get group member code: ${r.responseType} ${r.details}") return null } suspend fun apiVerifyContact(rh: Long?, contactId: Long, connectionCode: String?): Pair? { - return when (val r = sendCmd(rh, CC.APIVerifyContact(contactId, connectionCode))) { - is CR.ConnectionVerified -> r.verified to r.expectedCode - else -> null - } + val r = sendCmd(rh, CC.APIVerifyContact(contactId, connectionCode)) + if (r is API.Result && r.res is CR.ConnectionVerified) return r.res.verified to r.res.expectedCode + Log.e(TAG, "apiVerifyContact bad response: ${r.responseType} ${r.details}") + return null } suspend fun apiVerifyGroupMember(rh: Long?, groupId: Long, groupMemberId: Long, connectionCode: String?): Pair? { - return when (val r = sendCmd(rh, CC.APIVerifyGroupMember(groupId, groupMemberId, connectionCode))) { - is CR.ConnectionVerified -> r.verified to r.expectedCode - else -> null - } + val r = sendCmd(rh, CC.APIVerifyGroupMember(groupId, groupMemberId, connectionCode)) + if (r is API.Result && r.res is CR.ConnectionVerified) return r.res.verified to r.res.expectedCode + Log.e(TAG, "apiVerifyGroupMember bad response: ${r.responseType} ${r.details}") + return null } - - - suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair?, (() -> Unit)?> { + suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair?, (() -> Unit)?> { val userId = try { currentUserId("apiAddContact") } catch (e: Exception) { return null to null } - val r = sendCmd(rh, CC.APIAddContact(userId, incognito)) - return when (r) { - is CR.Invitation -> (r.connReqInvitation to r.connection) to null - else -> { - if (!(networkErrorAlert(r))) { - return null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) } - } - null to null - } + val short = appPrefs.privacyShortLinks.get() + val r = sendCmd(rh, CC.APIAddContact(userId, short = short, incognito = incognito)) + return when { + r is API.Result && r.res is CR.Invitation -> (r.res.connLinkInvitation to r.res.connection) to null + !(networkErrorAlert(r)) -> null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) } + else -> null to null } } suspend fun apiSetConnectionIncognito(rh: Long?, connId: Long, incognito: Boolean): PendingContactConnection? { val r = sendCmd(rh, CC.ApiSetConnectionIncognito(connId, incognito)) - - 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) } + return null } 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 - } + if (r is API.Result && r.res is CR.ConnectionUserChanged) return r.res.toConnection + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r) } - } - - 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 apiConnectPlan(rh: Long?, connLink: String): Pair? { + val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } + val r = sendCmd(rh, CC.APIConnectPlan(userId, connLink)) + if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan + apiConnectResponseAlert(r) + return null + } + + suspend fun apiConnect(rh: Long?, incognito: Boolean, connLink: CreatedConnLink): PendingContactConnection? { val userId = try { currentUserId("apiConnect") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.APIConnect(userId, incognito, connReq)) + val r = sendCmd(rh, CC.APIConnect(userId, incognito, connLink)) when { - r is CR.SentConfirmation -> return r.connection - r is CR.SentInvitation -> return r.connection - r is CR.ContactAlreadyExists -> { + r is API.Result && r.res is CR.SentConfirmation -> return r.res.connection + r is API.Result && r.res is CR.SentInvitation -> return r.res.connection + r is API.Result && r.res is CR.ContactAlreadyExists -> AlertManager.shared.showAlertMsg( generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.contact.displayName) + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.res.contact.displayName) ) - return null - } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat - && r.chatError.errorType is ChatErrorType.InvalidConnReq -> { + else -> apiConnectResponseAlert(r) + } + return null + } + + private fun apiConnectResponseAlert(r: API) { + when { + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.InvalidConnReq -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.invalid_connection_link), generalGetString(MR.strings.please_check_correct_link_and_maybe_ask_for_a_new_one) ) - return null } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.SMP - && r.chatError.agentError.smpErr is SMPErrorType.AUTH -> { + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.UnsupportedConnReq -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.unsupported_connection_link), + generalGetString(MR.strings.link_requires_newer_app_version_please_upgrade) + ) + } + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.SMP + && r.err.agentError.smpErr is SMPErrorType.AUTH -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_error_auth), generalGetString(MR.strings.connection_error_auth_desc) ) - return null } - r is 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 } } } @@ -1476,22 +1391,18 @@ object ChatController { suspend fun apiConnectContactViaAddress(rh: Long?, incognito: Boolean, contactId: Long): Contact? { val userId = try { currentUserId("apiConnectContactViaAddress") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId)) - when { - r is CR.SentInvitationToContact -> return r.contact - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiConnectContactViaAddress", generalGetString(MR.strings.connection_error), r) - } - return null - } + if (r is API.Result && r.res is CR.SentInvitationToContact) return r.res.contact + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiConnectContactViaAddress", generalGetString(MR.strings.connection_error), r) } + return null } suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { val cInfo = chat.chatInfo if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) { - withChats { - removeChat(chat.remoteHostId, cInfo.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(chat.remoteHostId, cInfo.id) } } } @@ -1499,10 +1410,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 @@ -1523,13 +1435,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 @@ -1539,11 +1450,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() @@ -1553,7 +1464,7 @@ object ChatController { suspend fun apiClearChat(rh: Long?, type: ChatType, id: Long): ChatInfo? { val r = sendCmd(rh, CC.ApiClearChat(type, id)) - if (r is CR.ChatCleared) return r.chatInfo + if (r is API.Result && r.res is CR.ChatCleared) return r.res.chatInfo Log.e(TAG, "apiClearChat bad response: ${r.responseType} ${r.details}") return null } @@ -1561,9 +1472,9 @@ object ChatController { suspend fun apiUpdateProfile(rh: Long?, profile: Profile): Pair>? { val userId = kotlin.runCatching { currentUserId("apiUpdateProfile") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiUpdateProfile(userId, profile)) - if (r is CR.UserProfileNoChange) return profile to emptyList() - if (r is CR.UserProfileUpdated) return r.toProfile to r.updateSummary.changedContacts - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName) { + if (r is API.Result && r.res is CR.UserProfileNoChange) return profile to emptyList() + if (r is API.Result && r.res is CR.UserProfileUpdated) return r.res.toProfile to r.res.updateSummary.changedContacts + if (r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.DuplicateName) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc)) } Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}") @@ -1572,73 +1483,70 @@ object ChatController { suspend fun apiSetProfileAddress(rh: Long?, on: Boolean): User? { val userId = try { currentUserId("apiSetProfileAddress") } catch (e: Exception) { return null } - return when (val r = sendCmd(rh, CC.ApiSetProfileAddress(userId, on))) { - is CR.UserProfileNoChange -> null - is CR.UserProfileUpdated -> r.user.updateRemoteHostId(rh) + val r = sendCmd(rh, CC.ApiSetProfileAddress(userId, on)) + return when { + r is API.Result && r.res is CR.UserProfileNoChange -> null + r is API.Result && r.res is CR.UserProfileUpdated -> r.res.user.updateRemoteHostId(rh) else -> throw Exception("failed to set profile address: ${r.responseType} ${r.details}") } } suspend fun apiSetContactPrefs(rh: Long?, contactId: Long, prefs: ChatPreferences): Contact? { val r = sendCmd(rh, CC.ApiSetContactPrefs(contactId, prefs)) - if (r is CR.ContactPrefsUpdated) return r.toContact + if (r is API.Result && r.res is CR.ContactPrefsUpdated) return r.res.toContact Log.e(TAG, "apiSetContactPrefs bad response: ${r.responseType} ${r.details}") return null } suspend fun apiSetContactAlias(rh: Long?, contactId: Long, localAlias: String): Contact? { val r = sendCmd(rh, CC.ApiSetContactAlias(contactId, localAlias)) - if (r is CR.ContactAliasUpdated) return r.toContact + if (r is API.Result && r.res is CR.ContactAliasUpdated) return r.res.toContact Log.e(TAG, "apiSetContactAlias bad response: ${r.responseType} ${r.details}") return null } suspend fun apiSetGroupAlias(rh: Long?, groupId: Long, localAlias: String): GroupInfo? { val r = sendCmd(rh, CC.ApiSetGroupAlias(groupId, localAlias)) - if (r is 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?, short: Boolean): CreatedConnLink? { val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } - val r = sendCmd(rh, CC.ApiCreateMyAddress(userId)) - return when (r) { - is CR.UserContactLinkCreated -> r.connReqContact - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r) - } - null - } + val r = sendCmd(rh, CC.ApiCreateMyAddress(userId, short)) + if (r is API.Result && r.res is CR.UserContactLinkCreated) return r.res.connLinkContact + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r) } + return null } suspend fun apiDeleteUserAddress(rh: Long?): User? { val userId = try { currentUserId("apiDeleteUserAddress") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.ApiDeleteMyAddress(userId)) - if (r is CR.UserContactLinkDeleted) return r.user.updateRemoteHostId(rh) + if (r is API.Result && r.res is CR.UserContactLinkDeleted) return r.res.user.updateRemoteHostId(rh) Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}") return null } @@ -1646,9 +1554,9 @@ object ChatController { private suspend fun apiGetUserAddress(rh: Long?): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("apiGetUserAddress") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiShowMyAddress(userId)) - if (r is CR.UserContactLink) return r.contactLink - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore - && r.chatError.storeError is StoreError.UserContactLinkNotFound + if (r is API.Result && r.res is CR.UserContactLink) return r.res.contactLink + if (r is API.Error && r.err is ChatError.ChatErrorStore + && r.err.storeError is StoreError.UserContactLinkNotFound ) { return null } @@ -1659,9 +1567,9 @@ object ChatController { suspend fun userAddressAutoAccept(rh: Long?, autoAccept: AutoAccept?): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("userAddressAutoAccept") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiAddressAutoAccept(userId, autoAccept)) - if (r is CR.UserContactLinkUpdated) return r.contactLink - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore - && r.chatError.storeError is StoreError.UserContactLinkNotFound + if (r is API.Result && r.res is CR.UserContactLinkUpdated) return r.res.contactLink + if (r is API.Error && r.err is ChatError.ChatErrorStore + && r.err.storeError is StoreError.UserContactLinkNotFound ) { return null } @@ -1672,10 +1580,10 @@ object ChatController { suspend fun apiAcceptContactRequest(rh: Long?, incognito: Boolean, contactReqId: Long): Contact? { val r = sendCmd(rh, CC.ApiAcceptContact(incognito, contactReqId)) return when { - r is CR.AcceptingContactRequest -> r.contact - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.SMP - && r.chatError.agentError.smpErr is SMPErrorType.AUTH -> { + r is API.Result && r.res is CR.AcceptingContactRequest -> r.res.contact + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.SMP + && r.err.agentError.smpErr is SMPErrorType.AUTH -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_error_auth), generalGetString(MR.strings.sender_may_have_deleted_the_connection_request) @@ -1693,89 +1601,89 @@ object ChatController { suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Boolean { val r = sendCmd(rh, CC.ApiRejectContact(contactReqId)) - if (r is CR.ContactRequestRejected) return true + if (r is API.Result && r.res is CR.ContactRequestRejected) return true Log.e(TAG, "apiRejectContactRequest bad response: ${r.responseType} ${r.details}") return false } suspend fun apiGetCallInvitations(rh: Long?): List { val r = sendCmd(rh, CC.ApiGetCallInvitations()) - if (r is CR.CallInvitations) return r.callInvitations + if (r is API.Result && r.res is CR.CallInvitations) return r.res.callInvitations Log.e(TAG, "apiGetCallInvitations bad response: ${r.responseType} ${r.details}") return emptyList() } suspend fun apiSendCallInvitation(rh: Long?, contact: Contact, callType: CallType): Boolean { val r = sendCmd(rh, CC.ApiSendCallInvitation(contact, callType)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiRejectCall(rh: Long?, contact: Contact): Boolean { val r = sendCmd(rh, CC.ApiRejectCall(contact)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiSendCallOffer(rh: Long?, contact: Contact, rtcSession: String, rtcIceCandidates: String, media: CallMediaType, capabilities: CallCapabilities): Boolean { val webRtcSession = WebRTCSession(rtcSession, rtcIceCandidates) val callOffer = WebRTCCallOffer(CallType(media, capabilities), webRtcSession) val r = sendCmd(rh, CC.ApiSendCallOffer(contact, callOffer)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiSendCallAnswer(rh: Long?, contact: Contact, rtcSession: String, rtcIceCandidates: String): Boolean { val answer = WebRTCSession(rtcSession, rtcIceCandidates) val r = sendCmd(rh, CC.ApiSendCallAnswer(contact, answer)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiSendCallExtraInfo(rh: Long?, contact: Contact, rtcIceCandidates: String): Boolean { val extraInfo = WebRTCExtraInfo(rtcIceCandidates) val r = sendCmd(rh, CC.ApiSendCallExtraInfo(contact, extraInfo)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiEndCall(rh: Long?, contact: Contact): Boolean { val r = sendCmd(rh, CC.ApiEndCall(contact)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiCallStatus(rh: Long?, contact: Contact, status: WebRTCCallStatus): Boolean { val r = sendCmd(rh, CC.ApiCallStatus(contact, status)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiGetNetworkStatuses(rh: Long?): List? { val r = sendCmd(rh, CC.ApiGetNetworkStatuses()) - if (r is CR.NetworkStatuses) return r.networkStatuses + if (r is API.Result && r.res is CR.NetworkStatuses) return r.res.networkStatuses Log.e(TAG, "apiGetNetworkStatuses bad response: ${r.responseType} ${r.details}") return null } suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean { val r = sendCmd(rh, CC.ApiChatRead(type, id)) - if (r is CR.CmdOk) return true + 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 + if (r.result is CR.CmdOk) return true Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}") return false } suspend fun apiChatUnread(rh: Long?, type: ChatType, id: Long, unreadChat: Boolean): Boolean { val r = sendCmd(rh, CC.ApiChatUnread(type, id, unreadChat)) - if (r is CR.CmdOk) return true + if (r.result is CR.CmdOk) return true Log.e(TAG, "apiChatUnread bad response: ${r.responseType} ${r.details}") return false } suspend fun uploadStandaloneFile(user: UserLike, file: CryptoFile, ctrl: ChatCtrl? = null): Pair { val r = sendCmd(null, CC.ApiUploadStandaloneFile(user.userId, file), ctrl) - return if (r is CR.SndStandaloneFileCreated) { - r.fileTransferMeta to null + return if (r is API.Result && r.res is CR.SndStandaloneFileCreated) { + r.res.fileTransferMeta to null } else { Log.e(TAG, "uploadStandaloneFile error: $r") null to r.toString() @@ -1784,8 +1692,8 @@ object ChatController { suspend fun downloadStandaloneFile(user: UserLike, url: String, file: CryptoFile, ctrl: ChatCtrl? = null): Pair { val r = sendCmd(null, CC.ApiDownloadStandaloneFile(user.userId, url, file), ctrl) - return if (r is CR.RcvStandaloneFileCreated) { - r.rcvFileTransfer to null + return if (r is API.Result && r.res is CR.RcvStandaloneFileCreated) { + r.res.rcvFileTransfer to null } else { Log.e(TAG, "downloadStandaloneFile error: $r") null to r.toString() @@ -1794,8 +1702,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 @@ -1805,7 +1713,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( @@ -1816,10 +1724,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) }) @@ -1847,21 +1755,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 @@ -1877,7 +1783,7 @@ object ChatController { private fun showFilesToApproveAlert( srvsToApprove: Set, - otherFileErrs: List, + otherFileErrs: List, approveFiles: (() -> Unit) ) { val srvsToApproveStr = srvsToApprove.sorted().joinToString(separator = ", ") @@ -1940,9 +1846,9 @@ object ChatController { suspend fun apiCancelFile(rh: Long?, fileId: Long, ctrl: ChatCtrl? = null): AChatItem? { val r = sendCmd(rh, CC.CancelFile(fileId), ctrl) - return when (r) { - is CR.SndFileCancelled -> r.chatItem_ - is CR.RcvFileCancelled -> r.chatItem_ + return when { + r is API.Result && r.res is CR.SndFileCancelled -> r.res.chatItem_ + r is API.Result && r.res is CR.RcvFileCancelled -> r.res.chatItem_ else -> { Log.d(TAG, "apiCancelFile bad response: ${r.responseType} ${r.details}") null @@ -1953,34 +1859,32 @@ object ChatController { suspend fun apiNewGroup(rh: Long?, incognito: Boolean, groupProfile: GroupProfile): GroupInfo? { val userId = kotlin.runCatching { currentUserId("apiNewGroup") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiNewGroup(userId, incognito, groupProfile)) - if (r is CR.GroupCreated) return r.groupInfo + if (r is API.Result && r.res is CR.GroupCreated) return r.res.groupInfo Log.e(TAG, "apiNewGroup bad response: ${r.responseType} ${r.details}") return null } suspend fun apiAddMember(rh: Long?, groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? { val r = sendCmd(rh, CC.ApiAddMember(groupId, contactId, memberRole)) - return when (r) { - is CR.SentGroupInvitation -> r.member - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiAddMember", generalGetString(MR.strings.error_adding_members), r) - } - null - } + if (r is API.Result && r.res is CR.SentGroupInvitation) return r.res.member + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiAddMember", generalGetString(MR.strings.error_adding_members), r) } + return null } suspend fun apiJoinGroup(rh: Long?, groupId: Long) { val r = sendCmd(rh, CC.ApiJoinGroup(groupId)) - when (r) { - is CR.UserAcceptedGroupSent -> - withChats { - updateGroup(rh, r.groupInfo) + 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)) @@ -1995,58 +1899,53 @@ object ChatController { } } - suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List): List? = - when (val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds))) { - is CR.UserDeletedMembers -> r.members - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r) - } - null - } + suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean = false): List? { + val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages)) + if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.members + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r) } + return null + } - suspend fun apiMembersRole(rh: Long?, groupId: Long, memberIds: List, memberRole: GroupMemberRole): List = - when (val r = sendCmd(rh, CC.ApiMembersRole(groupId, memberIds, memberRole))) { - is CR.MembersRoleUser -> r.members - else -> { - 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 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 = - when (val r = sendCmd(rh, CC.ApiBlockMembersForAll(groupId, memberIds, blocked))) { - is CR.MembersBlockedForAllUser -> r.members - else -> { - 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 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 -> { @@ -2060,82 +1959,65 @@ object ChatController { } } - suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { - return when (val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole))) { - is CR.GroupLinkCreated -> r.connReqContact to r.memberRole - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) - } - null - } + suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { + val short = appPrefs.privacyShortLinks.get() + val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole, short)) + if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.connLinkContact to r.res.memberRole + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) } + return null } - suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { - return when (val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole))) { - is CR.GroupLink -> r.connReqContact to r.memberRole - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiGroupLinkMemberRole", generalGetString(MR.strings.error_updating_link_for_group), r) - } - null - } + suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { + val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole)) + if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiGroupLinkMemberRole", generalGetString(MR.strings.error_updating_link_for_group), r) } + return null } suspend fun apiDeleteGroupLink(rh: Long?, groupId: Long): Boolean { - return when (val r = sendCmd(rh, CC.APIDeleteGroupLink(groupId))) { - is CR.GroupLinkDeleted -> true - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiDeleteGroupLink", generalGetString(MR.strings.error_deleting_link_for_group), r) - } - false - } + val r = sendCmd(rh, CC.APIDeleteGroupLink(groupId)) + if (r is API.Result && r.res is CR.GroupLinkDeleted) return true + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiDeleteGroupLink", generalGetString(MR.strings.error_deleting_link_for_group), r) } + return false } - suspend fun apiGetGroupLink(rh: Long?, groupId: Long): Pair? { - return when (val r = sendCmd(rh, CC.APIGetGroupLink(groupId))) { - is CR.GroupLink -> r.connReqContact to r.memberRole - else -> { - Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") - null - } - } + suspend fun apiGetGroupLink(rh: Long?, groupId: Long): Pair? { + val r = sendCmd(rh, CC.APIGetGroupLink(groupId)) + if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") + return null } suspend fun apiCreateMemberContact(rh: Long?, groupId: Long, groupMemberId: Long): Contact? { - return when (val r = sendCmd(rh, CC.APICreateMemberContact(groupId, groupMemberId))) { - is CR.NewMemberContact -> r.contact - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiCreateMemberContact", generalGetString(MR.strings.error_creating_member_contact), r) - } - null - } + val r = sendCmd(rh, CC.APICreateMemberContact(groupId, groupMemberId)) + if (r is API.Result && r.res is CR.NewMemberContact) return r.res.contact + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiCreateMemberContact", generalGetString(MR.strings.error_creating_member_contact), r) } + return null } suspend fun apiSendMemberContactInvitation(rh: Long?, contactId: Long, mc: MsgContent): Contact? { - return when (val r = sendCmd(rh, CC.APISendMemberContactInvitation(contactId, mc))) { - is CR.NewMemberContactSentInv -> r.contact - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiSendMemberContactInvitation", generalGetString(MR.strings.error_sending_message_contact_invitation), r) - } - null - } + val r = sendCmd(rh, CC.APISendMemberContactInvitation(contactId, mc)) + if (r is API.Result && r.res is CR.NewMemberContactSentInv) return r.res.contact + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiSendMemberContactInvitation", generalGetString(MR.strings.error_sending_message_contact_invitation), r) } + return null } suspend fun allowFeatureToContact(rh: Long?, contact: Contact, feature: ChatFeature, param: Int? = null) { val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val toContact = apiSetContactPrefs(rh, contact.contactId, prefs) if (toContact != null) { - withChats { - updateContact(rh, toContact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rh, toContact) } } } @@ -2144,7 +2026,7 @@ object ChatController { suspend fun listRemoteHosts(): List? { val r = sendCmd(null, CC.ListRemoteHosts()) - if (r is CR.RemoteHostList) return r.remoteHosts + if (r is API.Result && r.res is CR.RemoteHostList) return r.res.remoteHosts apiErrorAlert("listRemoteHosts", generalGetString(MR.strings.error_alert_title), r) return null } @@ -2157,14 +2039,14 @@ object ChatController { suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true, address: RemoteCtrlAddress?, port: Int?): CR.RemoteHostStarted? { val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast, address, port)) - if (r is CR.RemoteHostStarted) return r + if (r is API.Result && r.res is CR.RemoteHostStarted) return r.res apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r) return null } suspend fun switchRemoteHost (rhId: Long?): RemoteHostInfo? { val r = sendCmd(null, CC.SwitchRemoteHost(rhId)) - if (r is CR.CurrentRemoteHost) return r.remoteHost_ + if (r is API.Result && r.res is CR.CurrentRemoteHost) return r.res.remoteHost_ apiErrorAlert("switchRemoteHost", generalGetString(MR.strings.error_alert_title), r) return null } @@ -2186,45 +2068,49 @@ object ChatController { suspend fun storeRemoteFile(rhId: Long, storeEncrypted: Boolean?, localPath: String): CryptoFile? { val r = sendCmd(null, CC.StoreRemoteFile(rhId, storeEncrypted, localPath)) - if (r is CR.RemoteFileStored) return r.remoteFileSource + if (r is API.Result && r.res is CR.RemoteFileStored) return r.res.remoteFileSource apiErrorAlert("storeRemoteFile", generalGetString(MR.strings.error_alert_title), r) return null } - suspend fun getRemoteFile(rhId: Long, file: RemoteFile): Boolean = sendCmd(null, CC.GetRemoteFile(rhId, file)) is CR.CmdOk + suspend fun getRemoteFile(rhId: Long, file: RemoteFile): Boolean = sendCmd(null, CC.GetRemoteFile(rhId, file)).result is CR.CmdOk - suspend fun connectRemoteCtrl(desktopAddress: String): Pair { + suspend fun connectRemoteCtrl(desktopAddress: String): Pair { val r = sendCmd(null, CC.ConnectRemoteCtrl(desktopAddress)) - return if (r is CR.RemoteCtrlConnecting) SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion) to null - else if (r is CR.ChatCmdError) null to r - else { - apiErrorAlert("connectRemoteCtrl", generalGetString(MR.strings.error_alert_title), r) - null to null + return when { + r is API.Result && r.res is CR.RemoteCtrlConnecting -> SomeRemoteCtrl(r.res.remoteCtrl_, r.res.ctrlAppInfo, r.res.appVersion) to null + r is API.Error -> null to r.err + else -> { + apiErrorAlert("connectRemoteCtrl", generalGetString(MR.strings.error_alert_title), r) + null to null + } } } suspend fun findKnownRemoteCtrl(): Boolean = sendCommandOkResp(null, CC.FindKnownRemoteCtrl()) - suspend fun confirmRemoteCtrl(rcId: Long): Pair { + suspend fun confirmRemoteCtrl(rcId: Long): Pair { val r = sendCmd(null, CC.ConfirmRemoteCtrl(remoteCtrlId = rcId)) - return if (r is CR.RemoteCtrlConnecting) SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion) to null - else if (r is CR.ChatCmdError) null to r - else { - apiErrorAlert("confirmRemoteCtrl", generalGetString(MR.strings.error_alert_title), r) - null to null + return when { + r is API.Result && r.res is CR.RemoteCtrlConnecting -> SomeRemoteCtrl(r.res.remoteCtrl_, r.res.ctrlAppInfo, r.res.appVersion) to null + r is API.Error -> null to r.err + else -> { + apiErrorAlert("confirmRemoteCtrl", generalGetString(MR.strings.error_alert_title), r) + null to null + } } } suspend fun verifyRemoteCtrlSession(sessionCode: String): RemoteCtrlInfo? { val r = sendCmd(null, CC.VerifyRemoteCtrlSession(sessionCode)) - if (r is CR.RemoteCtrlConnected) return r.remoteCtrl + if (r is API.Result && r.res is CR.RemoteCtrlConnected) return r.res.remoteCtrl apiErrorAlert("verifyRemoteCtrlSession", generalGetString(MR.strings.error_alert_title), r) return null } suspend fun listRemoteCtrls(): List? { val r = sendCmd(null, CC.ListRemoteCtrls()) - if (r is CR.RemoteCtrlList) return r.remoteCtrls + if (r is API.Result && r.res is CR.RemoteCtrlList) return r.res.remoteCtrls apiErrorAlert("listRemoteCtrls", generalGetString(MR.strings.error_alert_title), r) return null } @@ -2235,72 +2121,71 @@ object ChatController { private suspend fun sendCommandOkResp(rh: Long?, cmd: CC, ctrl: ChatCtrl? = null): Boolean { val r = sendCmd(rh, cmd, ctrl) - val ok = r is CR.CmdOk + val ok = r is API.Result && r.res is CR.CmdOk if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error_alert_title), r) return ok } suspend fun apiGetVersion(): CoreVersionInfo? { val r = sendCmd(null, CC.ShowVersion()) - return if (r is CR.VersionInfo) { - r.versionInfo - } else { - Log.e(TAG, "apiGetVersion bad response: ${r.responseType} ${r.details}") - null - } + if (r is API.Result && r.res is CR.VersionInfo) return r.res.versionInfo + Log.e(TAG, "apiGetVersion bad response: ${r.responseType} ${r.details}") + return null } - private fun networkErrorAlert(r: CR): Boolean { + private fun networkErrorAlert(r: API): Boolean { + if (r !is API.Error) return false + val e = r.err return when { - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.BROKER - && r.chatError.agentError.brokerErr is BrokerErrorType.TIMEOUT -> { + e is ChatError.ChatErrorAgent + && e.agentError is AgentErrorType.BROKER + && e.agentError.brokerErr is BrokerErrorType.TIMEOUT -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_timeout), - String.format(generalGetString(MR.strings.network_error_desc), serverHostname(r.chatError.agentError.brokerAddress)) + String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.agentError.brokerAddress)) ) true } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.BROKER - && r.chatError.agentError.brokerErr is BrokerErrorType.NETWORK -> { + e is ChatError.ChatErrorAgent + && e.agentError is AgentErrorType.BROKER + && e.agentError.brokerErr is BrokerErrorType.NETWORK -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_error), - String.format(generalGetString(MR.strings.network_error_desc), serverHostname(r.chatError.agentError.brokerAddress)) + String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.agentError.brokerAddress)) ) true } - 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 } @@ -2391,34 +2276,34 @@ 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) } } } @@ -2429,24 +2314,24 @@ object ChatController { } 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) } } } @@ -2456,11 +2341,11 @@ object ChatController { 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 (chatModel.chatsContext.hasChat(rhId, contactRequest.id)) { + chatModel.chatsContext.updateChatInfo(rhId, cInfo) } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) } } } @@ -2469,18 +2354,18 @@ object ChatController { 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) } } } @@ -2489,8 +2374,8 @@ object ChatController { if (chatModel.chatId.value == r.mergedContact.id) { chatModel.chatId.value = r.intoContact.id } - withChats { - removeChat(rhId, r.mergedContact.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, r.mergedContact.id) } } } @@ -2501,8 +2386,8 @@ object ChatController { is CR.ContactSubSummary -> { for (sub in r.contactSubscriptions) { if (active(r.user)) { - withChats { - updateContact(rhId, sub.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, sub.contact) } } val err = sub.contactError @@ -2528,20 +2413,20 @@ object ChatController { 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) } } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { if (cItem.isReport) { - addChatItem(rhId, cInfo, cItem) + chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem) } } } else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) { - withChats { - increaseUnreadCounter(rhId, r.user) + withContext(Dispatchers.Main) { + chatModel.chatsContext.increaseUnreadCounter(rhId, r.user) } } val file = cItem.file @@ -2562,12 +2447,12 @@ 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.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { if (cItem.isReport) { - updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) } } } @@ -2576,12 +2461,12 @@ 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 { + withContext(Dispatchers.Main) { if (r.reaction.chatReaction.chatItem.isReport) { - updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) } } } @@ -2590,8 +2475,8 @@ object ChatController { if (!active(r.user)) { r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled(deletedChatItem.chatItem)) { - withChats { - decreaseUnreadCounter(rhId, r.user) + withContext(Dispatchers.Main) { + chatModel.chatsContext.decreaseUnreadCounter(rhId, r.user) } } } @@ -2614,22 +2499,22 @@ 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) { - decreaseGroupReportsCounter(rhId, cInfo.id) + chatModel.chatsContext.decreaseGroupReportsCounter(rhId, cInfo.id) } } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { if (cItem.isReport) { if (toChatItem == null) { - removeChatItem(rhId, cInfo, cItem) + chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem) } else { - upsertChatItem(rhId, cInfo, toChatItem.chatItem) + chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem) } } } @@ -2640,9 +2525,9 @@ object ChatController { } 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 } @@ -2650,123 +2535,137 @@ 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.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.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.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.MembersRoleUser -> if (active(r.user)) { - withChats { + withContext(Dispatchers.Main) { r.members.forEach { member -> - upsertGroupMember(rhId, r.groupInfo, member) + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, member) } } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { r.members.forEach { member -> - upsertGroupMember(rhId, r.groupInfo, 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) } } 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) { @@ -2775,14 +2674,14 @@ object ChatController { } 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 -> @@ -2883,26 +2782,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 -> { @@ -2916,8 +2815,8 @@ object ChatController { } is CR.ContactDisabled -> { if (active(r.user)) { - withChats { - updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) } } } @@ -3041,20 +2940,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}") } } @@ -3106,8 +3006,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) } } } @@ -3116,10 +3016,10 @@ object ChatController { if (activeUser(rh, user)) { val cInfo = aChatItem.chatInfo val cItem = aChatItem.chatItem - withChats { upsertChatItem(rh, cInfo, cItem) } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) } + withContext(Dispatchers.Main) { if (cItem.isReport) { - upsertChatItem(rh, cInfo, cItem) + chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) } } } @@ -3133,15 +3033,16 @@ object ChatController { return } val cInfo = ChatInfo.Group(r.groupInfo) - withChats { + withContext(Dispatchers.Main) { + val chatsCtx = chatModel.chatsContext r.chatItemIDs.forEach { itemId -> - decreaseGroupReportsCounter(rhId, cInfo.id) - val cItem = chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach + 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 = getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id + val isLastChatItem = chatsCtx.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { ntfManager.cancelNotificationsForChat(cInfo.id) ntfManager.displayNotification( @@ -3156,22 +3057,25 @@ object ChatController { } else { CIDeleted.Deleted(Clock.System.now()) } - upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted))) + chatsCtx.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) + 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))) } - 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))) } } } @@ -3183,8 +3087,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) { + if (cItem.content.msgContent is MsgContent.MCReport) { + chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) + } + } if (createdChat) { notify() } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { @@ -3229,15 +3137,15 @@ 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) @@ -3274,12 +3182,12 @@ 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 smpWebPort = appPrefs.networkSMPWebPort.get() + val smpProxyMode = appPrefs.networkSMPProxyMode.get() + val smpProxyFallback = appPrefs.networkSMPProxyFallback.get() + val smpWebPortServers = appPrefs.networkSMPWebPortServers.get() val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get() val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get() @@ -3302,7 +3210,7 @@ object ChatController { sessionMode = sessionMode, smpProxyMode = smpProxyMode, smpProxyFallback = smpProxyFallback, - smpWebPort = smpWebPort, + smpWebPortServers = smpWebPortServers, tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpTimeoutPerKb = tcpTimeoutPerKb, @@ -3318,12 +3226,12 @@ 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.networkSMPWebPort.set(cfg.smpWebPort) + appPrefs.networkSMPProxyMode.set(cfg.smpProxyMode) + appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback) + appPrefs.networkSMPWebPortServers.set(cfg.smpWebPortServers) appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb) @@ -3412,11 +3320,11 @@ sealed class CC { class ApiJoinGroup(val groupId: 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): CC() + class ApiRemoveMembers(val groupId: Long, val memberIds: List, val withMessages: Boolean): CC() class ApiLeaveGroup(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC() class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC() - class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC() + class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole, val short: Boolean): CC() class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC() @@ -3455,11 +3363,11 @@ sealed class CC { class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC() class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC() class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC() - class APIAddContact(val userId: Long, val incognito: Boolean): CC() + class APIAddContact(val userId: Long, val short: Boolean, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class 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 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() @@ -3471,7 +3379,7 @@ sealed class CC { class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC() class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC() - class ApiCreateMyAddress(val userId: Long): CC() + class ApiCreateMyAddress(val userId: Long, val short: Boolean): CC() class ApiDeleteMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() @@ -3597,11 +3505,11 @@ sealed class CC { is ApiJoinGroup -> "/_join #$groupId" is ApiMembersRole -> "/_member role #$groupId ${memberIds.joinToString(",")} ${memberRole.memberRole}" is ApiBlockMembersForAll -> "/_block #$groupId ${memberIds.joinToString(",")} blocked=${onOff(blocked)}" - is ApiRemoveMembers -> "/_remove #$groupId ${memberIds.joinToString(",")}" + is ApiRemoveMembers -> "/_remove #$groupId ${memberIds.joinToString(",")} messages=${onOff(withMessages)}" is ApiLeaveGroup -> "/_leave #$groupId" is ApiListMembers -> "/_members #$groupId" is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}" - is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()}" + is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()} short=${onOff(short)}" is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIDeleteGroupLink -> "/_delete link #$groupId" is APIGetGroupLink -> "/_get link #$groupId" @@ -3640,11 +3548,11 @@ sealed class CC { is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId" is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else "" is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else "" - is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" + is APIAddContact -> "/_connect $userId short=${onOff(short)} incognito=${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is 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 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)}" @@ -3656,7 +3564,7 @@ sealed class CC { is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}" is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}" - is ApiCreateMyAddress -> "/_address $userId" + is ApiCreateMyAddress -> "/_address $userId short=${onOff(short)}" is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiShowMyAddress -> "/_show_address $userId" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" @@ -3677,8 +3585,8 @@ sealed class CC { is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${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" @@ -4425,13 +4333,13 @@ data class ParsedServerAddress ( @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 smpProxyMode: SMPProxyMode = SMPProxyMode.default, + val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.default, + val smpWebPortServers: SMPWebPortServers = SMPWebPortServers.default, val tcpConnectTimeout: Long, // microseconds val tcpTimeout: Long, // microseconds val tcpTimeoutPerKb: Long, // microseconds @@ -4529,12 +4437,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 @@ -4543,6 +4459,10 @@ enum class SMPProxyMode { @SerialName("unknown") Unknown, @SerialName("unprotected") Unprotected, @SerialName("never") Never; + + companion object { + val default = Always + } } @Serializable @@ -4550,6 +4470,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 @@ -5639,56 +5580,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) } } @@ -5743,16 +5750,14 @@ 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("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR() @@ -5769,7 +5774,7 @@ 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() @@ -5786,10 +5791,6 @@ sealed class CR { // TODO remove above @Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List): CR() @Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List): CR() - @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupRef): CR() - @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List): CR() - @Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR() - @Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR() @Serializable @SerialName("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() @@ -5805,7 +5806,7 @@ 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("userDeletedMembers") class UserDeletedMembers(val user: UserRef, val groupInfo: GroupInfo, val members: List): 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() @@ -5815,19 +5816,17 @@ sealed class 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("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): CR() - @Serializable @SerialName("deletedMember") class DeletedMember(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() + @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val withMessages: Boolean): CR() + @Serializable @SerialName("deletedMember") class DeletedMember(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember, val withMessages: Boolean): CR() @Serializable @SerialName("leftMember") class LeftMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("groupDeleted") class GroupDeleted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("contactsMerged") class ContactsMerged(val user: UserRef, val intoContact: Contact, val mergedContact: Contact): CR() - @Serializable @SerialName("groupInvitation") class GroupInvitation(val user: UserRef, val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR() - @Serializable @SerialName("groupRemoved") class GroupRemoved(val user: UserRef, val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR() - @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() - @Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR() @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @@ -5890,8 +5889,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() @@ -5931,8 +5928,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" @@ -5972,10 +5967,6 @@ sealed class CR { is ContactSubSummary -> "contactSubSummary" is NetworkStatusResp -> "networkStatus" is NetworkStatuses -> "networkStatuses" - is GroupSubscribed -> "groupSubscribed" - is MemberSubErrors -> "memberSubErrors" - is GroupEmpty -> "groupEmpty" - is UserContactLinkSubscribed -> "userContactLinkSubscribed" is NewChatItems -> "newChatItems" is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated" is ChatItemUpdated -> "chatItemUpdated" @@ -6005,11 +5996,9 @@ sealed class CR { 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" @@ -6070,8 +6059,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" @@ -6109,16 +6096,14 @@ 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 SentConfirmation -> withUser(user, json.encodeToString(connection)) is SentInvitation -> withUser(user, json.encodeToString(connection)) is SentInvitationToContact -> withUser(user, json.encodeToString(contact)) @@ -6135,7 +6120,7 @@ sealed class CR { is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}") is UserContactLink -> withUser(user, contactLink.responseDetails) is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails) - is UserContactLinkCreated -> withUser(user, connReqContact) + is UserContactLinkCreated -> withUser(user, json.encodeToString(connLinkContact)) is UserContactLinkDeleted -> withUser(user, noDetails()) is ContactConnected -> withUser(user, json.encodeToString(contact)) is ContactConnecting -> withUser(user, json.encodeToString(contact)) @@ -6150,10 +6135,6 @@ sealed class CR { is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions)) is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections" is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses)) - is GroupSubscribed -> withUser(user, json.encodeToString(group)) - is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors)) - is GroupEmpty -> withUser(user, json.encodeToString(group)) - is UserContactLinkSubscribed -> noDetails() is 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)) @@ -6168,7 +6149,7 @@ 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 UserDeletedMembers -> withUser(user, "groupInfo: $groupInfo\nmembers: $members") + 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") @@ -6178,19 +6159,17 @@ sealed class CR { is MembersRoleUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\ntoRole: $toRole") is MemberBlockedForAll -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nblocked: $blocked") is MembersBlockedForAllUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\nblocked: $blocked") - is DeletedMemberUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member") - is DeletedMember -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember") + is DeletedMemberUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nwithMessages: ${withMessages}") + is DeletedMember -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember\nwithMessages: ${withMessages}") is LeftMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is GroupDeleted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is ContactsMerged -> withUser(user, "intoContact: $intoContact\nmergedContact: $mergedContact") - is GroupInvitation -> withUser(user, json.encodeToString(groupInfo)) is UserJoinedGroup -> withUser(user, json.encodeToString(groupInfo)) is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact") - is GroupRemoved -> withUser(user, json.encodeToString(groupInfo)) is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) - is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") - is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") + is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole") + is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole") is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") @@ -6265,8 +6244,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) @@ -6279,13 +6256,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 { @@ -6300,11 +6273,34 @@ 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) + + companion object { + val nullableStateSaver: Saver> = Saver( + save = { link -> link?.connFullLink to link?.connShortLink }, + restore = { saved -> + val connFullLink = saved.first + if (connFullLink == null) null + else CreatedConnLink(connFullLink = connFullLink, connShortLink = saved.second) + } + ) + } +} + +fun simplexChatLink(uri: String): String = + if (uri.startsWith("simplex:/")) uri.replace("simplex:/", "https://simplex.chat/") + else uri + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @Serializable @SerialName("contactAddress") class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan() @Serializable @SerialName("groupLink") class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan() + @Serializable @SerialName("error") class Error(val chatError: ChatError): ConnectionPlan() } @Serializable @@ -6347,7 +6343,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 } @@ -6355,11 +6351,11 @@ abstract class TerminalItem { companion object { val sampleData = listOf( Cmd(0, null, CC.ShowActiveUser()), - Resp(1, null, CR.ActiveUser(User.sampleData)) + Resp(1, null, API.Result(null, CR.ActiveUser(User.sampleData))) ) fun cmd(rhId: Long?, c: CC) = Cmd(System.currentTimeMillis(), rhId, c) - fun resp(rhId: Long?, r: CR) = Resp(System.currentTimeMillis(), rhId, r) + fun resp(rhId: Long?, r: API) = Resp(System.currentTimeMillis(), rhId, r) } } @@ -6437,8 +6433,8 @@ enum class RatchetSyncState { } @Serializable -class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) { - val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}" +class UserContactLinkRec(val connLinkContact: CreatedConnLink, val autoAccept: AutoAccept? = null) { + val responseDetails: String get() = "connLinkContact: ${connLinkContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}" } @Serializable @@ -6503,6 +6499,16 @@ sealed class ChatError { @Serializable @SerialName("errorRemoteHost") class ChatErrorRemoteHost(val remoteHostError: RemoteHostError): ChatError() @Serializable @SerialName("errorRemoteCtrl") class ChatErrorRemoteCtrl(val remoteCtrlError: RemoteCtrlError): ChatError() @Serializable @SerialName("invalidJSON") class ChatErrorInvalidJSON(val json: String): ChatError() + + val resultType: String get() = when (this) { + is ChatErrorChat -> "chat" + is ChatErrorAgent -> "agent" + is ChatErrorStore -> "store" + is ChatErrorDatabase -> "database" + is ChatErrorRemoteHost -> "remoteHost" + is ChatErrorRemoteCtrl -> "remoteCtrl" + is ChatErrorInvalidJSON -> "invalid json" + } } @Serializable @@ -6530,6 +6536,7 @@ sealed class ChatErrorType { is ChatStoreChanged -> "chatStoreChanged" is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" + is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" is ContactNotReady -> "contactNotReady" is ContactNotActive -> "contactNotActive" @@ -6608,6 +6615,7 @@ sealed class ChatErrorType { @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() + @Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType() @Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType() @@ -6670,63 +6678,79 @@ sealed class StoreError { val string: String get() = when (this) { is DuplicateName -> "duplicateName" - is UserNotFound -> "userNotFound" - is UserNotFoundByName -> "userNotFoundByName" - is UserNotFoundByContactId -> "userNotFoundByContactId" - is UserNotFoundByGroupId -> "userNotFoundByGroupId" - is UserNotFoundByFileId -> "userNotFoundByFileId" - is UserNotFoundByContactRequestId -> "userNotFoundByContactRequestId" - is ContactNotFound -> "contactNotFound" - is ContactNotFoundByName -> "contactNotFoundByName" - is ContactNotFoundByMemberId -> "contactNotFoundByMemberId" - is ContactNotReady -> "contactNotReady" + is UserNotFound -> "userNotFound $userId" + is UserNotFoundByName -> "userNotFoundByName $contactName" + is UserNotFoundByContactId -> "userNotFoundByContactId $contactId" + is UserNotFoundByGroupId -> "userNotFoundByGroupId $groupId" + is UserNotFoundByFileId -> "userNotFoundByFileId $fileId" + is UserNotFoundByContactRequestId -> "userNotFoundByContactRequestId $contactRequestId" + is ContactNotFound -> "contactNotFound $contactId" + is ContactNotFoundByName -> "contactNotFoundByName $contactName" + is ContactNotFoundByMemberId -> "contactNotFoundByMemberId $groupMemberId" + is ContactNotReady -> "contactNotReady $contactName" is DuplicateContactLink -> "duplicateContactLink" is UserContactLinkNotFound -> "userContactLinkNotFound" - is ContactRequestNotFound -> "contactRequestNotFound" - is ContactRequestNotFoundByName -> "contactRequestNotFoundByName" - is GroupNotFound -> "groupNotFound" - is GroupNotFoundByName -> "groupNotFoundByName" - is GroupMemberNameNotFound -> "groupMemberNameNotFound" - is GroupMemberNotFound -> "groupMemberNotFound" - is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId" - is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound" + is ContactRequestNotFound -> "contactRequestNotFound $contactRequestId" + is ContactRequestNotFoundByName -> "contactRequestNotFoundByName $contactName" + is GroupNotFound -> "groupNotFound $groupId" + is GroupNotFoundByName -> "groupNotFoundByName $groupName" + is GroupMemberNameNotFound -> "groupMemberNameNotFound $groupId $groupMemberName" + is GroupMemberNotFound -> "groupMemberNotFound $groupMemberId" + is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId $memberId" + is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound $contactId" is GroupWithoutUser -> "groupWithoutUser" is DuplicateGroupMember -> "duplicateGroupMember" is GroupAlreadyJoined -> "groupAlreadyJoined" is GroupInvitationNotFound -> "groupInvitationNotFound" - is SndFileNotFound -> "sndFileNotFound" - is SndFileInvalid -> "sndFileInvalid" - is RcvFileNotFound -> "rcvFileNotFound" - is RcvFileDescrNotFound -> "rcvFileDescrNotFound" - is FileNotFound -> "fileNotFound" - is RcvFileInvalid -> "rcvFileInvalid" + is NoteFolderAlreadyExists -> "noteFolderAlreadyExists $noteFolderId" + is NoteFolderNotFound -> "noteFolderNotFound $noteFolderId" + is UserNoteFolderNotFound -> "userNoteFolderNotFound" + is SndFileNotFound -> "sndFileNotFound $fileId" + is SndFileInvalid -> "sndFileInvalid $fileId" + is RcvFileNotFound -> "rcvFileNotFound $fileId" + is RcvFileDescrNotFound -> "rcvFileDescrNotFound $fileId" + is FileNotFound -> "fileNotFound $fileId" + is RcvFileInvalid -> "rcvFileInvalid $fileId" is RcvFileInvalidDescrPart -> "rcvFileInvalidDescrPart" - is SharedMsgIdNotFoundByFileId -> "sharedMsgIdNotFoundByFileId" - is FileIdNotFoundBySharedMsgId -> "fileIdNotFoundBySharedMsgId" - is SndFileNotFoundXFTP -> "sndFileNotFoundXFTP" - is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP" - is ExtraFileDescrNotFoundXFTP -> "extraFileDescrNotFoundXFTP" - is ConnectionNotFound -> "connectionNotFound" - is ConnectionNotFoundById -> "connectionNotFoundById" - is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId" - is PendingConnectionNotFound -> "pendingConnectionNotFound" + is LocalFileNoTransfer -> "localFileNoTransfer $fileId" + is SharedMsgIdNotFoundByFileId -> "sharedMsgIdNotFoundByFileId $fileId" + is FileIdNotFoundBySharedMsgId -> "fileIdNotFoundBySharedMsgId $sharedMsgId" + is SndFileNotFoundXFTP -> "sndFileNotFoundXFTP $agentSndFileId" + is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP $agentRcvFileId" + is ConnectionNotFound -> "connectionNotFound $agentConnId" + is ConnectionNotFoundById -> "connectionNotFoundById $connId" + is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId $groupMemberId" + is PendingConnectionNotFound -> "pendingConnectionNotFound $connId" is IntroNotFound -> "introNotFound" is UniqueID -> "uniqueID" - is InternalError -> "internalError" - is NoMsgDelivery -> "noMsgDelivery" - is BadChatItem -> "badChatItem" - is ChatItemNotFound -> "chatItemNotFound" - is ChatItemNotFoundByText -> "chatItemNotFoundByText" - is ChatItemSharedMsgIdNotFound -> "chatItemSharedMsgIdNotFound" - is ChatItemNotFoundByFileId -> "chatItemNotFoundByFileId" - is ChatItemNotFoundByGroupId -> "chatItemNotFoundByGroupId" - is ProfileNotFound -> "profileNotFound" - is DuplicateGroupLink -> "duplicateGroupLink" - is GroupLinkNotFound -> "groupLinkNotFound" - is HostMemberIdNotFound -> "hostMemberIdNotFound" - is ContactNotFoundByFileId -> "contactNotFoundByFileId" - is NoGroupSndStatus -> "noGroupSndStatus" is LargeMsg -> "largeMsg" + is InternalError -> "internalError $message" + is DBException -> "dBException $message" + is DBBusyError -> "dBBusyError $message" + is BadChatItem -> "badChatItem $itemId" + is ChatItemNotFound -> "chatItemNotFound $itemId" + is ChatItemNotFoundByText -> "chatItemNotFoundByText $text" + is ChatItemSharedMsgIdNotFound -> "chatItemSharedMsgIdNotFound $sharedMsgId" + is ChatItemNotFoundByFileId -> "chatItemNotFoundByFileId $fileId" + is ChatItemNotFoundByContactId -> "chatItemNotFoundByContactId $contactId" + is ChatItemNotFoundByGroupId -> "chatItemNotFoundByGroupId $groupId" + is ProfileNotFound -> "profileNotFound $profileId" + is DuplicateGroupLink -> "duplicateGroupLink ${groupInfo.groupId}" + is GroupLinkNotFound -> "groupLinkNotFound ${groupInfo.groupId}" + is HostMemberIdNotFound -> "hostMemberIdNotFound $groupId" + is ContactNotFoundByFileId -> "contactNotFoundByFileId $fileId" + is NoGroupSndStatus -> "noGroupSndStatus $itemId $groupMemberId" + is DuplicateGroupMessage -> "duplicateGroupMessage $groupId $sharedMsgId $authorGroupMemberId $authorGroupMemberId" + is RemoteHostNotFound -> "remoteHostNotFound $remoteHostId" + is RemoteHostUnknown -> "remoteHostUnknown" + is RemoteHostDuplicateCA -> "remoteHostDuplicateCA" + is RemoteCtrlNotFound -> "remoteCtrlNotFound $remoteCtrlId" + is RemoteCtrlDuplicateCA -> "remoteCtrlDuplicateCA" + is ProhibitedDeleteUser -> "prohibitedDeleteUser $userId $contactId" + is OperatorNotFound -> "operatorNotFound $serverOperatorId" + is UsageConditionsNotFound -> "usageConditionsNotFound" + is InvalidQuote -> "invalidQuote" + is InvalidMention -> "invalidMention" } @Serializable @SerialName("duplicateName") object DuplicateName: StoreError() @@ -6754,6 +6778,9 @@ sealed class StoreError { @Serializable @SerialName("duplicateGroupMember") object DuplicateGroupMember: StoreError() @Serializable @SerialName("groupAlreadyJoined") object GroupAlreadyJoined: StoreError() @Serializable @SerialName("groupInvitationNotFound") object GroupInvitationNotFound: StoreError() + @Serializable @SerialName("noteFolderAlreadyExists") class NoteFolderAlreadyExists(val noteFolderId: Long): StoreError() + @Serializable @SerialName("noteFolderNotFound") class NoteFolderNotFound(val noteFolderId: Long): StoreError() + @Serializable @SerialName("userNoteFolderNotFound") object UserNoteFolderNotFound: StoreError() @Serializable @SerialName("sndFileNotFound") class SndFileNotFound(val fileId: Long): StoreError() @Serializable @SerialName("sndFileInvalid") class SndFileInvalid(val fileId: Long): StoreError() @Serializable @SerialName("rcvFileNotFound") class RcvFileNotFound(val fileId: Long): StoreError() @@ -6761,24 +6788,27 @@ sealed class StoreError { @Serializable @SerialName("fileNotFound") class FileNotFound(val fileId: Long): StoreError() @Serializable @SerialName("rcvFileInvalid") class RcvFileInvalid(val fileId: Long): StoreError() @Serializable @SerialName("rcvFileInvalidDescrPart") object RcvFileInvalidDescrPart: StoreError() + @Serializable @SerialName("localFileNoTransfer") class LocalFileNoTransfer(val fileId: Long): StoreError() @Serializable @SerialName("sharedMsgIdNotFoundByFileId") class SharedMsgIdNotFoundByFileId(val fileId: Long): StoreError() @Serializable @SerialName("fileIdNotFoundBySharedMsgId") class FileIdNotFoundBySharedMsgId(val sharedMsgId: String): StoreError() @Serializable @SerialName("sndFileNotFoundXFTP") class SndFileNotFoundXFTP(val agentSndFileId: String): StoreError() @Serializable @SerialName("rcvFileNotFoundXFTP") class RcvFileNotFoundXFTP(val agentRcvFileId: String): StoreError() - @Serializable @SerialName("extraFileDescrNotFoundXFTP") class ExtraFileDescrNotFoundXFTP(val fileId: Long): StoreError() @Serializable @SerialName("connectionNotFound") class ConnectionNotFound(val agentConnId: String): StoreError() @Serializable @SerialName("connectionNotFoundById") class ConnectionNotFoundById(val connId: Long): StoreError() @Serializable @SerialName("connectionNotFoundByMemberId") class ConnectionNotFoundByMemberId(val groupMemberId: Long): StoreError() @Serializable @SerialName("pendingConnectionNotFound") class PendingConnectionNotFound(val connId: Long): StoreError() @Serializable @SerialName("introNotFound") object IntroNotFound: StoreError() @Serializable @SerialName("uniqueID") object UniqueID: StoreError() + @Serializable @SerialName("largeMsg") object LargeMsg: StoreError() @Serializable @SerialName("internalError") class InternalError(val message: String): StoreError() - @Serializable @SerialName("noMsgDelivery") class NoMsgDelivery(val connId: Long, val agentMsgId: String): StoreError() + @Serializable @SerialName("dBException") class DBException(val message: String): StoreError() + @Serializable @SerialName("dBBusyError") class DBBusyError(val message: String): StoreError() @Serializable @SerialName("badChatItem") class BadChatItem(val itemId: Long): StoreError() @Serializable @SerialName("chatItemNotFound") class ChatItemNotFound(val itemId: Long): StoreError() @Serializable @SerialName("chatItemNotFoundByText") class ChatItemNotFoundByText(val text: String): StoreError() @Serializable @SerialName("chatItemSharedMsgIdNotFound") class ChatItemSharedMsgIdNotFound(val sharedMsgId: String): StoreError() @Serializable @SerialName("chatItemNotFoundByFileId") class ChatItemNotFoundByFileId(val fileId: Long): StoreError() + @Serializable @SerialName("chatItemNotFoundByContactId") class ChatItemNotFoundByContactId(val contactId: Long): StoreError() @Serializable @SerialName("chatItemNotFoundByGroupId") class ChatItemNotFoundByGroupId(val groupId: Long): StoreError() @Serializable @SerialName("profileNotFound") class ProfileNotFound(val profileId: Long): StoreError() @Serializable @SerialName("duplicateGroupLink") class DuplicateGroupLink(val groupInfo: GroupInfo): StoreError() @@ -6786,7 +6816,17 @@ sealed class StoreError { @Serializable @SerialName("hostMemberIdNotFound") class HostMemberIdNotFound(val groupId: Long): StoreError() @Serializable @SerialName("contactNotFoundByFileId") class ContactNotFoundByFileId(val fileId: Long): StoreError() @Serializable @SerialName("noGroupSndStatus") class NoGroupSndStatus(val itemId: Long, val groupMemberId: Long): StoreError() - @Serializable @SerialName("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 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 276d3e3909..a9f2dcaffc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -79,7 +79,11 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } if (rerunMigration) { chatModel.dbMigrationInProgress.value = true - migrated = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) + migrated = if (databaseBackend == "postgres") { + chatMigrateInit("simplex_v1", "postgresql://simplex@/simplex_v1", confirm.value) + } else { + chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) + } res = runCatching { json.decodeFromString(migrated[0] as String) }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } 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 5efd3747a3..5b9e63963c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -73,7 +73,7 @@ abstract class NtfManager { } val cInfo = chatModel.getChat(chatId)?.chatInfo chatModel.clearOverlays.value = true - if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(null, cInfo) + if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(secondaryChatsCtx = null, rhId = null, cInfo) } } 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 1daf5a7ba7..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 @@ -12,10 +12,10 @@ 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: (ComposeMessage) -> Unit, 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 22b013ff60..37aa7fc1d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -48,7 +48,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState 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 a730bd1b71..2a77d0a6dc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -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.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?, @@ -99,7 +98,7 @@ 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, @@ -126,8 +125,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 +139,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 +170,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 +199,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 +474,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) { @@ -1270,11 +1269,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))) } } } @@ -1283,8 +1282,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) } } } @@ -1334,6 +1333,7 @@ fun queueInfoText(info: Pair): String { } fun setChatTTLAlert( + chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, selectedChatTTL: MutableState, @@ -1353,7 +1353,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, @@ -1361,6 +1361,7 @@ fun setChatTTLAlert( } private fun setChatTTL( + chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, chatTTL: MutableState, @@ -1371,33 +1372,33 @@ 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 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, openAroundItemId = null ) 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 c13eb8f139..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 @@ -209,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 9050804db4..eabe9cb60a 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,74 +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.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.contentTag, pagination, search) ?: return@coroutineScope // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes /** When [openAroundItemId] is provided, chatId can be different too */ if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null) || !isActive) return@coroutineScope - processLoadedChat(chat, navInfo, contentTag, pagination, openAroundItemId, 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.contentTag == null) { // update main chats, not content tagged - withChats { - val oldChat = getChat(chat.id) + withContext(Dispatchers.Main) { + val oldChat = chatModel.chatsContext.getChat(chat.id) if (oldChat == null) { - addChat(chat) + chatModel.chatsContext.addChat(chat) } else { - updateChatInfo(chat.remoteHostId, chat.chatInfo) + chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo) // unreadChat is currently not actual in getChat query (always false) - updateChatStats(chat.remoteHostId, chat.id, chat.chatStats.copy(unreadChat = oldChat.chatStats.unreadChat)) + chatModel.chatsContext.updateChatStats(chat.remoteHostId, chat.id, chat.chatStats.copy(unreadChat = oldChat.chatStats.unreadChat)) } } } - withChats(contentTag) { - chatItemStatuses.clear() - chatItems.replaceAll(chat.chatItems) + withContext(Dispatchers.Main) { + chatsCtx.chatItemStatuses.clear() + chatsCtx.chatItems.replaceAll(chat.chatItems) chatModel.chatId.value = chat.id splits.value = newSplits if (chat.chatItems.isNotEmpty()) { @@ -92,8 +92,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) } @@ -111,8 +111,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 @@ -122,18 +122,20 @@ suspend fun processLoadedChat( } } is ChatPagination.Around -> { - val newSplits = if (openAroundItemId == null) { + val newSplits: ArrayList = if (openAroundItemId == null) { newItems.addAll(oldItems) - removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed) + ArrayList(removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed)) } else { - emptyList() + arrayListOf() } - // currently, items will always be added on top, which is index 0 - newItems.addAll(0, chat.chatItems) + 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) - withChats(contentTag) { - chatItems.replaceAll(newItems) - splits.value = listOf(chat.chatItems.last().id) + newSplits + 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 @@ -151,10 +153,12 @@ suspend fun processLoadedChat( } 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 } } @@ -240,7 +244,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) { @@ -259,8 +271,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) @@ -323,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 @@ -343,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 f9a7f25f48..6d7cdcdebe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -32,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.* @@ -59,8 +57,8 @@ data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val dat // 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, - contentTag: MsgContentTag?, scrollToItemId: MutableState = remember { mutableStateOf(null) }, onComposed: suspend (chatId: String) -> Unit ) { @@ -101,7 +99,7 @@ fun ChatView( .distinctUntilChanged() .filterNotNull() .collect { chatId -> - if (contentTag == null) { + if (chatsCtx.contentTag == null) { markUnreadChatAsRead(chatId) } showSearch.value = false @@ -116,13 +114,12 @@ fun ChatView( // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { derivedStateOf { - chatModel.chatsForContent(contentTag).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 contentTag ) { when (chatInfo) { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { @@ -136,15 +133,16 @@ fun ChatView( 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 && contentTag == null + val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.contentTag == null val c = chatModel.getChat(chatInfo.id) if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged withBGApi { - apiFindMessages(c, value, contentTag) + apiFindMessages(chatsCtx, c, value) searchText.value = value } } ChatLayout( + chatsCtx = chatsCtx, remoteHostId = remoteHostId, chatInfo = activeChatInfo, unreadCount, @@ -175,8 +173,8 @@ fun ChatView( ) } } else { - SelectedItemsBottomToolbar( - contentTag = contentTag, + SelectedItemsButtonsToolbar( + chatsCtx = chatsCtx, selectedChatItems = selectedChatItems, chatInfo = chatInfo, deleteItems = { canDeleteForAll -> @@ -264,7 +262,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: Pair? = null if (chatInfo is ChatInfo.Direct) { preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second @@ -274,34 +272,46 @@ 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 { activeChatInfo }.value + if (chatInfo is ChatInfo.Direct) { + var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } + var code: String? by remember { mutableStateOf(preloadedCode) } + KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) { + contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + preloadedContactInfo = contactInfo + code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second + preloadedCode = code + } + ChatInfoView(chatsCtx, chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) { + showSearch.value = true + } + } else if (chatInfo is ChatInfo.Group) { + var link: Pair? by remember(chatInfo.id) { mutableStateOf(preloadedLink) } + KeyChangeEffect(chatInfo.id) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) + preloadedLink = link + } + GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link?.first, link?.second, selectedItems, appBar, scrollToItemId, { + link = it + preloadedLink = it + }, close, { showSearch.value = true }) + } else { + LaunchedEffect(Unit) { + close() + } } - 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 { activeChatInfo.value?.id } + .drop(1) + .collect { + appBar.value = null + selectedItems.value = null + } } } } @@ -333,7 +343,7 @@ fun ChatView( setGroupMembers(chatRh, groupInfo, chatModel) if (!isActive) return@launch - if (contentTag == null) { + if (chatsCtx.contentTag == null) { ModalManager.end.closeModals() } ModalManager.end.showModalCloseable(true) { close -> @@ -347,12 +357,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, contentTag, pagination, searchText.value, null, visibleItemIndexes) + apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, searchText.value, null, visibleItemIndexes) } }, deleteMessage = { itemId, mode -> withBGApi { - val toDeleteItem = reversedChatItemsStatic(contentTag).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 @@ -377,23 +387,23 @@ 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) } } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { if (deletedChatItem.isReport) { if (toChatItem != null) { - upsertChatItem(chatRh, chatInfo, toChatItem) + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) } else { - removeChatItem(chatRh, chatInfo, deletedChatItem) + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem) } } } @@ -451,8 +461,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) } } } @@ -463,8 +473,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) } } } @@ -474,8 +484,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) } } } @@ -484,8 +494,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) } } } @@ -507,12 +517,12 @@ fun ChatView( reaction = reaction ) if (updatedCI != null) { - withChats { - updateChatItem(cInfo, updatedCI) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatItem(cInfo, updatedCI) } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { if (cItem.isReport) { - updateChatItem(cInfo, updatedCI) + chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, updatedCI) } } } @@ -532,7 +542,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 = { @@ -544,11 +554,11 @@ fun ChatView( 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 @@ -566,11 +576,8 @@ 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( chatRh, @@ -579,18 +586,15 @@ fun ChatView( itemsIds ) } - 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, @@ -598,8 +602,8 @@ fun ChatView( chatInfo.apiId ) } - withReportsChatsIfOpen { - markChatItemsRead(chatRh, chatInfo.id) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.markChatItemsRead(chatRh, chatInfo.id) } } }, @@ -619,13 +623,13 @@ fun ChatView( 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() } } } @@ -637,8 +641,8 @@ fun ChatView( LaunchedEffect(chatInfo.id) { onComposed(chatInfo.id) ModalManager.end.closeModals() - withChats { - chatItems.clearAndNotify() + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() } } } @@ -663,6 +667,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) @Composable fun ChatLayout( + chatsCtx: ChatModel.ChatsContext, remoteHostId: State, chatInfo: State, unreadCount: State, @@ -718,7 +723,7 @@ fun ChatLayout( 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 = { @@ -740,8 +745,7 @@ fun ChatLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { val composeViewHeight = remember { mutableStateOf(0.dp) } - val contentTag = LocalContentTag.current - Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, contentTag == null)) { + Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.contentTag == null)) { val remoteHostId = remember { remoteHostId }.value val chatInfo = remember { chatInfo }.value val oneHandUI = remember { appPrefs.oneHandUI.state } @@ -755,7 +759,7 @@ fun ChatLayout( override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f }) { ChatItemsList( - remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, + chatsCtx, remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, archiveReports, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, @@ -778,7 +782,7 @@ fun ChatLayout( } } } - if (contentTag == MsgContentTag.Report) { + if (chatsCtx.contentTag == MsgContentTag.Report) { Column( Modifier .layoutId(CHAT_COMPOSE_LAYOUT_ID) @@ -788,8 +792,8 @@ fun ChatLayout( ) { AnimatedVisibility(selectedChatItems.value != null) { if (chatInfo != null) { - SelectedItemsBottomToolbar( - contentTag = contentTag, + SelectedItemsButtonsToolbar( + chatsCtx = chatsCtx, selectedChatItems = selectedChatItems, chatInfo = chatInfo, deleteItems = { _ -> @@ -829,7 +833,7 @@ fun ChatLayout( } val reportsCount = reportsCount(chatInfo?.id) if (oneHandUI.value && chatBottomBar.value) { - if (contentTag == null && reportsCount > 0) { + if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate && chatsCtx.contentTag == null && reportsCount > 0) { ReportedCountToolbar(reportsCount, withStatusBar = true, showGroupReports) } else { StatusBarBackground() @@ -837,16 +841,16 @@ fun ChatLayout( } else { NavigationBarBackground(true, oneHandUI.value, noAlpha = true) } - if (contentTag == MsgContentTag.Report) { + if (chatsCtx.contentTag == MsgContentTag.Report) { if (oneHandUI.value) { StatusBarBackground() } Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { Box { if (selectedChatItems.value == null) { - GroupReportsAppBar(contentTag, { ModalManager.end.closeModal() }, onSearchValueChanged) + GroupReportsAppBar(chatsCtx, { ModalManager.end.closeModal() }, onSearchValueChanged) } else { - SelectedItemsTopToolbar(selectedChatItems, !oneHandUI.value) + SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value) } } } @@ -855,13 +859,13 @@ fun ChatLayout( Box { if (selectedChatItems.value == null) { if (chatInfo != null) { - ChatInfoToolbar(chatInfo, contentTag, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) } } else { - SelectedItemsTopToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value) + SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value) } } - if (contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) { + if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate && chatsCtx.contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) { ReportedCountToolbar(reportsCount, withStatusBar = false, showGroupReports) } } @@ -873,8 +877,8 @@ fun ChatLayout( @Composable fun BoxScope.ChatInfoToolbar( + chatsCtx: ChatModel.ChatsContext, chatInfo: ChatInfo, - contentTag: MsgContentTag?, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit, @@ -896,7 +900,7 @@ fun BoxScope.ChatInfoToolbar( showSearch.value = false } } - if (appPlatform.isAndroid && contentTag == null) { + if (appPlatform.isAndroid && chatsCtx.contentTag == null) { BackHandler(onBack = onBackClicked) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() @@ -1135,6 +1139,7 @@ private var reportsListState: LazyListState? = null @Composable fun BoxScope.ChatItemsList( + chatsCtx: ChatModel.ChatsContext, remoteHostId: Long?, chatInfo: ChatInfo, unreadCount: State, @@ -1173,108 +1178,126 @@ fun BoxScope.ChatItemsList( developerTools: Boolean, showViaProxy: Boolean ) { + 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 contentTag = LocalContentTag.current // 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(chatModel.chatItemsForContent(contentTag).value.asReversed(), unreadCount, revealedItems.value, chatModel.chatStateForContent(contentTag)) + if (chatsCtx != null) { + val mergedItems = remember { + derivedStateOf { + MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState) + } } - } - val reversedChatItems = remember { derivedStateOf { chatModel.chatItemsForContent(contentTag).value.asReversed() } } - val reportsCount = reportsCount(chatInfo.id) - val topPaddingToContent = topPaddingToContent(chatView = contentTag == null, contentTag == null && reportsCount > 0) - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() }) - val numberOfBottomAppBars = numberOfBottomAppBars() - /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of - * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears - * */ - val maxHeightForList = rememberUpdatedState( - with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() } - ) - val resetListState = remember { mutableStateOf(false) } - remember(chatModel.openAroundItemId.value) { - if (chatModel.openAroundItemId.value != null) { - closeSearch() - resetListState.value = !resetListState.value + val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } } + val reportsCount = reportsCount(chatInfo.id) + val topPaddingToContent = topPaddingToContent( + chatView = chatsCtx.contentTag == null, + additionalTopBar = chatsCtx.contentTag == null && reportsCount > 0 + ) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() }) + val numberOfBottomAppBars = numberOfBottomAppBars() + + /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of + * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears + * */ + val maxHeightForList = rememberUpdatedState( + with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() } + ) + val resetListState = remember { mutableStateOf(false) } + remember(chatModel.openAroundItemId.value) { + if (chatModel.openAroundItemId.value != null) { + closeSearch() + resetListState.value = !resetListState.value + } } - } - val highlightedItems = remember { mutableStateOf(setOf()) } - val 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 + val highlightedItems = remember { mutableStateOf(setOf()) } + val hoveredItemId = remember { mutableStateOf(null as Long?) } + val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) { + val openAroundItemId = chatModel.openAroundItemId.value + val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() } + val reportsState = reportsListState + if (openAroundItemId != null) { + highlightedItems.value += openAroundItemId + chatModel.openAroundItemId.value = null + } + hoveredItemId.value = null + if (reportsState != null) { + reportsListState = null + reportsState + } else if (index <= 0 || !searchValueIsEmpty.value) { + LazyListState(0, 0) + } else { + LazyListState(index + 1, -maxHeightForList.value) + } + }) + SaveReportsStateOnDispose(chatsCtx, listState) + val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } + val loadingMoreItems = remember { mutableStateOf(false) } + val animatedScrollingInProgress = remember { mutableStateOf(false) } + val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() } + LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) { + if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT) + ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect) } - if (reportsState != null) { - reportsListState = null - reportsState - } else if (index <= 0) { - LazyListState(0, 0) - } else { - LazyListState(index + 1, -maxHeightForList.value) - } - }) - SaveReportsStateOnDispose(listState) - val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } - val loadingMoreItems = remember { mutableStateOf(false) } - val animatedScrollingInProgress = remember { mutableStateOf(false) } - val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() } - LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) { - if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT) - ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect) - } - if (!loadingMoreItems.value) { - PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), contentTag, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> - if (loadingMoreItems.value) return@PreloadItems false - try { - loadingMoreItems.value = true - loadMessages(chatId, pagination) { - visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value) + PreloadItems(chatsCtx, chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), loadingMoreItems, resetListState, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> + if (loadingMoreItems.value || chatId != chatModel.chatId.value) return@PreloadItems false + loadingMoreItems.value = true + withContext(NonCancellable) { + try { + loadMessages(chatId, pagination) { + visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value) + } + } finally { + loadingMoreItems.value = false } - } finally { - loadingMoreItems.value = false } true } - } - - val remoteHostIdUpdated = rememberUpdatedState(remoteHostId) - val chatInfoUpdated = rememberUpdatedState(chatInfo) - val scope = rememberCoroutineScope() - val scrollToItem: (Long) -> Unit = remember { - // In group reports just set the itemId to scroll to so the main ChatView will handle scrolling - if (contentTag == MsgContentTag.Report) return@remember { scrollToItemId.value = it } - scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) - } - val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem, contentTag) } - if (contentTag == null) { - LaunchedEffect(Unit) { snapshotFlow { scrollToItemId.value }.filterNotNull().collect { - if (appPlatform.isAndroid) { - ModalManager.end.closeModals() + val remoteHostIdUpdated = rememberUpdatedState(remoteHostId) + val chatInfoUpdated = rememberUpdatedState(chatInfo) + val scope = rememberCoroutineScope() + val scrollToItem: (Long) -> Unit = remember { + // In group reports just set the itemId to scroll to so the main ChatView will handle scrolling + if (chatsCtx.contentTag == MsgContentTag.Report) return@remember { scrollToItemId.value = it } + scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) + } + val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(chatsCtx, remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) } + if (chatsCtx.contentTag == null) { + LaunchedEffect(Unit) { + snapshotFlow { scrollToItemId.value }.filterNotNull().collect { + if (appPlatform.isAndroid) { + ModalManager.end.closeModals() + } + scrollToItem(it) + scrollToItemId.value = null + } } - scrollToItem(it) - scrollToItemId.value = null } } - } - LoadLastItems(loadingMoreItems, resetListState, remoteHostId, chatInfo) - SmallScrollOnNewMessage(listState, reversedChatItems) - val finishedInitialComposition = remember { mutableStateOf(false) } - NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) + SmallScrollOnNewMessage(listState, reversedChatItems) + val finishedInitialComposition = remember { mutableStateOf(false) } + NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) - DisposableEffectOnGone( - always = { - chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(contentTag)), contentTag) - }, - whenGone = { - VideoPlayerHolder.releaseAll() - chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(contentTag)), contentTag) - } - ) + DisposableEffectOnGone( + whenGone = { + VideoPlayerHolder.releaseAll() + } + ) @Composable fun ChatViewListItem( @@ -1319,7 +1342,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1432,7 +1455,7 @@ fun BoxScope.ChatItemsList( fun Item() { ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) { androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { - SelectedChatItem(Modifier, cItem.id, selectedChatItems) + SelectedListItem(Modifier, cItem.id, selectedChatItems) } Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) { val member = cItem.chatDir.groupMember @@ -1456,8 +1479,8 @@ fun BoxScope.ChatItemsList( } } 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) } Row( Modifier @@ -1471,8 +1494,8 @@ fun BoxScope.ChatItemsList( } } 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 @@ -1486,8 +1509,8 @@ fun BoxScope.ChatItemsList( } } else { // direct message 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( @@ -1516,96 +1539,116 @@ fun BoxScope.ChatItemsList( ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap) } } - LazyColumnWithScrollBar( - Modifier.align(Alignment.BottomCenter), - state = listState.value, - contentPadding = PaddingValues( - top = topPaddingToContent, - bottom = composeViewHeight.value - ), - reverseLayout = true, - additionalBarOffset = composeViewHeight, - additionalTopBar = rememberUpdatedState(contentTag == null && reportsCount > 0), - chatBottomBar = remember { appPrefs.chatBottomBar.state } - ) { - val mergedItemsValue = mergedItems.value - itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged -> - val isLastItem = index == mergedItemsValue.items.lastIndex - val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null - val listItem = merged.newest() - val item = listItem.item - val range = if (merged is MergedItem.Grouped) { - merged.rangeInReversed.value - } else { - 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 } + LazyColumnWithScrollBar( + Modifier.align(Alignment.BottomCenter), + state = listState.value, + contentPadding = PaddingValues( + top = topPaddingToContent, + bottom = composeViewHeight.value + ), + reverseLayout = true, + additionalBarOffset = composeViewHeight, + additionalTopBar = rememberUpdatedState(chatsCtx.contentTag == null && reportsCount > 0), + chatBottomBar = remember { appPrefs.chatBottomBar.state } + ) { + val mergedItemsValue = mergedItems.value + itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged -> + val isLastItem = index == mergedItemsValue.items.lastIndex + val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null + val listItem = merged.newest() + val item = listItem.item + val range = if (merged is MergedItem.Grouped) { + merged.rangeInReversed.value + } else { + 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 } + } + MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead) } - MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead) } } - } - FloatingButtons(topPaddingToContent, topPaddingToContentPx, loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState) - FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState) + FloatingButtons( + chatsCtx, + reversedChatItems, + chatInfoUpdated, + topPaddingToContent, + topPaddingToContentPx, + loadingMoreItems, + loadingTopItems, + loadingBottomItems, + animatedScrollingInProgress, + mergedItems, + unreadCount, + maxHeight, + composeViewHeight, + searchValue, + markChatRead, + listState, + loadMessages + ) + FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState) - LaunchedEffect(Unit) { - snapshotFlow { listState.value.isScrollInProgress } - .collect { - chatViewScrollState.value = it - } - } - LaunchedEffect(Unit) { - snapshotFlow { listState.value.isScrollInProgress } - .filter { !it } - .collect { - if (animatedScrollingInProgress.value) { - animatedScrollingInProgress.value = false + LaunchedEffect(Unit) { + snapshotFlow { listState.value.isScrollInProgress } + .collect { + chatViewScrollState.value = it + } + } + LaunchedEffect(Unit) { + snapshotFlow { listState.value.isScrollInProgress } + .filter { !it } + .collect { + if (animatedScrollingInProgress.value) { + animatedScrollingInProgress.value = false + } } - } - } -} - -@Composable -private fun LoadLastItems(loadingMoreItems: MutableState, resetListState: State, remoteHostId: Long?, chatInfo: ChatInfo) { - val contentTag = LocalContentTag.current - LaunchedEffect(remoteHostId, chatInfo.id, resetListState.value) { - try { - loadingMoreItems.value = true - if (chatModel.chatStateForContent(contentTag).totalAfter.value <= 0) return@LaunchedEffect - delay(500) - withContext(Dispatchers.Default) { - apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, contentTag, 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() } @@ -1664,9 +1707,14 @@ 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, @@ -1674,9 +1722,44 @@ fun BoxScope.FloatingButtons( composeViewHeight: State, searchValue: State, markChatRead: () -> Unit, - listState: State + listState: State, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit ) { val scope = rememberCoroutineScope() + 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 @@ -1702,43 +1785,73 @@ 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).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( @@ -1759,9 +1872,11 @@ fun BoxScope.FloatingButtons( @Composable fun PreloadItems( + chatsCtx: ChatModel.ChatsContext, chatId: String, ignoreLoadingRequests: MutableSet, - contentTag: MsgContentTag?, + loadingMoreItems: State, + resetListState: State, mergedItems: State, listState: State, remaining: Int, @@ -1772,100 +1887,83 @@ fun PreloadItems( val chatId = rememberUpdatedState(chatId) val loadItems = rememberUpdatedState(loadItems) val ignoreLoadingRequests = rememberUpdatedState(ignoreLoadingRequests) - PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, contentTag, mergedItems, listState, remaining, loadItems) - PreloadItemsAfter(allowLoad, chatId, contentTag, mergedItems, listState, remaining, loadItems) -} - -@Composable -private fun PreloadItemsBefore( - allowLoad: State, - chatId: State, - ignoreLoadingRequests: State>, - contentTag: MsgContentTag?, - 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 = reversedChatItemsStatic(contentTag) - if (splits.isEmpty() && items.isNotEmpty() && lastVisibleIndex > mergedItems.value.items.size - remaining) { - lastIndexToLoadFrom = items.lastIndex - } - if (allowLoad.value && lastIndexToLoadFrom != null) { - items.getOrNull(lastIndexToLoadFrom)?.id - } else { - null - } - } - .filterNotNull() - .filter { !ignoreLoadingRequests.value.contains(it) } - .collect { loadFromItemId -> - withBGApi { - val items = reversedChatItemsStatic(contentTag) - val sizeWas = items.size - val oldestItemIdWas = items.lastOrNull()?.id - val triedToLoad = loadItems.value(chatId.value, ChatPagination.Before(loadFromItemId, ChatPagination.PRELOAD_COUNT)) - val itemsUpdated = reversedChatItemsStatic(contentTag) - if (triedToLoad && sizeWas == itemsUpdated.size && oldestItemIdWas == itemsUpdated.lastOrNull()?.id) { - ignoreLoadingRequests.value.add(loadFromItemId) - } - } - } - } -} - -@Composable -private fun PreloadItemsAfter( - allowLoad: MutableState, - chatId: State, - contentTag: MsgContentTag?, - 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 = reversedChatItemsStatic(contentTag) - 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 @@ -1877,6 +1975,7 @@ fun MemberImage(member: GroupMember) { private fun TopEndFloatingButton( modifier: Modifier = Modifier, unreadCount: State, + requestedTopScroll: State, animatedScrollingInProgress: State, onClick: () -> Unit, onLongClick: () -> Unit @@ -1890,11 +1989,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, + ) + } } } } @@ -2017,11 +2120,10 @@ private fun FloatingDate( } @Composable -private fun SaveReportsStateOnDispose(listState: State) { - val contentTag = LocalContentTag.current +private fun SaveReportsStateOnDispose(chatsCtx: ChatModel.ChatsContext, listState: State) { DisposableEffect(Unit) { onDispose { - reportsListState = if (contentTag == MsgContentTag.Report && 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 } } } @@ -2099,7 +2201,7 @@ private fun DateSeparator(date: Instant) { @Composable private fun MarkItemsReadAfterDelay( - itemKey: String, + itemKey: ChatViewItemKey, itemIds: List, finishedInitialComposition: State, chatId: ChatId, @@ -2132,23 +2234,25 @@ fun reportsCount(staleChatId: String?): Int { } } -private fun reversedChatItemsStatic(contentTag: MsgContentTag?): List = - chatModel.chatItemsForContent(contentTag).value.asReversed() +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 } @@ -2207,20 +2311,20 @@ private fun scrollToItem( } private fun findQuotedItemFromItem( + chatsCtx: ChatModel.ChatsContext, rhId: State, chatInfo: State, scope: CoroutineScope, - scrollToItem: (Long) -> Unit, - contentTag: MsgContentTag? + scrollToItem: (Long) -> Unit ): (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) @@ -2260,48 +2364,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, @@ -2318,6 +2433,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, @@ -2375,28 +2504,28 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List + +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 @@ -2654,7 +2785,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 ) } @@ -2789,6 +2920,7 @@ fun PreviewChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( + chatsCtx = ChatModel.ChatsContext(contentTag = null), remoteHostId = remember { mutableStateOf(null) }, chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, @@ -2866,6 +2998,7 @@ fun PreviewGroupChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( + chatsCtx = ChatModel.ChatsContext(contentTag = null), remoteHostId = remember { mutableStateOf(null) }, chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, 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 b48b32030f..894bcf3b37 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.util.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.filesToDelete -import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* @@ -473,8 +472,8 @@ fun ComposeView( ) if (!chatItems.isNullOrEmpty()) { chatItems.forEach { aChatItem -> - withChats { - addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + withContext(Dispatchers.Main) { + chatModel.chatsContext.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) } } return chatItems.first().chatItem @@ -505,9 +504,9 @@ fun ComposeView( ttl = ttl ) - withChats { + withContext(Dispatchers.Main) { chatItems?.forEach { chatItem -> - addChatItem(rhId, chat.chatInfo, chatItem) + chatModel.chatsContext.addChatItem(rhId, chat.chatInfo, chatItem) } } @@ -567,9 +566,9 @@ fun ComposeView( 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 { + withContext(Dispatchers.Main) { cItems.forEach { chatItem -> - addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem) + chatModel.chatsContext.addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem) } } } @@ -581,8 +580,8 @@ fun ComposeView( val mc = checkLinkPreview() val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) if (contact != null) { - withChats { - updateContact(chat.remoteHostId, contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(chat.remoteHostId, contact) } } } @@ -599,8 +598,10 @@ fun ComposeView( updatedMessage = UpdatedMessage(updateMsgContent(oldMsgContent), cs.memberMentions), live = live ) - if (updatedItem != null) withChats { - upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + if (updatedItem != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + } } return updatedItem?.chatItem } @@ -890,7 +891,7 @@ fun ComposeView( 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 = chatModel.chatsContext.chatItems.value.findLast { it.meta.editable } if (lastEditable != null) { composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) } @@ -998,9 +999,8 @@ fun ComposeView( chatModel.sharedContent.value = null } - val userCanSend = rememberUpdatedState(chat.chatInfo.userCanSend) val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) - val userIsObserver = rememberUpdatedState(chat.userIsObserver) + val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) Column { @@ -1055,7 +1055,6 @@ fun ComposeView( val attachmentEnabled = !composeState.value.attachmentDisabled && sendMsgEnabled.value - && userCanSend.value && !isGroupAndProhibitedFiles && !nextSendGrpInv.value IconButton( @@ -1101,8 +1100,8 @@ fun ComposeView( } } - LaunchedEffect(rememberUpdatedState(chat.chatInfo.userCanSend).value) { - if (!chat.chatInfo.userCanSend) { + LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) { + if (!chat.chatInfo.sendMsgEnabled) { clearCurrentDraft() clearState() } @@ -1158,13 +1157,12 @@ fun ComposeView( chat.chatInfo is ChatInfo.Direct, liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown, sendMsgEnabled = sendMsgEnabled.value, + userCantSendReason = userCantSendReason.value, sendButtonEnabled = sendMsgEnabled.value && !(simplexLinkProhibited || fileProhibited || voiceProhibited), nextSendGrpInv = nextSendGrpInv.value, needToAllowVoiceToContact, allowedVoiceByPrefs, allowVoiceToContact = ::allowVoiceToContact, - userIsObserver = userIsObserver.value, - userCanSend = userCanSend.value, sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, 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/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt index 85e3f3936e..b9538bc691 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 @@ -9,12 +9,10 @@ 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.TextRange 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.* @@ -23,34 +21,46 @@ 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, - contentTag: MsgContentTag?, selectedChatItems: MutableState?>, deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible archiveItems: () -> Unit, @@ -111,7 +121,7 @@ fun SelectedItemsBottomToolbar( } Divider(Modifier.align(Alignment.TopStart)) } - val chatItems = remember { derivedStateOf { chatModel.chatItemsForContent(contentTag).value } } + 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) } @@ -162,4 +172,4 @@ private fun recheckItems(chatInfo: ChatInfo, } 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 5524eff655..5710f09ed5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -15,9 +15,7 @@ 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.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.* import chat.simplex.common.model.* @@ -41,12 +39,11 @@ fun SendMsgView( isDirectChat: Boolean, liveMessageAlertShown: SharedPreference, sendMsgEnabled: Boolean, + userCantSendReason: Pair?, sendButtonEnabled: Boolean, nextSendGrpInv: Boolean, needToAllowVoiceToContact: Boolean, allowedVoiceByPrefs: Boolean, - userIsObserver: Boolean, - userCanSend: Boolean, sendButtonColor: Color = MaterialTheme.colors.primary, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, @@ -82,14 +79,14 @@ fun SendMsgView( (!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, @@ -102,16 +99,23 @@ fun SendMsgView( } } 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) @@ -135,11 +139,11 @@ fun SendMsgView( 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) } } @@ -155,7 +159,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) } @@ -343,8 +347,8 @@ private fun RecordVoiceView(recState: MutableState, stopRecOnNex } @Composable -private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) { - IconButton(onClick, Modifier.size(36.dp), enabled = enabled) { +private fun DisallowedVoiceButton(onClick: () -> Unit) { + IconButton(onClick, Modifier.size(36.dp)) { Icon( painterResource(MR.images.ic_keyboard_voice), stringResource(MR.strings.icon_descr_record_voice_message), @@ -460,14 +464,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 @@ -477,7 +480,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) @@ -576,12 +579,11 @@ fun PreviewSendMsgView() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, sendButtonEnabled = true, nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, placeholder = "", @@ -612,12 +614,11 @@ fun PreviewSendMsgViewEditing() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, sendButtonEnabled = true, nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, placeholder = "", @@ -648,12 +649,11 @@ fun PreviewSendMsgViewInProgress() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, sendButtonEnabled = true, nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, placeholder = "", 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..10694d13bf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -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) { @@ -62,11 +61,11 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea for (contactId in selectedContacts) { val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) if (member != null) { - 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 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 441fe313d4..22956738e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -8,6 +8,8 @@ import SectionItemViewLongClickable import SectionSpacer import SectionTextFooter import SectionView +import androidx.compose.animation.* +import androidx.compose.animation.core.animateDpAsState import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* @@ -17,6 +19,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString @@ -29,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 @@ -49,7 +50,19 @@ 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: CreatedConnLink?, + groupLinkMemberRole: GroupMemberRole?, + selectedItems: MutableState?>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, + scrollToItemId: MutableState, + onGroupLinkUpdated: (Pair?) -> Unit, + close: () -> Unit, + onSearchClicked: () -> Unit +) { BackHandler(onBack = close) // TODO derivedStateOf? val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } @@ -80,14 +93,16 @@ 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 + activeSortedMembers = remember { chatModel.groupMembers }.value .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } .sortedByDescending { it.memberRole }, developerTools, onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) }, groupLink, + selectedItems, + appBar, scrollToItemId, addMembers = { scope.launch(Dispatchers.Default) { @@ -166,8 +181,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() @@ -212,21 +227,23 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe text = generalGetString(messageId), confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { - withBGApi { - val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, listOf(mem.groupMemberId)) - if (updatedMembers != null) { - withChats { - updatedMembers.forEach { updatedMember -> - upsertGroupMember(rhId, groupInfo, updatedMember) - } - } - withReportsChatsIfOpen { - updatedMembers.forEach { updatedMember -> - upsertGroupMember(rhId, groupInfo, updatedMember) - } - } - } - } + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId)) + }, + destructive = true, + ) +} + +private fun removeMembersAlert(rhId: Long?, groupInfo: GroupInfo, memberIds: List, onSuccess: () -> Unit = {}) { + val messageId = if (groupInfo.businessChat == null) + MR.strings.members_will_be_removed_from_group_cannot_be_undone + else + MR.strings.members_will_be_removed_from_chat_cannot_be_undone + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.button_remove_members_question), + text = generalGetString(messageId), + confirmText = generalGetString(MR.strings.remove_member_confirmation), + onConfirm = { + removeMembers(rhId, groupInfo, memberIds, onSuccess) }, destructive = true, ) @@ -309,10 +326,12 @@ fun ModalData.GroupChatInfoLayout( setSendReceipts: (SendReceipts) -> Unit, chatItemTTL: MutableState, setChatItemTTL: (ChatItemTTL?) -> Unit, - members: List, + activeSortedMembers: List, developerTools: Boolean, onLocalAliasChanged: (String) -> Unit, - groupLink: String?, + groupLink: CreatedConnLink?, + selectedItems: MutableState?>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, scrollToItemId: MutableState, addMembers: () -> Unit, showMemberInfo: (GroupMember) -> Unit, @@ -333,20 +352,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 { @@ -401,7 +438,7 @@ fun ModalData.GroupChatInfoLayout( } } } - if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { + if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) } else { SendReceiptsOptionDisabled() @@ -424,7 +461,7 @@ fun ModalData.GroupChatInfoLayout( 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)) { + SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) { if (groupInfo.canAddMembers) { if (groupInfo.businessChat == null) { if (groupLink == null) { @@ -442,7 +479,7 @@ fun ModalData.GroupChatInfoLayout( } AddMembersButton(addMembersTitleId, tint, onAddMembersClick) } - if (members.size > 8) { + if (activeSortedMembers.size > 8) { SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { SearchRowView(searchText) } @@ -452,12 +489,34 @@ fun ModalData.GroupChatInfoLayout( } } } - items(filteredMembers.value) { member -> + items(filteredMembers.value, key = { it.groupMemberId }) { member -> Divider() val showMenu = remember { mutableStateOf(false) } - SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu) - MemberRow(member) + val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator + SectionItemViewLongClickable( + click = { + if (selectedItems.value != null) { + if (canBeSelected) { + toggleItemSelection(member.groupMemberId, selectedItems) + } + } else { + showMemberInfo(member) + } + }, + longClick = { showMenu.value = true }, + minHeight = 54.dp, + padding = PaddingValues(horizontal = DEFAULT_PADDING) + ) { + Box(contentAlignment = Alignment.CenterStart) { + androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems) + } + val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp) + DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu) + Box(Modifier.padding(start = selectionOffset)) { + MemberRow(member) + } + } } } item { @@ -482,12 +541,92 @@ 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 +private fun BoxScope.SelectedItemsButtonsToolbar(chat: Chat, groupInfo: GroupInfo, selectedItems: MutableState?>, activeMembers: State>) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + Column(Modifier.align(Alignment.BottomCenter)) { + AnimatedVisibility(selectedItems.value != null) { + SelectedItemsMembersToolbar( + selectedItems = selectedItems, + activeMembers = activeMembers, + groupInfo = groupInfo, + delete = { + removeMembersAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) { + selectedItems.value = null + } + }, + blockForAll = { block -> + if (block) { + blockForAllAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) { + selectedItems.value = null + } + } else { + unblockForAllAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) { + selectedItems.value = null + } + } + }, + changeRole = { toRole -> + updateMembersRoleDialog(toRole, groupInfo) { + updateMembersRole(toRole, chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) { + selectedItems.value = null + } + } + } + ) + } + if (oneHandUI.value) { + // That's placeholder to take some space for bottom app bar in oneHandUI + Box(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier)) + } + } +} + +@Composable +private fun SelectedItemsCounterToolbarSetter( + groupInfo: GroupInfo, + selectedItems: MutableState?>, + filteredMembers: State>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?> +) { + LaunchedEffect( + groupInfo, + /* variable, not value - intentionally - to reduce work but handle variable change because it changes in remember(members) { derivedState {} } */ + filteredMembers + ) { + snapshotFlow { selectedItems.value == null } + .collect { nullItems -> + if (!nullItems) { + appBar.value = { + SelectedItemsCounterToolbar(selectedItems, !remember { appPrefs.oneHandUI.state }.value) { + if (!groupInfo.membership.memberActive) return@SelectedItemsCounterToolbar + val ids: MutableSet = mutableSetOf() + for (mem in filteredMembers.value) { + if (groupInfo.membership.memberActive && groupInfo.membership.memberRole >= mem.memberRole && mem.memberRole < GroupMemberRole.Moderator) { + ids.add(mem.groupMemberId) + } + } + if (ids.isNotEmpty() && (selectedItems.value ?: setOf()).containsAll(ids)) { + selectedItems.value = (selectedItems.value ?: setOf()).minus(ids) + } else { + selectedItems.value = (selectedItems.value ?: setOf()).union(ids) + } + } + } + } else { + appBar.value = null + } + } } } @@ -612,7 +751,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr 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) } } @@ -686,8 +825,8 @@ private fun MemberVerifiedShield() { } @Composable -private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState) { - if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { +private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, selectedItems: MutableState?>, showMenu: MutableState) { + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { val canBlockForAll = member.canBlockForAll(groupInfo) val canRemove = member.canBeRemoved(groupInfo) if (canBlockForAll || canRemove) { @@ -711,6 +850,10 @@ private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: G showMenu.value = false }) } + if (selectedItems.value == null && member.memberRole < GroupMemberRole.Moderator) { + Divider() + SelectItemAction(showMenu) { toggleItemSelection(member.groupMemberId, selectedItems) } + } } } } else if (!member.blockedByAdmin) { @@ -813,12 +956,43 @@ 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, onSuccess: () -> Unit = {}) { + withBGApi { + val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds) + if (updatedMembers != null) { + withContext(Dispatchers.Main) { + updatedMembers.forEach { updatedMember -> + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, updatedMember) + } + } + withContext(Dispatchers.Main) { + updatedMembers.forEach { updatedMember -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, updatedMember) + } + } + onSuccess() + } + } +} + +fun toggleItemSelection(itemId: T, selectedItems: MutableState?>) { + val select = selectedItems.value?.contains(itemId) != true + if (select) { + val sel = selectedItems.value ?: setOf() + selectedItems.value = sel + itemId + } else { + val sel = (selectedItems.value ?: setOf()).toMutableSet() + sel.remove(itemId) + selectedItems.value = sel + } +} + @Preview @Composable fun PreviewGroupChatInfoLayout() { @@ -835,10 +1009,12 @@ 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) } ) 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..6e1b9a731d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.chat.group import SectionBottomSpacer +import SectionViewWithButton import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -15,11 +16,11 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.shareText +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR @Composable @@ -27,13 +28,13 @@ fun GroupLinkView( chatModel: ChatModel, rhId: Long?, groupInfo: GroupInfo, - connReqContact: String?, + connLinkContact: CreatedConnLink?, memberRole: GroupMemberRole?, - onGroupLinkUpdated: ((Pair?) -> Unit)?, + onGroupLinkUpdated: ((Pair?) -> Unit)?, creatingGroup: Boolean = false, close: (() -> Unit)? = null ) { - var groupLink by rememberSaveable { mutableStateOf(connReqContact) } + var groupLink by rememberSaveable(stateSaver = CreatedConnLink.nullableStateSaver) { mutableStateOf(connLinkContact) } val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) } var creatingLink by rememberSaveable { mutableStateOf(false) } fun createLink() { @@ -99,7 +100,7 @@ fun GroupLinkView( @Composable fun GroupLinkLayout( - groupLink: String?, + groupLink: CreatedConnLink?, groupInfo: GroupInfo, groupLinkMemberRole: MutableState, creatingLink: Boolean, @@ -150,7 +151,15 @@ fun GroupLinkLayout( } initialLaunch = false } - SimpleXLinkQRCode(groupLink) + val showShortLink = remember { mutableStateOf(true) } + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + if (groupLink.connShortLink == null) { + SimpleXCreatedLinkQRCode(groupLink, short = false) + } else { + SectionViewWithButton(titleButton = { ToggleShortLinkButton(showShortLink) }) { + SimpleXCreatedLinkQRCode(groupLink, short = showShortLink.value) + } + } Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, @@ -160,7 +169,7 @@ fun GroupLinkLayout( SimpleButton( stringResource(MR.strings.share_link), icon = painterResource(MR.images.ic_share), - click = { clipboard.shareText(simplexChatLink(groupLink)) } + click = { clipboard.shareText(groupLink.simplexChatUri(short = showShortLink.value)) } ) if (creatingGroup && close != null) { ContinueButton(close) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 41e4df8792..285c96165c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -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.* @@ -40,6 +38,7 @@ import chat.simplex.common.views.chatlist.openLoadedChat import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.datetime.Clock +import kotlinx.coroutines.* @Composable fun GroupMemberInfoView( @@ -63,11 +62,11 @@ fun GroupMemberInfoView( 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() } @@ -87,7 +86,7 @@ fun GroupMemberInfoView( getContactChat = { chatModel.getContactChat(it) }, openDirectChat = { withBGApi { - apiLoadMessages(rhId, ChatType.Direct, it, null, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) + apiLoadMessages(chatModel.chatsContext, rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) if (chatModel.getContactChat(it) != null) { closeAll() } @@ -100,8 +99,8 @@ 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) closeAll() @@ -137,26 +136,10 @@ fun GroupMemberInfoView( 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 members = chatModel.controller.apiMembersRole(rhId, groupInfo.groupId, listOf(member.groupMemberId), it) - withChats { - members.forEach { member -> - upsertGroupMember(rhId, groupInfo, member) - } - } - withReportsChatsIfOpen { - members.forEach { member -> - upsertGroupMember(rhId, groupInfo, member) - } - } - }.onFailure { - newRole.value = prevValue - } - } + updateMembersRole(newRole.value, rhId, groupInfo, listOf(member.groupMemberId), onFailure = { newRole.value = prevValue }) } }, switchMemberAddress = { @@ -165,11 +148,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() } @@ -182,11 +165,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() } @@ -202,11 +185,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() } @@ -228,11 +211,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 } @@ -263,14 +246,14 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c withBGApi { val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId)) if (removedMembers != null) { - withChats { + withContext(Dispatchers.Main) { removedMembers.forEach { removedMember -> - upsertGroupMember(rhId, groupInfo, removedMember) + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, removedMember) } } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { removedMembers.forEach { removedMember -> - upsertGroupMember(rhId, groupInfo, removedMember) + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, removedMember) } } } @@ -317,7 +300,7 @@ fun GroupMemberInfoLayout( } @Composable - fun AdminDestructiveSection() { + fun ModeratorDestructiveSection() { val canBlockForAll = member.canBlockForAll(groupInfo) val canRemove = member.canBeRemoved(groupInfo) if (canBlockForAll || canRemove) { @@ -494,8 +477,8 @@ fun GroupMemberInfoLayout( } } - if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { - AdminDestructiveSection() + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ModeratorDestructiveSection() } else { NonAdminBlockSection() } @@ -709,16 +692,37 @@ fun MemberProfileImage( ) } -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 @@ -732,6 +736,22 @@ 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 { @@ -777,11 +797,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)) } } } @@ -793,7 +813,19 @@ fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName), confirmText = generalGetString(MR.strings.block_for_all), onConfirm = { - blockMemberForAll(rhId, gInfo, mem, true) + blockMemberForAll(rhId, gInfo, listOf(mem.groupMemberId), true) + }, + destructive = true, + ) +} + +fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List, onSuccess: () -> Unit = {}) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.block_members_for_all_question), + text = generalGetString(MR.strings.block_members_desc), + confirmText = generalGetString(MR.strings.block_for_all), + onConfirm = { + blockMemberForAll(rhId, gInfo, memberIds, true, onSuccess) }, destructive = true, ) @@ -805,24 +837,36 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName), confirmText = generalGetString(MR.strings.unblock_for_all), onConfirm = { - blockMemberForAll(rhId, gInfo, mem, false) + blockMemberForAll(rhId, gInfo, listOf(mem.groupMemberId), false) }, ) } -fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { +fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List, onSuccess: () -> Unit = {}) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.unblock_members_for_all_question), + text = generalGetString(MR.strings.unblock_members_desc), + confirmText = generalGetString(MR.strings.unblock_for_all), + onConfirm = { + blockMemberForAll(rhId, gInfo, memberIds, false, onSuccess) + }, + ) +} + +fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, memberIds: List, blocked: Boolean, onSuccess: () -> Unit = {}) { withBGApi { - val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, listOf(member.groupMemberId), blocked) - withChats { + val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, memberIds, blocked) + withContext(Dispatchers.Main) { updatedMembers.forEach { updatedMember -> - upsertGroupMember(rhId, gInfo, updatedMember) + chatModel.chatsContext.upsertGroupMember(rhId, gInfo, updatedMember) } } - withReportsChatsIfOpen { + withContext(Dispatchers.Main) { updatedMembers.forEach { updatedMember -> - upsertGroupMember(rhId, gInfo, updatedMember) + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, gInfo, updatedMember) } } + onSuccess() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt new file mode 100644 index 0000000000..2c4d4b16a8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt @@ -0,0 +1,129 @@ +package chat.simplex.common.views.chat.group + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.WarningOrange +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +fun SelectedItemsMembersToolbar( + selectedItems: MutableState?>, + activeMembers: State>, + groupInfo: GroupInfo, + delete: () -> Unit, + blockForAll: (Boolean) -> Unit, // Boolean - block or unlock + changeRole: (GroupMemberRole) -> Unit, +) { + val deleteEnabled = remember { mutableStateOf(false) } + val blockForAllEnabled = remember { mutableStateOf(false) } + val unblockForAllEnabled = remember { mutableStateOf(false) } + val blockForAllButtonEnabled = remember { derivedStateOf { (blockForAllEnabled.value && !unblockForAllEnabled.value) || (!blockForAllEnabled.value && unblockForAllEnabled.value) } } + + val roleToMemberEnabled = remember { mutableStateOf(false) } + val roleToObserverEnabled = remember { mutableStateOf(false) } + val roleButtonEnabled = remember { derivedStateOf { (roleToMemberEnabled.value && !roleToObserverEnabled.value) || (!roleToMemberEnabled.value && roleToObserverEnabled.value) } } + Box( + Modifier + .background(MaterialTheme.colors.background) + .navigationBarsPadding() + .imePadding() + ) { + // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty + Box(Modifier.alpha(0f)) { + ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + } + Row( + Modifier + .matchParentSize() + .padding(horizontal = 2.dp) + .height(AppBarHeight * fontSizeSqrtMultiplier) + .pointerInput(Unit) { + detectGesture { + true + } + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(delete, enabled = deleteEnabled.value) { + Icon( + painterResource(MR.images.ic_delete), + null, + Modifier.size(22.dp), + tint = if (!deleteEnabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error + ) + } + + IconButton({ blockForAll(blockForAllEnabled.value) }, enabled = blockForAllButtonEnabled.value) { + Icon( + painterResource(if (unblockForAllEnabled.value && blockForAllButtonEnabled.value) MR.images.ic_do_not_touch else MR.images.ic_back_hand), + null, + Modifier.size(22.dp), + tint = if (!blockForAllButtonEnabled.value) MaterialTheme.colors.secondary else if (blockForAllEnabled.value) MaterialTheme.colors.error else WarningOrange + ) + } + + IconButton({ changeRole(if (roleToMemberEnabled.value) GroupMemberRole.Member else GroupMemberRole.Observer) }, enabled = roleButtonEnabled.value) { + Icon( + painterResource(if (roleToObserverEnabled.value || !roleButtonEnabled.value) MR.images.ic_person else MR.images.ic_person_edit), + null, + Modifier.size(22.dp), + tint = if (!roleButtonEnabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + Divider(Modifier.align(Alignment.TopStart)) + } + LaunchedEffect(groupInfo, activeMembers.value.toList(), selectedItems.value) { + recheckItems(groupInfo, selectedItems, activeMembers.value, deleteEnabled, blockForAllEnabled, unblockForAllEnabled, roleToMemberEnabled, roleToObserverEnabled) + } +} + +private fun recheckItems( + groupInfo: GroupInfo, + selectedItems: MutableState?>, + activeMembers: List, + deleteEnabled: MutableState, + blockForAllEnabled: MutableState, + unblockForAllEnabled: MutableState, + roleToMemberEnabled: MutableState, + roleToObserverEnabled: MutableState, +) { + val selected = selectedItems.value ?: return + var rDeleteEnabled = true + var rBlockForAllEnabled = true + var rUnblockForAllEnabled = true + var rRoleToMemberEnabled = true + var rRoleToObserverEnabled = true + val rSelectedItems = mutableSetOf() + for (mem in activeMembers) { + if (selected.contains(mem.groupMemberId) && groupInfo.membership.memberRole >= mem.memberRole && mem.memberRole < GroupMemberRole.Moderator && groupInfo.membership.memberActive) { + rDeleteEnabled = rDeleteEnabled && mem.memberStatus != GroupMemberStatus.MemRemoved && mem.memberStatus != GroupMemberStatus.MemLeft + rBlockForAllEnabled = rBlockForAllEnabled && !mem.blockedByAdmin + rUnblockForAllEnabled = rUnblockForAllEnabled && mem.blockedByAdmin + rRoleToMemberEnabled = rRoleToMemberEnabled && mem.memberRole != GroupMemberRole.Member + rRoleToObserverEnabled = rRoleToObserverEnabled && mem.memberRole != GroupMemberRole.Observer + rSelectedItems.add(mem.groupMemberId) // we are collecting new selected items here to account for any changes in members list + } + } + deleteEnabled.value = rDeleteEnabled + blockForAllEnabled.value = rBlockForAllEnabled + unblockForAllEnabled.value = rUnblockForAllEnabled + roleToMemberEnabled.value = rRoleToMemberEnabled + roleToObserverEnabled.value = rRoleToObserverEnabled + selectedItems.value = rSelectedItems +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt index 89ef19dcda..91f4f5173c 100644 --- 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 @@ -23,6 +23,12 @@ 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) @@ -126,7 +132,9 @@ fun GroupMentions( } val newName = existingMention?.key ?: composeState.value.mentionMemberName(member.memberProfile.displayName) mentions[newName] = CIMention(member) - var msgMention = "@" + if (newName.contains(" ")) "'$newName'" else newName + 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) { @@ -196,7 +204,7 @@ fun GroupMentions( MaxMentionsReached() } } - itemsIndexed(filteredMembers.value, key = { _, item -> item.memberId }) { i, member -> + itemsIndexed(filteredMembers.value, key = { _, item -> item.groupMemberId }) { i, member -> if (i != 0 || !showMaxReachedBox) { Divider() } 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 64195322c2..12c5b65769 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -15,12 +15,14 @@ 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.res.MR +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 +43,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() 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..fb24c028b2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -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() } 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 058ee59a3b..1eeeb99c93 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,16 +14,14 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.* -val LocalContentTag: ProvidableCompositionLocal = staticCompositionLocalOf { null } - @Composable -private fun GroupReportsView(staleChatId: State, scrollToItemId: MutableState) { - ChatView(staleChatId, contentTag = MsgContentTag.Report, scrollToItemId, onComposed = {}) +private fun GroupReportsView(reportsChatsCtx: ChatModel.ChatsContext, staleChatId: State, scrollToItemId: MutableState) { + ChatView(reportsChatsCtx, staleChatId, scrollToItemId, onComposed = {}) } @Composable fun GroupReportsAppBar( - contentTag: MsgContentTag?, + chatsCtx: ChatModel.ChatsContext, close: () -> Unit, onSearchValueChanged: (String) -> Unit ) { @@ -51,11 +49,11 @@ fun GroupReportsAppBar( } } ) - ItemsReload(contentTag) + ItemsReload(chatsCtx) } @Composable -private fun ItemsReload(contentTag: MsgContentTag?) { +private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) { LaunchedEffect(Unit) { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() @@ -65,18 +63,19 @@ private fun ItemsReload(contentTag: MsgContentTag?) { .filterNotNull() .filter { it.chatInfo is ChatInfo.Group } .collect { chat -> - reloadItems(chat, contentTag) + 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(contentTag = MsgContentTag.Report) + openChat(secondaryChatsCtx = reportsChatsCtx, chatModel.remoteHostId(), chatInfo) + ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close -> ModalView({}, showAppBar = false) { val chatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chatModel.chatId.value }?.chatInfo } }.value if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) { - GroupReportsView(staleChatId, scrollToItemId) + GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId) } else { LaunchedEffect(Unit) { close() @@ -86,6 +85,6 @@ suspend fun showGroupReportsView(staleChatId: State, scrollToItemId: Mu } } -private suspend fun reloadItems(chat: Chat, contentTag: MsgContentTag?) { - apiLoadMessages(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, contentTag, 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/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/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 0863c28007..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 @@ -92,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( @@ -265,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 e4bcc965c4..2e789df7bc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -10,7 +10,6 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.* import androidx.compose.ui.graphics.* @@ -30,7 +29,6 @@ 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 @@ -64,6 +62,7 @@ data class ChatItemReactionMenuItem ( @Composable fun ChatItemView( + chatsCtx: ChatModel.ChatsContext, rhId: Long?, cInfo: ChatInfo, cItem: ChatItem, @@ -73,6 +72,7 @@ fun ChatItemView( linkMode: SimplexLinkMode, revealed: State, highlighted: State, + hoveredItemId: MutableState, range: State, selectedChatItems: MutableState?>, searchIsNotBlank: State, @@ -115,16 +115,8 @@ fun ChatItemView( val onLinkLongClick = { _: String -> showMenu.value = true } val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value - val bubbleInteractionSource = remember { MutableInteractionSource() } - val lineInteractionSource = remember { MutableInteractionSource() } - val bubblePressed = bubbleInteractionSource.collectIsPressedAsState() - val linePressed = lineInteractionSource.collectIsPressedAsState() - val lineHovered = lineInteractionSource.collectIsHoveredAsState() - val lineActivated = remember { derivedStateOf { lineHovered.value || linePressed.value || bubblePressed.value } } Box( - modifier = (if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier) - .hoverable(lineInteractionSource) - .clickable(onClick = {}, interactionSource = lineInteractionSource, indication = null), + modifier = (if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier), contentAlignment = alignment, ) { val info = cItem.meta.itemStatus.statusInto @@ -240,20 +232,42 @@ fun ChatItemView( } @Composable - fun GoToItemInnerButton(alignStart: Boolean, icon: ImageResource, parentActivated: State, onClick: () -> Unit) { + fun GoToItemInnerButton(alignStart: Boolean, icon: ImageResource, iconSize: Dp = 22.dp, parentActivated: State, onClick: () -> Unit) { val buttonInteractionSource = remember { MutableInteractionSource() } val buttonHovered = buttonInteractionSource.collectIsHoveredAsState() val buttonPressed = buttonInteractionSource.collectIsPressedAsState() val buttonActivated = remember { derivedStateOf { buttonHovered.value || buttonPressed.value } } + + val fullyVisible = parentActivated.value || buttonActivated.value || hoveredItemId.value == cItem.id + val mixAlpha = 0.6f + val mixedBackgroundColor = if (fullyVisible) { + if (MaterialTheme.colors.isLight) { + MaterialTheme.colors.secondary.mixWith(Color.White, mixAlpha) + } else { + MaterialTheme.colors.secondary.mixWith(Color.Black, mixAlpha) + } + } else { + Color.Unspecified + } + val iconTint = if (fullyVisible) { + Color.White + } else { + if (MaterialTheme.colors.isLight) { + MaterialTheme.colors.secondary.mixWith(Color.White, mixAlpha) + } else { + MaterialTheme.colors.secondary.mixWith(Color.Black, mixAlpha) + } + } + IconButton( onClick, Modifier .padding(start = if (alignStart) 0.dp else DEFAULT_PADDING_HALF + 3.dp, end = if (alignStart) DEFAULT_PADDING_HALF + 3.dp else 0.dp) - .size(22.dp) - .alpha(if (parentActivated.value || buttonActivated.value) 1f else 0.4f), + .then(if (fullyVisible) Modifier.background(mixedBackgroundColor, CircleShape) else Modifier) + .size(22.dp), interactionSource = buttonInteractionSource ) { - Icon(painterResource(icon), null, Modifier.size(22.dp), tint = MaterialTheme.colors.secondary) + Icon(painterResource(icon), null, Modifier.size(iconSize), tint = iconTint) } } @@ -261,17 +275,17 @@ fun ChatItemView( fun GoToItemButton(alignStart: Boolean, parentActivated: State) { val chatTypeApiIdMsgId = cItem.meta.itemForwarded?.chatTypeApiIdMsgId if (searchIsNotBlank.value) { - GoToItemInnerButton(alignStart, MR.images.ic_search, parentActivated) { + GoToItemInnerButton(alignStart, MR.images.ic_search, 17.dp, parentActivated) { withBGApi { - openChat(rhId, cInfo.chatType, cInfo.apiId, null, cItem.id) + openChat(secondaryChatsCtx = null, rhId, cInfo.chatType, cInfo.apiId, cItem.id) closeReportsIfNeeded() } } } else if (chatTypeApiIdMsgId != null) { - GoToItemInnerButton(alignStart, MR.images.ic_arrow_forward, parentActivated) { + GoToItemInnerButton(alignStart, MR.images.ic_arrow_forward, 22.dp, parentActivated) { val (chatType, apiId, msgId) = chatTypeApiIdMsgId withBGApi { - openChat(rhId, chatType, apiId, null, msgId) + openChat(secondaryChatsCtx = null, rhId, chatType, apiId, msgId) closeReportsIfNeeded() } } @@ -280,13 +294,15 @@ fun ChatItemView( 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, lineActivated) + GoToItemButton(true, bubbleHovered) } Column(Modifier.weight(1f, fill = false)) { val enterInteraction = remember { HoverInteraction.Enter() } - KeyChangeEffect(highlighted.value) { - if (highlighted.value) { + LaunchedEffect(highlighted.value, hoveredItemId.value) { + if (highlighted.value || hoveredItemId.value == cItem.id) { bubbleInteractionSource.emit(enterInteraction) } else { bubbleInteractionSource.emit(HoverInteraction.Exit(enterInteraction)) @@ -295,7 +311,15 @@ fun ChatItemView( Column( Modifier .clipChatItem(cItem, itemSeparation.largeGap, revealed.value) - .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick, interactionSource = bubbleInteractionSource, indication = LocalIndication.current) + .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 @@ -313,7 +337,7 @@ fun ChatItemView( @Composable fun MsgReactionsMenu() { - val rs = MsgReaction.old.mapNotNull { r -> + val rs = MsgReaction.supported.mapNotNull { r -> if (null == cItem.reactions.find { it.userReacted && it.reaction.text == r.text }) { r } else { @@ -324,7 +348,7 @@ fun ChatItemView( Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically) { rs.forEach() { r -> Box( - Modifier.size(36.dp).clickable { + Modifier.size(36.dp).clip(CircleShape).clickable { setReaction(cInfo, cItem, true, r) showMenu.value = false }, @@ -340,7 +364,7 @@ fun ChatItemView( @Composable fun DeleteItemMenu() { DefaultDropdownMenu(showMenu) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -358,7 +382,7 @@ fun ChatItemView( if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { ArchiveReportItemAction(cItem.id, cInfo.groupInfo.membership.memberActive, showMenu, archiveReports) } - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report)) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report)) Divider() SelectItemAction(showMenu, selectChatItem) } @@ -448,7 +472,7 @@ fun ChatItemView( 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) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) } if (cItem.chatDir !is CIDirection.GroupSnd) { val groupInfo = cItem.memberToModerate(cInfo)?.first @@ -474,7 +498,7 @@ 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 = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -484,7 +508,7 @@ fun ChatItemView( cItem.isDeletedContent -> { DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -498,7 +522,7 @@ fun ChatItemView( } else { ExpandItemAction(revealed, showMenu, reveal) } - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -507,7 +531,7 @@ fun ChatItemView( } else -> { DefaultDropdownMenu(showMenu) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (selectedChatItems.value == null) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -524,7 +548,7 @@ fun ChatItemView( RevealItemAction(revealed, showMenu, reveal) } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -536,7 +560,7 @@ fun ChatItemView( 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) + MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) MarkedDeletedItemDropdownMenu() } else { if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { @@ -558,7 +582,7 @@ fun ChatItemView( DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -607,16 +631,23 @@ fun ChatItemView( } @Composable fun EventItemView() { - val reversedChatItems = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed() + val reversedChatItems = chatsCtx.chatItems.value.asReversed() CIEventView(eventItemViewText(reversedChatItems)) } @Composable fun DeletedItem() { - MarkedDeletedItemView(cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) DefaultDropdownMenu(showMenu) { + if (revealed.value) { + HideItemAction(revealed, showMenu, reveal) + } else if (!cItem.isDeletedContent) { + RevealItemAction(revealed, showMenu, reveal) + } else if (range.value != null) { + ExpandItemAction(revealed, showMenu, reveal) + } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), 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) @@ -697,11 +728,11 @@ fun ChatItemView( MsgContentItemDropdownMenu() } is CIContent.RcvChatFeature -> { - CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) + CIChatFeatureView(chatsCtx, 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) + CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) MsgContentItemDropdownMenu() } is CIContent.RcvChatPreference -> { @@ -710,23 +741,23 @@ fun ChatItemView( DeleteItemMenu() } is CIContent.SndChatPreference -> { - CIChatFeatureView(cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu) + CIChatFeatureView(chatsCtx, 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) + 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(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu) + 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(cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu) + CIChatFeatureView(chatsCtx, 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) + CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu) MsgContentItemDropdownMenu() } is CIContent.SndModerated -> DeletedItem() @@ -744,7 +775,7 @@ fun ChatItemView( } } if (!cItem.chatDir.sent) { - GoToItemButton(false, lineActivated) + GoToItemButton(false, bubbleHovered) } } if (cItem.content.msgContent != null && (cItem.meta.itemDeleted == null || revealed.value) && cItem.reactions.isNotEmpty()) { @@ -798,6 +829,7 @@ fun ItemInfoAction( @Composable fun DeleteItemAction( + chatsCtx: ChatModel.ChatsContext, cItem: ChatItem, revealed: State, showMenu: MutableState, @@ -806,14 +838,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) { @@ -865,14 +896,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() } ) } @@ -1282,7 +1313,7 @@ fun shapeStyle(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Bo } private fun closeReportsIfNeeded() { - if (appPlatform.isAndroid && ModalManager.end.isLastModalOpen(ModalViewId.GROUP_REPORTS)) { + if (appPlatform.isAndroid && ModalManager.end.isLastModalOpen(ModalViewId.SECONDARY_CHAT)) { ModalManager.end.closeModals() } } @@ -1391,6 +1422,7 @@ fun PreviewChatItemView( chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello") ) { ChatItemView( + chatsCtx = ChatModel.ChatsContext(contentTag = null), rhId = null, ChatInfo.Direct.sampleData, chatItem, @@ -1399,6 +1431,7 @@ fun PreviewChatItemView( 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) }, @@ -1439,6 +1472,7 @@ fun PreviewChatItemView( fun PreviewChatItemViewDeletedContent() { SimpleXTheme { ChatItemView( + chatsCtx = ChatModel.ChatsContext(contentTag = null), rhId = null, ChatInfo.Direct.sampleData, ChatItem.getDeletedContentSampleData(), @@ -1447,6 +1481,7 @@ fun PreviewChatItemViewDeletedContent() { 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) }, 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 689bf195f5..fd8a32af64 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -66,7 +66,11 @@ fun FramedItemView( @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()) 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 f731db2df9..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) { 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 89dc84a1fc..958b794bd7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -20,8 +20,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller -import chat.simplex.common.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.chat.* @@ -105,13 +103,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, null, 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) @@ -190,7 +189,7 @@ 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)) + else -> openDirectChat(rhId, contact.contactId) } } @@ -198,30 +197,33 @@ 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) suspend fun openChat( + secondaryChatsCtx: ChatModel.ChatsContext?, rhId: Long?, chatType: ChatType, apiId: Long, - contentTag: MsgContentTag? = null, openAroundItemId: Long? = null -) = +) { + if (secondaryChatsCtx != null) { + chatModel.secondaryChatsContext.value = secondaryChatsCtx + } apiLoadMessages( + chatsCtx = secondaryChatsCtx ?: chatModel.chatsContext, rhId, chatType, apiId, - contentTag, if (openAroundItemId != null) { ChatPagination.Around(openAroundItemId, ChatPagination.INITIAL_COUNT) } else { @@ -230,21 +232,22 @@ suspend fun openChat( "", 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.chatItemStatuses.clear() + 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 { @@ -546,7 +549,7 @@ fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection ModalManager.center.closeModals() ModalManager.end.closeModals() ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close -> - ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close) + ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connLinkInv, chatInfo.contactConnection, true, close) } showMenu.value = false }, @@ -603,11 +606,11 @@ 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, @@ -624,9 +627,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) } } } @@ -646,9 +649,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.updateChatTagReadNoContentTag(chat, wasUnread) } } } @@ -689,8 +692,8 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) if (contact != null && isCurrentUser && contactRequest != null) { val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) - withChats { - replaceChat(rhId, contactRequest.id, chat) + withContext(Dispatchers.Main) { + chatModel.chatsContext.replaceChat(rhId, contactRequest.id, chat) } chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) close?.invoke(chat) @@ -701,8 +704,8 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { withBGApi { chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) - withChats { - removeChat(rhId, contactRequest.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, contactRequest.id) } } } @@ -719,8 +722,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() } @@ -740,8 +743,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 @@ -804,8 +807,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), @@ -847,8 +850,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 @@ -902,16 +905,16 @@ 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.None) { ntfManager.cancelNotificationsForChat(chatInfo.id) } val updatedChat = chatModel.getChat(chatInfo.id) if (updatedChat != null) { - withChats { - updateChatTagReadNoContentTag(updatedChat, wasUnread) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatTagReadNoContentTag(updatedChat, wasUnread) } } val current = currentState?.value 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 e551ed9635..87c02f038c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -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 @@ -127,38 +124,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()), - updated = true, - 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() 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 6c88162f8a..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 @@ -32,8 +32,6 @@ import chat.simplex.common.model.ChatController.apiDeleteChatTag import chat.simplex.common.model.ChatController.apiSetChatTags import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.clearActiveChatFilterIfNeeded -import chat.simplex.common.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.chat.item.ItemAction @@ -43,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) { @@ -417,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) } } @@ -453,14 +452,14 @@ private fun deleteTag(rhId: Long?, tag: ChatTag, saving: MutableState) 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/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt index da70aef621..4e65a3649e 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 @@ -56,13 +55,13 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel when (contactType) { ContactType.RECENT -> { withApi { - openChat(rhId, chat.chatInfo) + openChat(secondaryChatsCtx = null, rhId, chat.chatInfo) ModalManager.start.closeModals() } } ContactType.CHAT_DELETED -> { withApi { - openChat(rhId, chat.chatInfo) + openChat(secondaryChatsCtx = null, rhId, chat.chatInfo) ModalManager.start.closeModals() } } 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..4a911fa6f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -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() { @@ -538,15 +537,15 @@ fun deleteChatDatabaseFilesAndState() { // 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 +784,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/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt index de4e9fe281..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 @@ -21,6 +21,7 @@ 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 @@ -135,10 +136,15 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel @Composable fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State, onLongClick: () -> Unit) { - Column(Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID).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..af207d1381 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -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,7 @@ class ModalData(val keyboardCoversBar: Boolean = true) { } enum class ModalViewId { - GROUP_REPORTS + SECONDARY_CHAT } class ModalManager(private val placement: ModalPlacement? = null) { @@ -154,13 +161,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/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index 37bf5b10b1..0d188bb73c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -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?, 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..d74846f8a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -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}") @@ -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..3d913cf957 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -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 @@ -30,6 +29,7 @@ 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,10 +42,10 @@ 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() + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId = rhId, groupInfo) + chatModel.chatsContext.chatItems.clearAndNotify() + chatModel.chatsContext.chatItemStatuses.clear() chatModel.chatId.value = groupInfo.id } setGroupMembers(rhId, groupInfo, chatModel) @@ -57,7 +57,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, connLinkContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close) } } } 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..330c80b7a2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -7,13 +7,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign 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,7 +19,7 @@ enum class ConnectionLinkType { suspend fun planAndConnect( rhId: Long?, - uri: String, + shortOrFullLink: String, incognito: Boolean?, close: (() -> Unit)?, cleanup: (() -> Unit)? = null, @@ -29,18 +27,19 @@ suspend fun planAndConnect( filterKnownGroup: ((GroupInfo) -> Unit)? = null, ): 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) + if (result != null) { + val (connectionLink, connectionPlan) = result + val link = strHasSingleSimplexLink(shortOrFullLink.trim()) val linkText = if (link?.format is Format.SimplexLink) "

${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}" else @@ -50,10 +49,10 @@ suspend fun planAndConnect( InvitationLinkPlan.Ok -> { Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito") if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_invitation_link), text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, connectDestructive = false, @@ -68,7 +67,7 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, onDismiss = cleanup, onDismissRequest = cleanup, destructive = true, @@ -76,7 +75,7 @@ suspend fun planAndConnect( ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, connectDestructive = true, @@ -97,7 +96,7 @@ suspend fun planAndConnect( String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } else { AlertManager.privacySensitive.showAlertMsg( @@ -105,7 +104,7 @@ suspend fun planAndConnect( generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } is InvitationLinkPlan.Known -> { @@ -120,7 +119,7 @@ suspend fun planAndConnect( String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } } @@ -128,10 +127,10 @@ suspend fun planAndConnect( ContactAddressPlan.Ok -> { Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito") if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_contact_link), text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, connectDestructive = false, @@ -146,7 +145,7 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, destructive = true, onDismiss = cleanup, onDismissRequest = cleanup, @@ -154,7 +153,7 @@ suspend fun planAndConnect( ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, connectDestructive = true, @@ -169,7 +168,7 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_plan_repeat_connection_request), text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, onDismiss = cleanup, onDismissRequest = cleanup, destructive = true, @@ -177,7 +176,7 @@ suspend fun planAndConnect( ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_repeat_connection_request), text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, connectDestructive = true, @@ -197,7 +196,7 @@ suspend fun planAndConnect( String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } is ContactAddressPlan.Known -> { @@ -212,19 +211,19 @@ suspend fun planAndConnect( String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } is ContactAddressPlan.ContactViaAddress -> { Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress, incognito=$incognito") val contact = connectionPlan.contactAddressPlan.contact if (incognito != null) { - close?.invoke() + close() connectContactViaAddress(chatModel, rhId, contact.contactId, incognito) } else { askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) } - cleanup?.invoke() + cleanup() } } is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { @@ -235,14 +234,14 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_via_group_link), text = generalGetString(MR.strings.you_will_join_group) + linkText, confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, onDismiss = cleanup, onDismissRequest = cleanup, hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_group_link), text = generalGetString(MR.strings.you_will_join_group) + linkText, connectDestructive = false, @@ -256,7 +255,7 @@ suspend fun planAndConnect( if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - ownGroupLinkConfirmConnect(chatModel, rhId, uri, linkText, incognito, connectionPlan, groupInfo, close, cleanup) + ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, incognito, connectionPlan, groupInfo, close, cleanup) } } GroupLinkPlan.ConnectingConfirmReconnect -> { @@ -266,7 +265,7 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_plan_repeat_join_request), text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, onDismiss = cleanup, onDismissRequest = cleanup, destructive = true, @@ -274,7 +273,7 @@ suspend fun planAndConnect( ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_repeat_join_request), text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, connectDestructive = true, @@ -304,7 +303,7 @@ suspend fun planAndConnect( hostDevice = hostDevice(rhId), ) } - cleanup?.invoke() + cleanup() } is GroupLinkPlan.Known -> { Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") @@ -326,22 +325,23 @@ suspend fun planAndConnect( hostDevice = hostDevice(rhId), ) } - cleanup?.invoke() + cleanup() } } } - } - } else { - Log.d(TAG, "planAndConnect, plan error") - if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close, cleanup) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan = null, close, - title = generalGetString(MR.strings.connect_plan_connect_via_link), - connectDestructive = false, - cleanup = cleanup, - ) + is ConnectionPlan.Error -> { + Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}") + if (incognito != null) { + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan = null, close, cleanup) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, rhId, connectionLink, connectionPlan = null, close, + title = generalGetString(MR.strings.connect_plan_connect_via_link), + connectDestructive = false, + cleanup = cleanup, + ) + } + } } } return completable @@ -350,17 +350,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 +378,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 +407,7 @@ fun askCurrentOrIncognitoProfileAlert( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) @@ -414,7 +415,7 @@ fun askCurrentOrIncognitoProfileAlert( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) @@ -445,7 +446,7 @@ 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?, @@ -471,7 +472,7 @@ fun ownGroupLinkConfirmConnect( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }) { Text( @@ -484,7 +485,7 @@ fun ownGroupLinkConfirmConnect( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) @@ -493,7 +494,7 @@ fun ownGroupLinkConfirmConnect( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) 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/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 923c0256a8..1b3138d21c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -4,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 @@ -31,7 +32,6 @@ 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.topPaddingToContent @@ -39,7 +39,6 @@ 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 +49,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 +144,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 +163,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 +186,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 +314,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 +337,8 @@ fun ActiveProfilePicker( } if (updatedConn != null) { - withChats { - updateContactConnection(user.remoteHostId, updatedConn) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(user.remoteHostId, updatedConn) } } @@ -368,8 +367,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() @@ -451,15 +450,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 +535,18 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection } } +@Composable +fun ToggleShortLinkButton(short: MutableState) { + Text( + stringResource(if (short.value) MR.strings.full_link_button_text else MR.strings.short_link_button_text), + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { short.value = !short.value }, + style = MaterialTheme.typography.body2, fontSize = 14.sp, color = MaterialTheme.colors.primary + ) +} + @Composable fun AddContactLearnMoreButton() { IconButton( @@ -677,17 +694,17 @@ private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanu 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..bacb5ab802 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -12,13 +12,33 @@ import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.stringResource import boofcv.alg.drawing.FiducialImageEngine import boofcv.alg.fiducial.qrcode.* -import chat.simplex.common.model.CryptoFile +import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.launch +@Composable +fun SimpleXCreatedLinkQRCode( + connLink: CreatedConnLink, + short: Boolean, + modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), + tintColor: Color = Color(0xff062d56), + withLogo: Boolean = true, + onShare: (() -> Unit)? = null, +) { + QRCode( + connLink.simplexChatUri(short), + modifier, + padding, + tintColor, + withLogo, + onShare, + ) +} + @Composable fun SimpleXLinkQRCode( connReq: String, @@ -38,14 +58,6 @@ fun SimpleXLinkQRCode( ) } -fun simplexChatLink(uri: String): String { - return if (uri.startsWith("simplex:/")) { - uri.replace("simplex:/", "https://simplex.chat/") - } else { - uri - } -} - @Composable fun QRCode( connReq: String, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 7d76f91521..a14f163a91 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,18 @@ 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.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* @@ -27,11 +30,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() } @@ -41,6 +40,73 @@ fun ModalData.ChooseServerOperators( val selectedOperatorIds = remember { stateGetOrPut("selectedOperatorIds") { serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() } } val selectedOperators = remember { derivedStateOf { serverOperators.value.filter { selectedOperatorIds.value.contains(it.operatorId) } } } + ColumnWithScrollBar( + Modifier + .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), + maxIntrinsicSize = true + ) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), bottomPadding = DEFAULT_PADDING) + } + + Spacer(Modifier.weight(1f)) + Column( + (if (appPlatform.isDesktop) Modifier.width(450.dp).align(Alignment.CenterHorizontally) else Modifier) + .fillMaxWidth() + .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), + horizontalAlignment = Alignment.Start + ) { + Text( + stringResource(MR.strings.onboarding_conditions_private_chats_not_accessible), + style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp) + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.onboarding_conditions_by_using_you_agree), + style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp) + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.onboarding_conditions_privacy_policy_and_conditions_of_use), + style = TextStyle(fontSize = 17.sp), + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + ModalManager.fullscreen.showModal(endButtons = { ConditionsLinkButton() }) { + SimpleConditionsView(rhId = null) + } + } + ) + } + Spacer(Modifier.weight(1f)) + + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + AcceptConditionsButton(enabled = selectedOperatorIds.value.isNotEmpty(), selectedOperators, selectedOperatorIds) + TextButtonBelowOnboardingButton(stringResource(MR.strings.onboarding_conditions_configure_server_operators)) { + ModalManager.fullscreen.showModalCloseable { close -> + ChooseServerOperators(serverOperators, selectedOperatorIds, close) + } + } + } + } + } + } +} + +@Composable +fun ModalData.ChooseServerOperators( + serverOperators: State>, + selectedOperatorIds: MutableState>, + close: (() -> Unit) +) { + LaunchedEffect(Unit) { + prepareChatBeforeFinishingOnboarding() + } + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false) { ColumnWithScrollBar( Modifier .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), @@ -53,7 +119,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,43 +143,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()) }, - updated = false, - 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) } } } @@ -168,115 +202,36 @@ 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, + enabled: Boolean, selectedOperators: State>, - selectedOperatorIds: State>, - close: () -> Unit + 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 @@ -301,12 +256,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() { @@ -345,9 +296,7 @@ private fun enabledOperators(operators: List, selectedOperatorId } @Composable -private fun ChooseServerOperatorsInfoView( - modalManager: ModalManager -) { +private fun ChooseServerOperatorsInfoView() { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.onboarding_network_operators)) @@ -363,21 +312,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 96e6eb2294..52eea3dd9d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -14,7 +14,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.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatController.appPrefs @@ -161,29 +161,24 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool } if (updatedConditions) { - Row( + Text( + stringResource(MR.strings.view_updated_conditions), + color = MaterialTheme.colors.primary, modifier = Modifier - .clip(shape = CircleShape) - .clickable { + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { modalManager.showModalCloseable { close -> UsageConditionsView( userServers = mutableStateOf(emptyList()), currUserServers = mutableStateOf(emptyList()), - updated = true, close = close, rhId = rhId ) } } - .padding(horizontal = 6.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text( - stringResource(MR.strings.view_updated_conditions), - color = MaterialTheme.colors.primary - ) - } + ) } if (!viaSettings) { @@ -191,14 +186,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)) } @@ -214,8 +216,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) } } @@ -752,17 +763,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)) } } ), @@ -780,7 +781,7 @@ private val versionDescriptions: List = listOf( ), VersionDescription( version = "v6.3", - // post = "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html", + 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, 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..7a1fc21b17 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -1096,6 +1096,7 @@ object AppearanceScope { "en" to "English", "ar" to "العربية", "bg" to "Български", + "ca" to "Català", "cs" to "Čeština", "de" to "Deutsch", "es" to "Español", 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..72fa45b936 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -11,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 } 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..569f4ff5f8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -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, @@ -88,6 +88,13 @@ fun PrivacySettingsView( simplexLinkMode.set(it) chatModel.simplexLinkMode.value = it }) + if (appPrefs.developerTools.get()) { + SettingsPreferenceItem( + null, + stringResource(MR.strings.privacy_short_links), + chatModel.controller.appPrefs.privacyShortLinks + ) + } } SectionDividerSpaced() @@ -119,15 +126,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 +150,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) } } } @@ -284,7 +291,7 @@ private fun DeliveryReceiptsSection( SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) { SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) { DefaultSwitch( - checked = currentUser.sendRcptsContacts ?: false, + checked = currentUser.sendRcptsContacts, onCheckedChange = { enable -> setOrAskSendReceiptsContacts(enable) } @@ -292,7 +299,7 @@ private fun DeliveryReceiptsSection( } SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) { DefaultSwitch( - checked = currentUser.sendRcptsSmallGroups ?: false, + checked = currentUser.sendRcptsSmallGroups, onCheckedChange = { enable -> setOrAskSendReceiptsGroups(enable) } 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..8c7c2d8416 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -5,6 +5,7 @@ import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView +import SectionViewWithButton import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -61,7 +62,8 @@ fun UserAddressView( fun createAddress() { withBGApi { progressIndicator = true - val connReqContact = chatModel.controller.apiCreateUserAddress(user?.value?.remoteHostId) + val short = appPreferences.privacyShortLinks.get() + val connReqContact = chatModel.controller.apiCreateUserAddress(user.value?.remoteHostId, short = short) if (connReqContact != null) { chatModel.userAddress.value = UserContactLinkRec(connReqContact) @@ -102,7 +104,7 @@ fun UserAddressView( sendEmail = { userAddress -> uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(simplexChatLink(userAddress.connReqContact)) + generalGetString(MR.strings.email_invite_body).format(simplexChatLink(userAddress.connLinkContact.connFullLink)) // TODO [short links] replace with short link ) }, setProfileAddress = ::setProfileAddress, @@ -198,10 +200,14 @@ private fun UserAddressLayout( } else { val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.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) + ShareAddressButton { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } // ShareViaEmailButton { sendEmail(userAddress) } BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) @@ -584,7 +590,7 @@ fun PreviewUserAddressLayoutAddressCreated() { SimpleXTheme { UserAddressLayout( user = User.sampleData, - userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"), + userAddress = UserContactLinkRec(CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null)), createAddress = {}, share = { _ -> }, deleteAddress = {}, 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 e85d88c5f8..0c38b0c045 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.* @@ -45,7 +46,7 @@ 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 smpWebPort = remember { mutableStateOf(currentCfgVal.smpWebPort) } + val smpWebPortServers = remember { mutableStateOf(currentCfgVal.smpWebPortServers) } val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) } val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) } @@ -84,7 +85,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U sessionMode = sessionMode.value, smpProxyMode = smpProxyMode.value, smpProxyFallback = smpProxyFallback.value, - smpWebPort = smpWebPort.value, + smpWebPortServers = smpWebPortServers.value, tcpConnectTimeout = networkTCPConnectTimeout.value, tcpTimeout = networkTCPTimeout.value, tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, @@ -99,7 +100,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U sessionMode.value = cfg.sessionMode smpProxyMode.value = cfg.smpProxyMode smpProxyFallback.value = cfg.smpProxyFallback - smpWebPort.value = cfg.smpWebPort + smpWebPortServers.value = cfg.smpWebPortServers networkTCPConnectTimeout.value = cfg.tcpConnectTimeout networkTCPTimeout.value = cfg.tcpTimeout networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb @@ -154,7 +155,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U sessionMode = sessionMode, smpProxyMode = smpProxyMode, smpProxyFallback = smpProxyFallback, - smpWebPort, + smpWebPortServers, networkTCPConnectTimeout, networkTCPTimeout, networkTCPTimeoutPerKb, @@ -187,7 +188,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U sessionMode: MutableState, smpProxyMode: MutableState, smpProxyFallback: MutableState, - smpWebPort: MutableState, + smpWebPortServers: MutableState, networkTCPConnectTimeout: MutableState, networkTCPTimeout: MutableState, networkTCPTimeoutPerKb: MutableState, @@ -226,11 +227,16 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U } SectionDividerSpaced() SectionView(stringResource(MR.strings.network_smp_web_port_section_title).uppercase()) { - PreferenceToggle(stringResource(MR.strings.network_smp_web_port_toggle), checked = smpWebPort.value) { - smpWebPort.value = it - } + ExposedDropDownSettingRow( + stringResource(MR.strings.network_smp_web_port_toggle), + SMPWebPortServers.entries.map { it to stringResource(it.text) }, + smpWebPortServers + ) { smpWebPortServers.value = it } } - SectionTextFooter(String.format(stringResource(MR.strings.network_smp_web_port_footer), if (smpWebPort.value) "443" else "5223")) + 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()) { @@ -320,7 +326,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)) @@ -355,7 +361,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)) @@ -548,7 +554,7 @@ fun PreviewAdvancedNetworkSettingsLayout() { sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) }, - smpWebPort = remember { mutableStateOf(false) }, + smpWebPortServers = remember { mutableStateOf(SMPWebPortServers.Preset) }, networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, networkTCPTimeout = remember { mutableStateOf(10_000000) }, networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) }, 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 c211ba4c66..98f671ddc4 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 @@ -188,7 +188,6 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { UsageConditionsView( currUserServers, userServers, - updated = conditionsAction is UsageConditionsAction.Review, close, rhId ) @@ -712,7 +711,6 @@ private fun UnsavedChangesIndicator() { fun UsageConditionsView( currUserServers: MutableState>, userServers: MutableState>, - updated: Boolean, close: () -> Unit, rhId: Long? ) { @@ -773,17 +771,16 @@ fun UsageConditionsView( } ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { - val title = if (updated) MR.strings.operator_updated_conditions else MR.strings.operator_conditions_of_use - AppBarTitle(stringResource(title), 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 || updated) 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))), @@ -791,13 +788,12 @@ fun UsageConditionsView( ) Spacer(Modifier.fillMaxWidth().height(DEFAULT_PADDING)) } - if (updated) { - ConditionsDiffButton() - 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_ }) } @@ -807,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) } @@ -815,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/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 3a9d72b40e..4bb2244785 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -1214,7 +1214,7 @@ يريد الاتصال بك! خوادم ICE الخاصة بك خصوصيتك - حدثت ملف تعريف المجموعة + حُدثت ملف تعريف المجموعة أنت: %1$s تحديث تحديث إعدادات الشبكة؟ @@ -2156,7 +2156,7 @@ أشرطة أدوات التطبيق تمويه الشفافية - فعّل flux + فعّل flux في إعدادات الشبكة والخوادم لتحسين خصوصية البيانات الوصفية. اللامركزية الشبكية المُشغل المُعد مسبقًا الثاني في التطبيق! لتحسين خصوصية البيانات الوصفية. @@ -2282,7 +2282,6 @@ سبب آخر البلاغات 1 بلاغ - بلاغات الأعضاء المؤرشفة %d بلاغات بلاغات الأعضاء بلّغ عن المحتوى: سيراه مشرفو المجموعة فقط. @@ -2348,4 +2347,33 @@ إرسال أسرع للرسائل. رُفض رُفض + خطأ في قراءة عبارة مرور قاعدة البيانات + ينتظر + حُدثت الشروط + إزالة الأعضاء؟ + سيتم إخفاء جميع الرسائل الجديدة من هؤلاء الأعضاء! + سيتم إزالة الأعضاء من الدردشة - لا يمكن التراجع عن هذا! + إلغاء حظر الأعضاء للجميع؟ + حظر الأعضاء للجميع؟ + سيتم عرض رسائل من هؤلاء الأعضاء! + لا يمكن قراءة عبارة المرور في Keystore، يُرجى إدخالها يدويًا. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطورين. + سيتم إزالة الأعضاء من المجموعة - لا يمكن التراجع عن هذا! + المشرفين + لا يمكن قراءة عبارة المرور في Keystore. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطورين. + موافقة الانتظار + ضبّط مُشغلي الخادم + سياسة الخصوصية وشروط الاستخدام. + لا يمكن الوصول إلى الدردشات الخاصة والمجموعات وجهات اتصالك لمشغلي الخادم. + باستخدام SimpleX Chat، توافق على:\n- إرسال المحتوى القانوني فقط في المجموعات العامة.\n- احترام المستخدمين الآخرين – لا سبام. + اقبل + استخدم روابط قصيرة (تجريبي) + يتطلب هذا الرابط إصدار تطبيق أحدث. يُرجى ترقية التطبيق أو اطلب من جهة اتصالك إرسال رابط متوافق. + رابط كامل + رابط قصير + رابط قناة SimpleX + رابط اتصال غير مدعوم + استخدم منفذ TCP 443 للخوادم المُعدة مسبقًا فقط. + إيقاف التشغيل + الخوادم المُعدة مسبقًا + جميع الخوادم diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 52e25a6d8d..6726009a5f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -91,12 +91,14 @@ SimpleX contact address SimpleX one-time invitation SimpleX group link + SimpleX channel link via %1$s SimpleX links Description Full link Via browser Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red. + Use short links (BETA) Spam @@ -168,6 +170,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 @@ -460,7 +464,6 @@ 1 report %d reports Member reports - Archived member reports Share message… @@ -484,8 +487,6 @@ 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 @@ -505,6 +506,19 @@ Report content: only group moderators will see it. Report other: only group moderators will see it. + You can\'t send messages! + contact not ready + contact deleted + not synchronized + contact disabled + you are observer + Please contact group admin. + request to join rejected + group is deleted + removed from group + you left + can\'t send messages + Image Waiting for image @@ -796,6 +810,8 @@ 1-time link SimpleX address Or show this code + Full link + Short link Share profile Select chat profile Error switching profile @@ -959,6 +975,10 @@ 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 @@ -1163,6 +1183,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. @@ -1738,25 +1763,32 @@ Remove member? + Remove members? Remove member Send direct message Member will be removed from group - this cannot be undone! + Members will be removed from group - this cannot be undone! Member will be removed from chat - this cannot be undone! + Members will be removed from chat - this cannot be undone! Remove Remove member Block member? Block member Block Block member for all? + Block members for all? Block for all All new messages from %s will be hidden! + All new messages from these members will be hidden! Unblock member? Unblock member Unblock Unblock member for all? + Unblock members for all? Unblock for all Messages from %s will be shown! + Messages from these members will be shown! Blocked by admin blocked disabled @@ -2122,6 +2154,7 @@ offered %s: %2s cancelled %s all members + moderators admins owners Enabled for @@ -2284,7 +2317,7 @@ 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. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index 0d071e4cce..0e179fadd4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -182,12 +182,12 @@ 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 tu com el teu contacte pots suprimir de manera irreversible els missatges enviats. (24 hores) + 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. - Permet suprimir de manera irreversible els missatges enviats. (24 hores) + 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. @@ -307,7 +307,7 @@ Cancel·la el canvi d\'adreça Accepta les condicions Condicions acceptades - "Afegiu perfil" + Afegiu un perfil Voleu cancel·lar el canvi d\'adreça? 1 minut Accepta @@ -323,7 +323,7 @@ Configuració d\'adreça Afegeix a un altre dispositiu administradors - Suprimir contacte? + 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. @@ -331,7 +331,7 @@ %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. - Suprimir perfil + Suprimeix el perfil %d hores Esborrar per mi Els missatges directes entre membres estan prohibits en aquest xat. @@ -341,7 +341,7 @@ %d missatge(s) bloquejat(s) per l\'administrador %d missatge(s) marcat(s) eliminat(s) Error de desxifrat - Suprimir cua + Suprimeix la cua Esborrar fitxer SimpleX s\'executa en segon pla en lloc d\'utilitzar notificacions push.]]> Desactivar notificacions @@ -350,29 +350,29 @@ %d minuts Desactiva el bloqueig SimpleX Error del servidor de destinació: %1$s - Suprimir el missatge? - Suprimir %d missatges? - Suprimir %d missatges dels membres? - Suprimir el missatge del membre? + 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 - Suprimir contacte - Suprimir sense notificació + Suprimeix el contacte + Suprimeix sense notificació Desconnectat Missatge que desapareix - Suprimir - Suprimir - Suprimir la connexió pendent? + Suprimeix + Suprimeix + Voleu suprimir la connexió pendent? Contrasenya i exportació de la base de dades - Suprimir servidor + 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. - Suprimir l\'adreça? + Voleu suprimir l\'adreça? Opcions de desenvolupador Desactivar Esborrar adreça - Suprimir imatge + Suprimeix la imatge El nom mostrat no pot contenir espais en blanc. Descentralitzada Desactivar @@ -386,11 +386,11 @@ 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 - Suprimir perfil? + Voleu suprimir el perfil? Esborrar la base de dades - Suprimir tots els fitxers - Suprimir fitxers i mitjans? - Suprimir fitxers de tots els perfils + 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 @@ -427,17 +427,17 @@ %s.]]> %s.]]> %s, accepteu les condicions d\'ús.]]> - Suprimir perfil? - Suprimir perfil + Voleu suprimir el perfil? + Suprimeix el perfil predeterminat (%s) - Suprimir per a tothom + Suprimeix per a tothom Missatges directes Missatges que desapareixen Els missatges que desapareixen estan prohibits en aquest xat. %dd %d dia %d dies - Suprimir en + Suprimeix després %dh %d hora Els missatges directes entre membres estan prohibits. @@ -638,7 +638,7 @@ Introduïu el vostre nom: Error en obrir el navegador Error en exportar la base de dades de xat - Error en suprimir 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 @@ -667,14 +667,14 @@ Error en carregar els detalls Error en rebre el fitxer Error en acceptar la sol·licitud de contacte - Error en suprimir el contacte - Error en suprimir el grup + 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 - Error en suprimir la sol·licitud de contacte - Error en suprimir la connexió de contacte pendent - Error en suprimir les notes privades - Error en suprimir el perfil d\'usuari + 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 @@ -875,7 +875,7 @@ %ds %d setmanes Baixa noves versions de GitHub. - Habilita Flux + 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 @@ -939,7 +939,7 @@ habilitat per al contacte habilitat habilitat per a tu - Els contactes poden marcar missatges per suprimir-los; els podreu veure. + Els contactes poden marcar missatges per suprimir-los. Encara els podreu veure. Habilitat per Personalitza i comparteix temes de color. Temes personalitzats @@ -1149,7 +1149,7 @@ 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 revelar aquest missatge. + El missatge es marcarà per suprimir-lo. Els destinataris podran visualitzar aquest missatge. Nova experiència de xat 🎉 Unir-te Enganxar enllaç per connectar! @@ -1160,7 +1160,7 @@ 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 tu pots suprimir missatges de manera irreversible (el teu contacte pot marcar-los per suprimir-los). (24 hores) + 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! @@ -1282,7 +1282,7 @@ Nou arxiu de bases de dades Obrir la carpeta de la base de dades Aturar SimpleX? - Atura SimpleX per exportar, importar o suprimir la base de dades de xat. No podreu rebre ni enviar missatges mentre el xat estigui aturat. + 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. @@ -1315,7 +1315,7 @@ 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 (pots marcar-los per suprimir-los). (24 hores) + 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 @@ -1959,7 +1959,7 @@ 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 mitjans rebuts i enviats. Les imatges de baixa resolució es mantindran. + 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. @@ -2215,7 +2215,7 @@ L\'actualització del perfil s\'enviarà als vostres contactes. a + b ratllat - Suprimir en + Suprimeix els missatges després Eliminar el perfil de xat per La connexió no està preparada. Error en crear la llista de xat @@ -2258,7 +2258,7 @@ Violació de les normes de la comunitat Contingut inadequat Perfil inadequat - Suprimir informe + Suprimeix l\'informe L\'informe s\'arxivarà. Informar Spam @@ -2269,7 +2269,6 @@ El fitxer està blocat per l\'operador del servidor:\n%1$s. Motiu de l\'informe? 1 informe - Informes de membres arxivats %d informes Informes de membres Informes @@ -2286,13 +2285,13 @@ Només ho veieu vosaltres i moderació Error en crear informe Establir nom del xat… - Suprimir els missatges de xat del teu dispositiu. + 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 abans del seleccionat se suprimiran. + 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 @@ -2325,4 +2324,33 @@ Ajudar els administradors a moderar els seus grups. rebutjat rebutjat + pendent + pendent d\'aprovació + Tots els missatges nous d\'aquests/es membres s\'amagaran! + Blocar membres per a tots/es? + Condicions actualitzades + Error en llegir la contrasenya de la base de dades + Els/les membres s\'eliminaran del xat; això no es pot desfer! + Els missatges d\'aquests/es membres es mostraran! + Desblocar membres per a tots/es? + moderació + La frase de contrasenya a Keystore no es pot llegir. Això pot haver passat després que l\'actualització del sistema sigui incompatible amb l\'aplicació. Si no és el cas, poseu-vos en contacte amb els desenvolupadors. + Els/les membres s\'eliminaran del grup; això no es pot desfer! + La frase de contrasenya a Keystore no es pot llegir, introduïu-la manualment. Això pot haver passat després que l\'actualització del sistema sigui incompatible amb l\'aplicació. Si no és el cas, poseu-vos en contacte amb els desenvolupadors. + Expulsar membres? + Política de privadesa i condicions d\'ús. + Els xats privats, els grups i els vostres contactes no són accessibles per als operadors de servidor. + Acceptar + En utilitzar SimpleX Chat accepteu:\n- enviar només contingut legal en grups públics.\n- Respectar els altres usuaris, sense correu brossa. + Configurar els operadors de servidor + Enllaç al canal SimpleX + Aquest enllaç requereix una versió de l\'aplicació més recent. Actualitzeu l\'aplicació o demaneu al vostre contacte que enviï un enllaç compatible. + Enllaç de connexió no compatible + Emprar enllaços curts (BETA) + Enllaç complet + Enllaç curt + Tots els servidors + Apagat + Feu servir el port TCP 443 només per a servidors predefinits. + Servidors predefinits diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 19e93e6973..439b9df18d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -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ů @@ -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. @@ -1373,9 +1373,7 @@ Zjednodušený režim inkognito Vytvořit nový profil v desktopové aplikaci. 💻 Změnit inkognito při připojování. - - připojit k adresáři skupin (BETA)! -\n- doručenky (až 20 členů). -\n- rychlejší a stabilnější. + - připojení k adresáři skupin (BETA)!\n- doručenky (až 20 členů).\n- rychlejší a stabilnější. odeslat přímou zprávu smazaný kontakt Chyba @@ -2113,7 +2111,7 @@ %s.]]> %s.]]> Přidány servery pro média & soubory - Povolit flux + 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 @@ -2175,7 +2173,6 @@ Chyba aktualizace serveru Operátor serveru Decentralizace sítě - Archivována hlášení členů Archivovat hlášení Opustit chat? Nastavení adres @@ -2254,7 +2251,7 @@ 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! - Odeslat soukromý report + Odešlete soukromý report Pomozte administrátorům moderovat své skupiny. Rychlejší mazání skupin. Od %s. @@ -2338,7 +2335,7 @@ 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. - Nastavit expirace zpráv. + 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. @@ -2354,4 +2351,33 @@ Můžete přenést exportovanou databázi. zamítnuto Jemný + Chyba čtení přístupové fráze databáze + Přístupová fráze v úlozišti klíčů nelze načíst, prosím zadejte ji ručně. To se může stát po aktualizaci systému nekompatibilní s aplikací. Pokud to tak není, kontaktujte prosím vývojáře. + Přístupovou frázi v ůložišti klíčů nelze načíst. Mohlo se to stát po aktualizaci systému nekompatibilní s aplikací. Pokud to není váš případ, kontaktujte prosím vývojáře. + Aktualizované podmínky + čekám na schválení + čekám + Blokovat členy všem? + Všechny nové zprávy od těchto členů budou skryty! + Odblokovat členy všem? + moderátoři + Zprávy od těchto členů budou zobrazeny! + Členové budou odstraněny ze skupiny - toto nelze zvrátit! + Odebrat členy? + Členové budou odstraněny z chatu - toto nelze zvrátit! + Použitím SimpleX chatu souhlasíte že:\n- ve veřejných skupinách budete zasílat pouze legální obsah.\n- budete respektovat ostatní uživatele – žádný spam. + Přijmout + Nastavit operátora serveru + Zásady ochrany soukromí a podmínky používání. + Soukromé konverzace, skupiny a kontakty nejsou přístupné provozovatelům serverů. + Nepodporovaný odkaz k připojení + Používejte krátké odkazy (BETA) + Tento odkaz vyžaduje novější verzi aplikace. Prosím aktualizujte aplikaci nebo požádejte kontakt o odeslání kompatibilního odkazu. + odkaz SimpleX kanálu + Úplný odkaz + Krátký odkaz + Všechny servery + Vypnut + Přednastavené servery + Použít TCP port 443 jen pro přednastavené servery. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 444c7fedaa..6646720c5c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -20,7 +20,7 @@ Gelöscht als gelöscht markiert Das Senden von Dateien wird noch nicht unterstützt - Der Empfang von Dateien wird noch nicht unterstützt + Das Herunterladen von Dateien wird noch nicht unterstützt Profil Unbekanntes Nachrichtenformat Ungültiges Nachrichtenformat @@ -58,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. @@ -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 @@ -597,8 +597,8 @@ 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? - Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden! - 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 Älter als ein Tag @@ -661,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. @@ -1077,22 +1077,22 @@ 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 @@ -1145,7 +1145,7 @@ 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. %1$d Nachrichten konnten nicht entschlüsselt werden. - Der Hash der vorherigen Nachricht unterscheidet sich.\" + Der Hash der vorherigen Nachricht unterscheidet sich. Sie können die SimpleX-Sperre über die Einstellungen aktivieren. SOCKS-Proxy-Einstellungen System-Authentifizierung @@ -1159,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 @@ -1483,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 @@ -1652,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 + es wurde %s blockiert + Es wurden %s freigegeben Sie haben %s blockiert Sie haben %s freigegeben Mitglied für Alle blockieren? @@ -1714,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 @@ -1906,7 +1906,7 @@ 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 @@ -2089,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 @@ -2154,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? @@ -2191,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. @@ -2246,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 @@ -2382,7 +2382,6 @@ %d Meldungen Mitglieder-Meldungen Meldungen - Archivierte Mitglieder-Meldungen Inhalt verletzt Nutzungsbedingungen Spam Verbindung blockiert @@ -2434,4 +2433,33 @@ Verpassen Sie keine wichtigen Nachrichten. Chats in Listen verwalten abgelehnt + Das Passwort kann nicht aus dem Schlüsselbund gelesen werden. Dies kann nach einer Systemaktualisierung passiert sein, die nicht mit der App kompatibel war. Wenden Sie sich bitte an die Entwickler, wenn dies nicht der Fall ist. + ausstehend + Fehler beim Lesen des Datenbank-Passworts + Aktualisierte Nutzungsbedingungen + ausstehende Genehmigung + Das Passwort kann nicht aus dem Schlüsselbund gelesen werden. Bitte geben Sie es manuell ein. Dies kann nach einer Systemaktualisierung passiert sein, die nicht mit der App kompatibel war. Wenden Sie sich bitte an die Entwickler, wenn dies nicht der Fall ist. + Mitglieder entfernen? + Mitglieder werden aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden! + Mitglieder werden aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! + Mitglieder für Alle freigeben? + Nachrichten dieser Mitglieder werden angezeigt! + Moderatoren + Mitglieder für Alle blockieren? + Alle neuen Nachrichten dieser Mitglieder werden nicht angezeigt! + Durch die Nutzung von SimpleX Chat erklären Sie sich damit einverstanden:\n- nur legale Inhalte in öffentlichen Gruppen zu versenden.\n- andere Nutzer zu respektieren - kein Spam. + Datenschutz- und Nutzungsbedingungen. + Akzeptieren + Server-Betreiber konfigurieren + Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich. + Verbindungs-Link wird nicht unterstützt + Kurze Links verwenden (BETA) + Verkürzter Link + Vollständiger Link + SimpleX-Kanal-Link + Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden. + Alle Server + Aus + TCP-Port 443 nur für voreingestellte Server verwenden. + Voreingestellte Server diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 56ae24a08b..5667c42d2d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -7,9 +7,9 @@ Bueno para la batería. La aplicación comprueba si hay mensajes cada 10 minutos. Podrías perderte llamadas o mensajes urgentes.]]> Aceptar Copia de seguridad de los datos de la aplicación - un dia - un mes - una semana + 1 día + 1 mes + 1 semana Se permiten los mensajes temporales pero sólo si tu contacto también los permite. Añadir servidores mediante el escaneo de códigos QR. Añadir servidores predefinidos @@ -20,7 +20,7 @@ Permites a tus contactos enviar mensajes de voz. siempre La aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano. - ICONO APLICACIÓN + ICONO DE LA APLICACIÓN La optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración. El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos. Se puede desactivar en Configuración – las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.]]> @@ -29,7 +29,7 @@ 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. @@ -49,7 +49,7 @@ Audio activado ID de mensaje erróneo Auto aceptar imágenes - Se eliminarán todos los chats y mensajes. ¡No podrá deshacerse! + Se eliminarán todos los chats y mensajes. ¡No puede deshacerse! Aceptar Se permiten mensajes temporales. Android Keystore se usará para almacenar de forma segura la frase de contraseña - permite que el servicio de notificación funcione. @@ -72,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 la pierdes NO podrás recuperar o cambiar la frase de contraseña.]]> Tanto tú como tu contacto podéis enviar mensajes de voz. ¡Consume más 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) @@ -108,7 +108,7 @@ Llamada con cifrado de extremo a extremo cifrado de extremo a extremo mensaje duplicado - Herramientas desarrollo + Herramientas para desarrolladores Eliminar los archivos de todos los perfiles Activar ¡Base de datos cifrada! @@ -129,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 @@ -158,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! @@ -238,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 @@ -272,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 @@ -312,12 +311,12 @@ 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 @@ -343,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: @@ -411,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 @@ -428,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 @@ -490,7 +489,7 @@ eliminado por el moderador invitación a conectarse ¡Las notificaciones instantáneas están desactivadas! - mensaje nuevo + nuevo mensaje Nueva solicitud de contacto Inicie sesión con sus credenciales Error en la entrega del mensaje @@ -519,7 +518,7 @@ OK (sólo almacenado por miembros del grupo) Ayuda sintaxis markdown - Servidores y Redes + Servidores y Red Se usarán hosts .onion si están disponibles. cursiva Llamada audio entrante @@ -564,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 @@ -578,7 +577,7 @@ 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. @@ -596,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. @@ -605,7 +604,7 @@ Notificaciones privadas imagen del perfil No se permiten mensajes de voz. - Proteger la pantalla + Proteger pantalla de la aplicación repositorio GitHub .]]> Grabar mensaje de voz ha expulsado a %1$s @@ -680,11 +679,11 @@ 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. @@ -744,14 +743,14 @@ Altavoz desactivado Inciar chat nuevo 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 dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos. + ¡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. Para actualizar la configuración el cliente se reconectará a todos los servidores. ¿Usar servidores SimpleX Chat\? Enlace de grupo SimpleX - Invitación única SimpleX + Invitación SimpleX de un uso Enlaces SimpleX El servidor requiere autorización para crear colas, comprueba la contraseña Para recibir notificaciones, introduce la contraseña de la base de datos @@ -782,7 +781,7 @@ %s no está verificado Probar servidor Probar servidores - Estrella en GitHub + 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. @@ -871,7 +870,7 @@ Mediante navegador mediante %1$s Servicio SimpleX Chat - ¡Bienvenido %1$s ! + ¡Bienvenido %1$s! has sido invitado al grupo Esperando archivo Esperando imagen @@ -936,7 +935,7 @@ 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. Sistema @@ -993,10 +992,10 @@ Confirmar actualizaciones de la bases de datos la versión de la base de datos es más reciente que la aplicación, pero no hay migración hacia versión anterior para: %s EXPERIMENTAL - IDs de la base de datos y opciónes de aislamiento de transporte. + IDs de la base de datos y opciones de aislamiento de transporte. El archivo se recibirá cuando el contacto termine de subirlo. La imagen se recibirá cuando el contacto termine de subirla. - Mostrar opciones de desarrollador + Mostrar opciones para desarrolladores Ocultar: Mostrar: Eliminar perfil @@ -1069,7 +1068,7 @@ Hash de mensaje incorrecto ID de mensaje incorrecto Puede ocurrir si tu contacto o tu usáis una copia de seguridad antigua de la base de datos. - El hash del mensaje anterior es diferente.\" + El hash del mensaje anterior es diferente. %1$d mensaje(s) no ha(n) podido ser descifrado(s). Detener archivo El archivo será eliminado de los servidores. @@ -1107,7 +1106,7 @@ 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 @@ -1144,7 +1143,7 @@ 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 @@ -1234,7 +1233,7 @@ APLICACIÓN Reiniciar Cerrar - Las notificaciones dejarán de funcionar hasta que reinicies la aplicación + Las notificaciones dejarán de funcionar hasta que vuelvas a iniciar la aplicación Desactivado Error al cancelar cambio de dirección Sin chats filtrados @@ -1319,7 +1318,7 @@ ¿Permitir confirmaciones para grupos\? Las confirmaciones están deshabilitadas para los grupos %d Las confirmaciones están activadas para %d grupos - Grupos pequeños (max. 20) + Grupos pequeños (máx. 20) Activar para todos los grupos %s: %s desactivado @@ -1368,7 +1367,7 @@ Recuerda: los servidores están conectados mediante proxy SOCKS, pero las llamadas y las previsualizaciones de enlaces usan conexión directa.]]> Cifrar archivos locales Nueva aplicación para ordenador! - 6 idiomas nuevos para el interfaz + 6 nuevos idiomas para la interfaz Cifrado de los nuevos archivos locales (excepto vídeos). Envía un mensaje para conectar Descubre y únete a grupos @@ -1518,8 +1517,8 @@ No se envía el historial a los miembros nuevos. O muestra el código QR Hasta 100 últimos mensajes son enviados a los miembros nuevos. - El código QR escaneado no es un enlace SimpleX. - El texto pegado no es un enlace SimpleX. + El código QR escaneado no es un enlace de SimpleX. + El texto pegado no es un enlace de SimpleX. Permitir acceso a la cámara Podrás ver el enlace de invitación en detalles de conexión. ¿Guardar invitación no usada? @@ -1543,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 @@ -1553,7 +1552,7 @@ ¿Bloqear miembro para todos? Creado: %s Bloquear para todos - ¿Desbloquear miembro para todos? + ¿Desbloquear el miembro para todos? Desbloquear para todos bloqueado bloqueado por administrador @@ -1571,7 +1570,7 @@ 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 @@ -1684,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. @@ -1712,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 @@ -1808,7 +1807,7 @@ Respuesta recibida Mosaico Quitar imagen - Restablecer color + Reiniciar color Escala Respuesta enviada Establecer tema predefinido @@ -1823,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 @@ -1835,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 @@ -1856,7 +1854,7 @@ 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 Suscritas @@ -1903,9 +1901,9 @@ La dirección del servidor es incompatible con la configuración de red: %1$s. La versión del servidor es incompatible con tu aplicación: %1$s. Tamaño de la fuente - Error al restablecer las estadísticas - Restablecer - Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse! + 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. @@ -1944,8 +1942,8 @@ ¿Reconectar servidores? Reconectar con el servidor para forzar la entrega de mensajes. Se usa tráfico adicional. Reconectar todos los servidores para forzar la entrega de mensajes. Usa tráfico adicional. - Restablecer todas las estadísticas - ¿Restablecer todas las estadísticas? + Reiniciar estadísticas + ¿Reiniciar todas las estadísticas? Mensajes enviados Total enviados Archivos descargados @@ -1965,7 +1963,7 @@ Tamaño Conexiones activas Iniciado el %s. - Iniciado el %s \nLa información es privada en tu dispositivo. + Iniciado el %s \nLos datos son privados en tu dispositivo. Bloques eliminados Bloques descargados Bloques subidos @@ -1999,7 +1997,7 @@ 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 Eliminar sólo la conversación @@ -2020,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 @@ -2071,7 +2069,7 @@ %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. @@ -2085,7 +2083,7 @@ 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 Perfil a compartir @@ -2110,7 +2108,7 @@ 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. @@ -2121,7 +2119,7 @@ Seguridad de conexión Compartir enlace de un uso con un amigo Comparte tu dirección SimpleX en redes sociales. - Configurar dirección + Ajustes de dirección Crear enlace de un uso Para redes sociales ¿Dirección SimpleX o enlace de un uso? @@ -2153,11 +2151,11 @@ 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. @@ -2169,8 +2167,8 @@ 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. + 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 @@ -2187,7 +2185,7 @@ 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.]]> @@ -2204,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,13 +2215,13 @@ 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 miembro será eliminado del chat. ¡No puede deshacerse! El rol cambiará a %s. Todos serán notificados. Dejarás de recibir mensajes de este chat. El historial del chat se conserva. Cómo ayuda a la privacidad @@ -2268,7 +2266,7 @@ Grupos Abrir con %s Añadir a la lista - Todos los chats serán quitados de la lista %s y esta será eliminada + Todos los chats se quitarán de la lista %s y esta será eliminada Crear lista Eliminar ¿Eliminar lista? @@ -2286,8 +2284,8 @@ informe archivado por %s ¿Archivar informe? El informe será archivado para ti. - Informe - Borrar informe + Informar + Eliminar informe 1 informe Informes Informar de spam: sólo los moderadores del grupo lo verán. @@ -2303,8 +2301,7 @@ Archivo bloqueado por el operador del servidor:\n%1$s Archivar Archivar informe - Informes de miembro - Informes de miembros archivados + Informes de miembros %d informes Informar del perfil de un miembro: sólo los moderadores del grupo lo verán. Otro motivo @@ -2326,9 +2323,9 @@ Desactivar ¿Modificar la eliminación automática de mensajes? Elimina los mensajes del dispositivo - Los mensajes de esta conversación nunca se borran. - Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No podrá deshacerse! - Un año + 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. @@ -2347,18 +2344,47 @@ No se permite informar de mensajes en este grupo. Todos los informes serán archivados para ti. ¿Archivar todos los informes? - Las menciones ahora notifican. - Mencionar miembros 👋 + Las menciones ahora se notifican. + Menciona a miembros 👋 No pierdas los mensajes importantes. - Organiza los chats en listas - Nombres privados de archivos de media. + 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 moderar sus grupos. - Mejor rendimiento de los grupos - Mejor privacidad y seguridad + Ayuda a los admins a moderar sus grupos. + Rendimiento de grupos mejorado + Privacidad y seguridad mejoradas Envía informes privados - Establece vencimiento de los mensajes en chats. + Establece el vencimiento para los mensajes en los chats. rechazado rechazado + ¿Expulsar miembros? + ¡Los mensajes de estos miembros serán mostrados! + ¿Desbloquear los miembros para todos? + ¡Todos los mensajes nuevos de estos miembros estarán ocultos! + ¿Bloquear miembros para todos? + Los miembros serán expulsados del chat. ¡No puede deshacerse! + Condiciones actualizadas + moderadores + Los miembros serán expulsados del grupo. ¡No puede deshacerse! + pendiente de aprobación + pendiente + Error al leer la frase de contraseña de la base de datos + La frase de contraseña no se ha podido leer en Keystore. Puede deberse a alguna actualización del sistema incompatible con la aplicación. Si no es así, por favor, ponte en contacto con los desarrolladores. + La frase de contraseña no se ha podido leer en Keystore. Por favor, introdúcela manualmente. Puede deberse a alguna actualización del sistema incompatible con la aplicación. Si no es así, por favor, ponte en contacto con los desarrolladores. + Aceptar + Política de privacidad y condiciones de uso. + Los chats privados, los grupos y tus contactos no son accesibles para los operadores de servidores. + Al usar SimpleX Chat, aceptas:\n- enviar únicamente contenido legal en los grupos públicos.\n- respetar a los demás usuarios - spam prohibido. + Configurar operadores de servidores + Enlace de canal SimpleX + Enlace completo + Enlace corto + Este enlace requiere una versión más reciente de la aplicación. Por favor, actualiza la aplicación o pide a tu contacto un enlace compatible. + Enlace de conexión no compatible + Usar enlaces cortos (BETA) + Usar puerto TCP 443 solo en servidores predefinidos. + Todos los servidores + Servidores predefinidos + No diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 6e23eca6b6..43fcb2f5f7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -2124,7 +2124,6 @@ invitation acceptée Archiver le signalement Demander - Rapports du membre archivés Ajouter à la liste Toutes les discussions seront supprimées de la liste %s, et la liste sera supprimée Ajouter des membres à l\'équipe @@ -2326,4 +2325,23 @@ Tous les signalements seront archivés pour vous. Archiver tous les signalements ? Archiver %d signalements ? + Modifier la suppression automatique des messages ? + Supprimer les messages de discussion de votre appareil. + par défaut (%s) + Bloquer ces membres pour tous ? + Désactiver la suppression automatique des messages ? + Désactiver la suppression des messages + Ne manquez pas les messages importants. + Recevoir une notification en cas de mention. + Vie privée et sécurité renforcées + Suppression plus rapide des groupes. + Envoi plus rapide des messages. + Pour moi + Archiver les rapports + Pour tous les modérateurs + Les membres peuvent signaler des messages aux modérateurs. + Groupes plus performants + Erreur lors de la lecture de la phase secrète de la base de données + Aider les administrateurs à modérer leurs groupes. + Tous les nouveaux messages de ces membres seront cachés ! diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 8aa0dd28cc..c8897c4063 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -1,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,10 +10,10 @@ 5 perc 1 perc A SimpleX-címről - Cím módosításának megszakítása? + Megszakítja a cím módosítását? Megszakítás 30 másodperc - Egyszer használható meghívási hivatkozás + 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 @@ -26,10 +26,10 @@ Elfogadás gombra fent, majd: Elfogadás inkognitóban - Kapcsolatkérés elfogadása? + Elfogadja a meghívási kérést? Elfogadás Elfogadás - Cím hozzáadása a profilhoz, hogy a partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve az Ön partnerei 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. További kiemelőszín híváshiba Csoporttagok letiltása @@ -48,7 +48,7 @@ Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS-proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének elküldése közvetlen kapcsolatot használnak.]]> Alkalmazásadatok biztonsági mentése Az adatbázis előkészítése sikertelen - A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve az Ön partnerei számára. + A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. A csevegési profillal (alapértelmezett), vagy a kapcsolattal (BÉTA). Egy új, véletlenszerű profil lesz megosztva. A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. @@ -90,7 +90,7 @@ titkosítás elfogadása… Nem lehet meghívni a partnert! téves üzenet ID - Kapcsolatkérések automatikus elfogadása + Meghívási kérések automatikus elfogadása Megjegyzés: NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]> hívás… További másodlagos szín @@ -113,7 +113,7 @@ félkövér Az alkalmazás-jelkód helyettesítve lesz egy önmegsemmisítő-jelkóddal. Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek. - Hangüzenetek engedélyezése? + Engedélyezi a hangüzeneteket? Mindig használjon továbbítókiszolgálót mindig A hívás már befejeződött! @@ -128,8 +128,8 @@ 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 időszakos lekérdezését. A beállításokban újraengedélyezheti. - Tag letiltása? + Az akkumulátor-optimalizálás aktív, ez kikapcsolja a háttérszolgáltatást és az új üzenetek időszakos lekérdezését. Ezt a beállításokban újraengedélyezheti. + Letiltja a tagot? %1$s hívása befejeződött Jó akkumulátoridő. 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ő @@ -141,7 +141,7 @@ Engedélyezés Érvénytelen számítógépcím Profil hozzáadása - Csatolás + Mellékelés Alkalmazás jelkód Felkérték a kép fogadására Kamera @@ -160,14 +160,14 @@ 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 el lesz rejtve! + %s összes új üzenete el lesz rejtve! Az alkalmazás verziója: v%s A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. Kiszolgáló hozzáadása Hang bekapcsolva hanghívás (nem e2e titkosított) letiltva - Az adatbázis jelmondatának módosítása? + Módosítja az adatbázis jelmondatát? kapcsolódva Jelkód módosítása a következőre módosította %s szerepkörét: „%s” @@ -175,7 +175,7 @@ Módosítás Jelkód megerősítése Jelszó megerősítése - Tag szerepkörének módosítása? + Módosítja a tag szerepkörét? Zárolási mód módosítása Kapcsolódott kapcsolódott @@ -191,7 +191,7 @@ Ö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 kapcsolat %1$d @@ -199,7 +199,7 @@ Csoport létrehozása véletlenszerű profillal. 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ódás egyszer használható meghívási hivatkozással? + 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 @@ -207,18 +207,18 @@ Cím létrehozása Másolás Folytatás - Kapcsolódás egy hivatkozáson keresztül? + Kapcsolódik egy hivatkozáson keresztül? A partner már létezik Fő verzió: v%s Partner ellenőrizve - Kapcsolódás saját magához? + Kapcsolódik saját magához? Kimásolva a vágólapra - Kapcsolatkérés elküldve! + Meghívási kérés elküldve! Kapcsolódás a számítógéphez Kapcsolat - Név helyesbítése a következőre: %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 Partnerbeállítások Kapcsolat @@ -234,13 +234,13 @@ Kapcsolódási hiba 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. - Hozzájárulás + Közreműködés kapcsolódás (bemutatkozó meghívó) SimpleX-cím létrehozása törölt partner - Csoporttag üzenetének törlése? + Törli a tag üzenetét? A csevegés fut - Egyszer használható meghívási 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 @@ -279,7 +279,7 @@ 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 @@ -287,18 +287,18 @@ Hitelesítés törlése készítő Megerősítés - Csak számomra - %d üzenet törlése? + Csak nálam + Töröl %d üzenetet? Egyéni témák kapcsolódás (elfogadva) Kiszolgáló címének ellenőrzése és újrapróbálkozás. - Csoport törlése? + Törli a csoportot? Adatbázis fejlesztésének megerősítése Saját 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ájl- és a médiatartalmakat? befejezett CSEVEGÉSI ADATBÁZIS Önmegsemmisítő-jelkód módosítása @@ -307,9 +307,9 @@ 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őköz Kapcsolódás inkognitóban @@ -318,7 +318,7 @@ 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 @@ -332,16 +332,16 @@ Fájl létrehozása Tikos csoport létrehozása Elvetés - Partner 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 a partnerekével. Fájl-összehasonlítás Csevegések - Üzenet törlése? - Függőben lévő partnerkérelem törlése? + Törli az üzenetet? + Törli a függőben lévő meghívót? Adatbázis titkosítva! - Üzenetek kiürítése? + Kiüríti a csevegést? Adatbázis visszafejlesztése Üzenetek kiürítése Az adatbázis titkosítási jelmondata frissítve lesz. @@ -372,8 +372,8 @@ 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 @@ -404,10 +404,10 @@ 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 módosítsa. - Kézbesítés jelentések letiltása a csoportok számára? + Letiltja a kézbesítési jelentéseket a csoportok számára? nap %d nap - Duplikált megjelenített név! + Duplikált megjelenítendő név! Letiltás (felülírások megtartásával) Adatbázis fejlesztése %d üzenet letiltva @@ -423,7 +423,7 @@ Az adatbázis titkosítva lesz. Adatbázis-jelmondat és -exportálás Az adatbázis titkosítva lesz, a jelmondat pedig a Keystore-ban lesz tárolva. - Automatikus üzenet törlés engedélyezése? + Engedélyezi az automatikus üzenettörlést? Törlé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 @@ -447,7 +447,7 @@ 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? + 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 @@ -456,67 +456,67 @@ 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 + Hiba történt az üzenet elküldésekor Adja meg a jelkódot Mindenkinél - Hiba a titkosítás újraegyeztetésekor - 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 + 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 Adja meg az üdvözlőüzenetet… (nem kötelező) - Hiba a csevegési adatbázis exportálásakor - Hiba a fájl mentésekor + 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 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 + Hiba történt a csevegés elindításakor A csoport profilja a tagok eszközein tárolódik, nem a kiszolgálókon. Adja meg a jelmondatot… - Hiba a felhasználói adatvédelem frissítésekor + 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 + Hiba történt a csoporthoz való csatlakozáskor Kedvenc Csoport moderálása Fájl Csoporthivatkozás a titkosítás újraegyeztetése szükséges %s számára - Hiba a profilváltáskor! + Hiba történt a profilváltáskor! Kísérleti funkciók Engedélyezés (felülírások megtartásával) Adja meg a helyes jelmondatot. A csoport törölve lesz az Ön számára – ez a művelet nem vonható vissza! - Adatbázis titkosítása? + Titkosítja az adatbázist? A zárolási képernyőn megjelenő hívások engedélyezése a Beállításokban. titkosítás elfogadva - Kézbesítési jelentések engedélyezése? - Hiba a csoportprofil mentésekor + 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. @@ -526,52 +526,52 @@ 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 + Hiba történt a csoporthivatkozás törlésekor Fájl elmentve Kapcsolat javítása? Fájlok és médiatartalmak KONZOLHOZ Nem sikerült a titkosítást újraegyeztetni. - Hiba a felhasználó-profil törlésekor + Hiba történt a felhasználó-profil törlésekor Csoporttag általi javítás nem támogatott Adja meg az üdvözlőüzenetet… Titkosított adatbázis 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 Adja meg a kiszolgálót kézzel A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! - Hiba a 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 a partner törlésekor + 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 módosítá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 módosítá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ő partner 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ő meghívó törlésekor + Hiba történt a csevegési adatbázis importálásakor + Hiba történt a kézbesítési jelentések engedélyezésekor! + Hiba történt az XFTP-kiszolgálók mentésekor A tagok küldhetnek egymásnak közvetlen üzeneteket. - Hiba a tag eltávolításakor + Hiba történt a tag eltávolításakor befejeződött A csoport üdvözlőüzenete Adja meg a csoport nevét: - Hiba a meghívó küldésekor + 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 @@ -580,11 +580,11 @@ súgó Önmegsemmisítő-jelkód engedélyezése KÍSÉRLETI - Hiba a cím módosításának megszakításakor - Hiba a fájl fogadásakor + Hiba történt a cím módosításának megszakításakor + Hiba történt a fájl fogadásakor titkosítás rendben - Hiba a kapcsolatkérés törlésekor - Kézbesítési jelentések engedélyezése a csoportok számára? + Hiba történt a meghívási kérés törlésekor + Engedélyezi a kézbesítési jelentéseket a csoportok számára? Partner általi javítás nem támogatott Fájl nem található Kapcsolat bontása @@ -592,23 +592,23 @@ 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ö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 módosítá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 + Hiba történt a részletek betöltésekor + Hiba történt a meghívási kérés elfogadásakor a titkosítás újraegyeztetése engedélyezve van %s számára a titkosítás újraegyeztetése szükséges Rejtett csevegési profilok @@ -621,7 +621,7 @@ 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 + 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 @@ -652,14 +652,14 @@ 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. Hogyan működik - Elrejtés: - Hiba a partnerrel 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 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! @@ -693,7 +693,7 @@ 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! + 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 @@ -737,10 +737,10 @@ Az onion-kiszolgálók nem lesznek használva. perc Tudjon meg többet - Új kapcsolatkérés + Új meghívási ké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 @@ -775,11 +775,11 @@ érvénytelen adat 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. - A fájl betöltése… + Fájl betöltése… Nincs hozzáadandó partner Üzenetvázlat - meghívta, hogy csatlakozzon - Egyszer használható meghívási hivatkozás + Függőben lévő meghívó + Egyszer használható meghívó Értesítések Egyszerre csak 10 kép küldhető el ajánlotta: %s, ekkor: %2s @@ -809,8 +809,8 @@ (ez az eszköz: v%s)]]> %s ajánlotta Csoport elhagyása - Az összes %s által írt üzenet 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. Az üzenet visszafejtése sikertelen volt, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült. + %s összes üzenete meg fog jelenni! + Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült visszafejteni az üzenetet, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült. megfigyelő inkognitó a csoporthivatkozáson keresztül Onion-kiszolgálók használata, ha azok rendelkezésre állnak. @@ -820,9 +820,9 @@ csatlakozás mint %s Nincs csevegés kijelölve Csak helyi profiladatok - inkognitó egy egyszer használható meghívási hivatkozáson keresztül + inkognitó egy egyszer használható meghívón keresztül Moderálva: %s - Egyszer használható meghívási hivatkozás + Egyszer használható meghívó Érvénytelen név! Beszélgessünk a SimpleX Chatben Moderálva @@ -830,12 +830,12 @@ 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 @@ -871,7 +871,7 @@ elutasított hívás Időszakos fogadott, tiltott - Kapcsolatkérés megismétlése? + Megismétli a meghívási kérést? Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra) Szerepkör SimpleX kapcsolattartási cím @@ -906,8 +906,8 @@ Csak Ön tud hívásokat indítani. Biztonságos sorba állítás Értékelje az alkalmazást - Egyszer használható meghívási 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 @@ -926,7 +926,7 @@ (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ási 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. @@ -950,7 +950,7 @@ Csillagozás a GitHubon Eltávolítás Keresés - Titkosítás újraegyeztetése? + Újraegyezteti a titkosítást? Az önmegsemmisítő-jelkód engedélyezve! Biztonsági kiértékelés Cím @@ -958,7 +958,7 @@ Adatbázismentés visszaállítása Visszavonás Kérje meg a partnerét, hogy engedélyezze a hangüzenetek küldését. - Ön egy egyszer használható meghívási hivatkozást osztott meg + Ö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 @@ -966,8 +966,8 @@ 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? + 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 @@ -998,7 +998,7 @@ 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 @@ -1021,7 +1021,7 @@ 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 SIMPLEX CHAT TÁMOGATÁSA @@ -1038,14 +1038,14 @@ Kiszolgálók mentése Üdvözlőüzenet mp - A profilfrissítés el lesz küldve az Ön partnerei 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 Adatbázis megnyitása… - Leállítás? + Leállítja az alkalmazást? Jelmondat szükséges Privát értesítések Ön meghívta egy partnerét @@ -1061,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 @@ -1070,7 +1070,7 @@ tulajdonos Bekapcsolás %s, %s és %s kapcsolódott - Egyszer használható SimpleX-meghívási hivatkozás + Egyszer használható SimpleX-meghívó Hívások nem sikerült elküldeni KEZELŐFELÜLET SZÍNEI @@ -1087,7 +1087,7 @@ 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 @@ -1095,13 +1095,13 @@ 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. + Feloldja a tag letiltását? + A küldője törölhette a meghívási kérést. Érvénytelen adatbázis-jelmondat Saját SMP-kiszolgálók A kézbesítési jelentések le vannak tiltva Adatbázismappa megnyitása - egy egyszer használható meghívási hivatkozáson keresztül + egy egyszer használható meghívón keresztül Csoportbeállítások megadása a következőn keresztül: %1$s igen @@ -1113,18 +1113,18 @@ Cím megosztása A kiszolgáló QR-kódjának beolvasása Megállítás - Címmegosztás megállítása? + Megállítja a címmegosztást? Csevegési profilok módosítása - Csatlakozáskérés megismétlése? + Megismétli a meghívá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ő @@ -1150,7 +1150,7 @@ 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ó @@ -1175,7 +1175,7 @@ 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 @@ -1186,7 +1186,7 @@ Koppintson a 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 @@ -1202,7 +1202,7 @@ 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? + Menti a beállításokat? Nincsenek felhasználó-azonosítók. A közvetlen üzenetek küldése a tagok között le van tiltva. SOCKS-proxy használata? @@ -1210,7 +1210,7 @@ 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 @@ -1219,12 +1219,12 @@ Védje meg a csevegési profiljait egy jelszóval! Csak a partnere tud eltűnő üzeneteket küldeni. Saját ICE-kiszolgálók - QR-kód beolvasása 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? + 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 @@ -1251,7 +1251,7 @@ 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? + Megállítja a csevegést? Fogadva Beállítva 1 nap Felfedés @@ -1262,7 +1262,7 @@ SimpleX Chat-kiszolgálók használata? Csevegési profil felfedése Videók és fájlok legfeljebb 1GB méretig - TCP kapcsolat időtúllépése + TCP-kapcsolat időtúllépése A(z) %1$s nevű profilja meg lesz osztva. Ön már kapcsolódott a következőhöz: %1$s. A jelenlegi csevegési adatbázis TÖRÖLVE és CSERÉLVE lesz az importáltra!\nEz a művelet nem vonható vissza – profiljai, partnerei, csevegési üzenetei és fájljai véglegesen törölve lesznek. @@ -1271,7 +1271,7 @@ Koppintson ide az új csevegés indításához Várakozás a számítógépre… Az üzenetváltás jövője - Hálózati beállítások módosítása? + 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 @@ -1287,7 +1287,7 @@ a kapcsolattartási címhivatkozáson keresztül a SimpleX a háttérben fut a push értesítések használata helyett.]]> A partnereinek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nVisszavonhatja ezt a partnerkérelmet és eltávolíthatja a partnert (ezt később ismét megpróbálhatja egy új hivatkozással). - A jelszó nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonsági mentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. + A jelmondat nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonsági mentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. A partnerei továbbra is kapcsolódva maradnak. A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát Az adatbázis nem működik megfelelően. Koppintson ide a további információkért @@ -1310,18 +1310,18 @@ Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen 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 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á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í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. Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (hiba: %1$s). A fájl fogadása le fog állni. Ne felejtse el, vagy tárolja biztonságosan – az elveszett jelszót nem lehet visszaállítani! A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését. - Ön egy egyszer használható meghívási hivatkozást osztott meg inkognitóban + Ön egy egyszer használható meghívót osztott meg inkognitóban Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál. Később engedélyezheti a „Beállításokban” Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! @@ -1341,15 +1341,15 @@ Üzenetek formázása a szövegbe szúrt speciális karakterekkel: Megnyitás az alkalmazásban gombra.]]> A csevegési profilja el lesz küldve\na partnere számára - Egy olyan partnerét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a 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 %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 el lesz fogadva, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később! 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 kvantumálló-protokollon keresztül. + Biztonságos kvantumbiztos protokollon keresztül. - legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése. Társítás számítógéppel menüt a hordozható eszköz alkalmazásban és olvassa be a QR-kódot.]]> %s ekkor: %s @@ -1361,14 +1361,14 @@ 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. + Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. (a megosztáshoz a partnerével) Csoportmeghívó elküldve - Átvitel-izoláció módjának frissítése? + Frissíti az átvitel-izoláció módját? Átvitel-izoláció Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak. A csevegési adatbázis nem titkosított – állítson be egy jelmondatot annak védelméhez. - Közvetlen internet kapcsolat használata? + 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 el lesz küldve a csoporttagok számára Később engedélyezheti őket az „Adatvédelem és biztonság” menüben. @@ -1380,11 +1380,11 @@ A partnerei engedélyezhetik a teljes üzenet törlését. A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul – nem az eszközön van tárolva. Ha engedélyezni szeretné a hordozható eszköz-alkalmazás társítását a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, miután engedélyezte azt - A profilja, a partnerei és az elküldött üzenetei az Ön eszközén vannak tárolva. + A profilja, a partnerei és az elküldött üzenetei a saját eszközén vannak tárolva. Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> Ez a karakterlánc nem egy meghívási hivatkozás! Új csevegés indításához - A kapcsolódás már folyamatban van ezen az egyszer használható meghívási hivatkozáson keresztül! + 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! @@ -1399,20 +1399,20 @@ Inkognitóra váltás kapcsolódáskor. Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoportot Ön később törli, akkor nem fogja elveszíteni annak tagjait. Ön csatlakozott ehhez a csoporthoz - %1$s nevű csoporthoz!]]> + %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 el lesz küldve ezen csoporttag számára. + A meghívási kérés el lesz küldve ezen csoporttag számára. Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott. - Már küldött egy kapcsolatkérést ezen a címen keresztül! + Már küldött egy meghívási kérést ezen a címen keresztül! Megoszthatja ezt a SimpleX-címet a partnereivel, hogy kapcsolatba léphessenek vele: %s. - Amikor az emberek kapcsolatot kérnek, Ön elfogadhatja vagy elutasíthatja azokat. + Amikor az emberek meghívót küldenek, Ön elfogadhatja vagy elutasíthatja azokat. Megjelenítendő üzenet beállítása az új tagok számára! - Köszönet a felhasználóknak – hozzájárulás a Weblate-en! + 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 + 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. @@ -1421,7 +1421,7 @@ 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 – hozzájárulás a Weblate-en! + 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. @@ -1434,13 +1434,13 @@ 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! + Köszönet a felhasználóknak a Weblate-en való közreműködésért! A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet. Beállítás a rendszer-hitelesítés helyett. A fogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása a feladó online állapotba kerülése után fejeződik be. A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. Jelmondat mentése a Keystore-ba - Köszönet a felhasználóknak – 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 lesznek elküldve. A második jelölés, amit kihagytunk! ✅ @@ -1454,7 +1454,7 @@ A profil aktiválásához koppintson az ikonra. 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 – az üzenet a címzett(ek) számára valós időben frissül, ahogy Ön beírja az üzenetet @@ -1479,14 +1479,14 @@ 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 + Hiba történt a tartalom megjelenítésekor + Hiba történt az üzenet megjelenítésekor Láthatóvá teheti a SimpleXbeli partnerei számára a „Beállításokban”. Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára. A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás. A beillesztett szöveg nem egy SimpleX-hivatkozás. A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. - Csevegés indítása? + Elindítja a csevegést? Látható előzmények Alkalmazás jelkód Partner hozzáadása @@ -1502,10 +1502,10 @@ 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ási hivatkozásnak a megosztása + Megtartja a fel nem használt meghívót? + Ennek az egyszer használható meghívónak a megosztása Új csevegés - A csevegések betöltése… + Csevegések betöltése… Hivatkozás létrehozása… Vagy QR-kód beolvasása Érvénytelen QR-kód @@ -1553,9 +1553,9 @@ frissített profil %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? + 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 Létrehozva: %s @@ -1576,26 +1576,26 @@ Letiltva az adminisztrátor által letiltotta őt: %s Letiltás - Az összes tag számára letiltja ezt a tagot? + Az összes tag számára letiltja a tagot? %d üzenetet letiltott az adminisztrátor Feloldás Az összes tag számára feloldja a tag letiltását? Ön letiltotta őt: %s - Hiba 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 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 @@ -1610,10 +1610,10 @@ Letöltési hivatkozás részletei Engedélyezés a közvetlen csevegésekben (BÉTA)! Adja meg a jelmondatot - 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: + 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örölve lett, vagy érvénytelen a hivatkozás %s letöltve @@ -1628,7 +1628,7 @@ 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 @@ -1652,7 +1652,7 @@ Átköltöztetés egy másik eszközre Figyelmeztetés: az archívum törölve lesz.]]> Átköltöztetés egy másik eszközről - Kvantumálló 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. @@ -1660,13 +1660,13 @@ 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 kvantumálló 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 kvantumálló titkosí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 kvantumálló titkosítással sérülés utáni titkosságvédelemmel, visszautasítással és feltörés utáni helyreállítással vannak védve.]]> - Hiba az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel. + Ez a csevegés végpontok közötti kvantumbiztos titkosítással védett. + végpontok közötti titkosítással, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.]]> + végpontok közötti kvantumbiztos titkosítással, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.]]> + Hiba történt az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel. Keresse meg ezt az engedélyt az Android beállításaiban, és adja meg kézzel. Engedélyezés a beállításokban Engedély(ek) megadása a hívások kezdeményezéséhez @@ -1677,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 @@ -1744,13 +1744,13 @@ Használjon privát útválasztást ismeretlen kiszolgálókkal. Mindig használjon privát útválasztást. Üzenet-útválasztási mód - Közvetlen üzenetküldés, ha az IP-cím védett és 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. + NE küldjön üzeneteket közvetlenül, még akkor sem, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára. FÁJLOK IP-cím védelme @@ -1797,7 +1797,7 @@ 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 Üzenetsorbaállítási információ nincs @@ -1834,17 +1834,17 @@ Kapcsolódás Hibák Függőben - Statisztikagyűjtés kezdete: %s.\nAz összes adat privát módon van tárolva az Ön 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? + 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 @@ -1892,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 @@ -1932,9 +1932,9 @@ 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. + 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 csatlakozni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. + 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 @@ -1958,7 +1958,7 @@ 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 @@ -1999,7 +1999,7 @@ Mentés és újrakapcsolódás Használja az alkalmazást egy kézzel. A partnerek archiválása a későbbi csevegéshez. - TCP kapcsolat + TCP-kapcsolat Az exportált archívumot elmentheti. Tippek visszaállítása Csevegési lista átváltása: @@ -2016,10 +2016,10 @@ Új üzenet Érvénytelen hivatkozás Ellenőrizze, hogy a SimpleX-hivatkozás helyes-e. - Hiba a profilváltáskor + Hiba történt a profilváltáskor A kapcsolata át lett helyezve ide: %s, de egy váratlan hiba történt a profilra való átirányításkor. Az üzenetek törölve lesznek – ez a művelet nem vonható vissza! - Archívum eltávolítása? + 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 @@ -2031,14 +2031,14 @@ Jelszó Felhasználónév A hitelesítőadatai titkosítatlanul is elküldhetők. - Hiba a proxy mentésekor + Hiba történt a proxy mentésekor Győződjön meg arról, hogy a proxy konfigurációja helyes. Proxyhitelesítés Véletlenszerű hitelesítőadatok használata %1$d egyéb fájlhiba. Nincs mit továbbítani! %1$d fájl letöltése még folyamatban van. - %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. @@ -2046,13 +2046,13 @@ Ü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? + 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 @@ -2066,19 +2066,19 @@ Továbbfejlesztett hívásélmény Továbbfejlesztett üzenetdátumok. Továbbfejlesztett felhasználói élmény - Testre szabható ü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ási 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 + Hiba történt a kiszolgálók mentésekor Nincsenek üzenet-kiszolgálók. Nincsenek üzenetfogadási kiszolgálók. Nincsenek média- és fájlkiszolgálók. A(z) %s nevű csevegési profilhoz: - Cím vagy egyszer használható meghívási 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 @@ -2088,29 +2088,29 @@ Nincsenek fájlküldő-kiszolgálók. Nincsenek fájlfogadási kiszolgálók. Hibák a kiszolgálók konfigurációjában. - Hiba 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 partnere 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ö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ási hivatkozás megosztása egy baráttal - egyetlen partnerrel használható – személyesen vagy bármilyen üzenetváltón keresztül megosztható.]]> + 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ási hivatkozás biztonságosan megosztható bármilyen üzenetváltó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. 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ási hivatkozás? - Egyszer használható meghívási hivatkozás létrehozása - Kiszolgáló-üzemeltetők - Hálózati üzemeltetők + 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 az Ön partnere 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. - Jelölje ki a használni kívánt hálózati üzemeltetőket. + 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. @@ -2119,18 +2119,18 @@ Folytatás Feltételek felülvizsgálata Elfogadott feltételek - A feltételek automatikusan el lesznek fogadva az engedélyezett üzemeltetők számára: %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 kiszolgáló - Hálózati üzemeltető + Hálózatüzemeltető Weboldal - Feltételek elfogadva: %s. - A feltételek ekkor lesznek elfogadva: %s. + Feltételek elfogadásának ideje: %s. + A feltételek el lesznek elfogadva a következő időpontban: %s. Kiszolgálók használata %s használata - A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket a következő 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.]]> @@ -2149,17 +2149,17 @@ Hozzáadott média- és fájlkiszolgálók Feltételek megnyitása Módosítások megtekintése - Hiba a kiszolgáló frissítésekor + 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 + A Flux kiszolgálókat engedélyezheti a beállításokban, a „Hálózat és kiszolgálók” menüben, a metaadatok jobb védelme érdekében. Alkalmazás-eszköztárak a metaadatok jobb védelme érdekében. Javított csevegési navigáció @@ -2172,12 +2172,12 @@ A küldéshez másolhatja és csökkentheti az üzenet méretét. Adja hozzá a munkatársait a beszélgetésekhez. Üzleti cím - végpontok közötti titkosítással, a közvetlen üzenetek továbbá kvantumálló 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 @@ -2185,7 +2185,7 @@ 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! - Csevegés törlése? + Törli a csevegést? Csevegés elhagyása Csak a csevegés tulajdonosai módosíthatják a csevegési beállításokat. Könnyen elérhető csevegési eszköztár @@ -2195,7 +2195,7 @@ 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. @@ -2205,7 +2205,7 @@ 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ő meghívási kérelem Az üzemeltetőkről A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba. A titkosítás újraegyeztetése folyamatban van. @@ -2213,7 +2213,7 @@ Javítás Kapcsolat javítása? Naplózás engedélyezése - Hiba az adatbázis mentésekor + 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 @@ -2225,9 +2225,9 @@ Lista hozzáadása Összes Hozzáadás listához - Hiba a csevegési lista létrehozásakor - Hiba a csevegési lista betöltésekor - Hiba a csevegési lista frissítésekor + 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 @@ -2238,20 +2238,20 @@ 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 - Lista törlése? + Törli a listát? Szerkesztés - Listanév… + 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 a jelentés létrehozásakor - Hiba a beállítások mentésekor + 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 - Jelentés archiválása? + Archiválja a jelentést? archivált jelentés moderátor Közösségi irányelvek megsértése @@ -2274,7 +2274,6 @@ Jelentések %s által archivált jelentés %d jelentés - Tagok archivált jelentései Kéretlen tartalom A tartalom sérti a használati feltételeket A kapcsolat le van tiltva @@ -2285,15 +2284,15 @@ Hivatkozás megnyitása Hivatkozás megnyitása a csevegési listából Nem - Webhivatkozás megnyitása? + Megnyitja a webhivatkozást? Csevegés nevének beállítása… - Automatikus üzenettörlés letiltá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 Ön eszközéről. - Automatikus üzenettörlés módosítása? + Csevegési üzenetek törlése a saját eszközéről. + Módosítja az automatikus üzenettörlést? Ez a művelet nem vonható vissza – a kijelölt üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből. A következő TCP-port használata, amikor nincs port megadva: %1$s. TCP-port az üzenetváltáshoz @@ -2303,8 +2302,8 @@ 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. - Az összes jelentés archiválása? - %d jelentés archiválá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 @@ -2326,4 +2325,33 @@ Csevegések listákba szervezése elutasítva elutasítva + A jelmondat nem olvasható a Keystore-ban, ezért kézzel szükséges megadni. Ez az alkalmazással nem kompatibilis rendszerfrissítés után történhetett meg. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. + függőben + jóváhagyásra vár + Hiba történt az adatbázis-jelmondat olvasásakor + A jelmondat nem olvasható a Keystore-ban. Ez az alkalmazással nem kompatibilis rendszerfrissítés után történhetett meg. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. + Frissített feltételek + A tagok el lesznek távolítva a csoportból – ez a művelet nem vonható vissza! + Eltávolítja a tagokat? + Az összes tag számára letiltja ezeket a tagokat? + A tagok el lesznek távolítva a csevegésből – ez a művelet nem vonható vissza! + Az összes tag számára feloldja a tagok letiltását? + Ezen tagok összes új üzenete el lesz rejtve! + A tagok összes üzenete meg fog jelenni! + moderátorok + Elfogadás + A SimpleX Chat használatával Ön elfogadja, hogy:\n- csak elfogadott tartalmakat tesz közzé a nyilvános csoportokban.\n- tiszteletben tartja a többi felhasználót, és nem küld kéretlen tartalmat senkinek. + Adatvédelmi szabályzat és felhasználási feltételek. + A privát csevegések, a csoportok és a partnerek nem érhetők el a szerver üzemeltetői számára. + Kiszolgálóüzemeltetők beállítása + Nem támogatott kapcsolattartási hivatkozás + Rövid hivatkozások használata (béta) + Rövid hivatkozás + Teljes hivatkozás + Ez a hivatkozás újabb alkalmazásverziót igényel. Frissítse az alkalmazást vagy kérjen egy kompatibilis hivatkozást a partnerétől. + SimpleX-csatornahivatkozás + Összes kiszolgáló + Kikapcsolva + Előre beállított kiszolgálók + A 443-as TCP-port használata kizárólag az előre beállított kiszolgálokhoz. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/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_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/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 14fb1ad156..a269149e99 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -1696,7 +1696,7 @@ negosiasi ulang enkripsi diperbolehkan negosiasi ulang enkripsi diperlukan negosiasi ulang enkripsi diizinkan untuk %s - Aktifkan flux + Aktifkan Flux di pengaturan Jaringan dan server untuk privasi metadata yang lebih baik. untuk privasi metadata lebih baik. Navigasi obrolan ditingkatkan Galat @@ -1788,7 +1788,7 @@ 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 pulihkan data aplikasi menggunakan alat cadangan. Jika tidak, silakan hubungi pengembang. + 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 @@ -2250,7 +2250,6 @@ Semua 1 laporan Arsip - Laporan anggota arsip Alasan lain laporan arsip oleh %s Arsip laporan @@ -2317,4 +2316,47 @@ Laporkan pesan dilarang di grup ini. Dilarang laporkan pesan ke moderator. Arsipkan %d laporan? + Gagal membaca frasa sandi basis data + Privasi dan keamanan lebih baik + Semua pesan baru dari anggota ini akan disembunyikan! + Blokir anggota untuk semua? + Dapat notifikasi saat disebut. + Kirim pesan lebih cepat. + Jangan lewatkan pesan penting. + Kinerja grup yang lebih baik + Hapus grup lebih cepat. + Syarat diperbarui + Anggota akan dihapus dari obrolan - hal ini tidak dapat dibatalkan! + Hapus anggota? + ditolak + tertunda + menunggu persetujuan + Buka blokir anggota untuk semua? + Kirim laporan pribadi + Bantu admin memoderasi grup. + Atur pesan kedaluwarsa obrolan. + Nama berkas media pribadi. + ditolak + Anggota akan dihapus dari grup - tindakan ini tidak dapat dibatalkan! + moderator + Pesan dari anggota ini akan ditampilkan! + Sebutkan anggota 👋 + Atur obrolan ke dalam daftar + Frasa sandi di Keystore tidak dapat dibaca. Hal ini mungkin terjadi setelah pembaruan sistem yang tidak kompatibel dengan aplikasi. Jika tidak demikian, silakan hubungi pengembang. + Terima + Dengan menggunakan SimpleX Chat, Anda setuju untuk:\n- hanya mengirim konten legal di grup publik.\n- hormati pengguna lain – tidak ada spam. + Konfigurasikan operator server + Kebijakan privasi dan ketentuan penggunaan. + Obrolan pribadi, grup, dan kontak Anda tidak dapat diakses oleh operator server. + Frasa sandi di Keystore tidak dapat dibaca, silakan masukkan secara manual. Hal ini mungkin terjadi setelah pembaruan sistem yang tidak kompatibel dengan aplikasi. Jika tidak demikian, silakan hubungi pengembang. + Gunakan port TCP 443 hanya untuk presetel server. + Semua server + Mati + Presetel server + Tautan lengkap + Tautan ini perlu versi aplikasi yang baru. Harap perbarui aplikasi atau minta kontak untuk kirim tautan kompatibel. + Tautan saluran SimpleX + Gunakan tautan singkat (BETA) + Tautan koneksi tidak didukung + Tautan singkat diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index a1bf6014dc..6c086835ea 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -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 @@ -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. @@ -1069,7 +1069,7 @@ \nPuò accadere a causa di qualche bug o quando la connessione è compromessa. %1$d messaggi non decifrati. Hash del messaggio errato - L\'hash del messaggio precedente è diverso.\" + L\'hash del messaggio precedente è diverso. Si prega di segnalarlo agli sviluppatori. L\'invio del file verrà interrotto. Ferma file @@ -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 @@ -2177,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 @@ -2307,7 +2307,6 @@ Archivia la segnalazione Elimina la segnalazione Segnalazioni - Segnalazioni dei membri archiviate 1 segnalazione segnalazione archiviata da %s %d segnalazioni @@ -2354,7 +2353,7 @@ Ricevi una notifica quando menzionato. Aiuta gli amministratori a moderare i loro gruppi. Menziona i membri 👋 - Privacy e sicurezza migliorate + Privacy e sicurezza migliori Invio dei messaggi più veloce. Prestazioni dei gruppi migliorate Organizza le chat in elenchi @@ -2363,4 +2362,33 @@ Nomi privati dei file multimediali. rifiutato rifiutato + Errore di lettura della password del database + in attesa di approvazione + in attesa + Condizioni aggiornate + La password nell\'archivio chiavi non può essere letta. Potrebbe essere successo dopo un aggiornamento di sistema incompatibile con l\'app. In caso contrario, contatta gli sviluppatori. + La password nell\'archivio chiavi non può essere letta, inseriscila a mano. Potrebbe essere successo dopo un aggiornamento di sistema incompatibile con l\'app. In caso contrario, contatta gli sviluppatori. + I membri verranno rimossi dalla chat, non è reversibile! + I membri verranno rimossi dal gruppo, non è reversibile! + Rimuovere i membri? + I messaggi di questi membri verranno mostrati! + Sbloccare i membri per tutti? + Bloccare i membri per tutti? + moderatori + Tutti i nuovi messaggi di questi membri verranno nascosti! + Usando SimpleX Chat accetti di:\n- inviare solo contenuto legale nei gruppi pubblici.\n- rispettare gli altri utenti - niente spam. + Le chat private, i gruppi e i tuoi contatti non sono accessibili agli operatori dei server. + Accetta + Configura gli operatori dei server + Informativa sulla privacy e condizioni d\'uso. + Questo link richiede una versione più recente dell\'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile. + Link completo + Link breve + Link del canale SimpleX + Link di connessione non supportato + Usa link brevi (BETA) + Tutti i server + Off + Server preimpostati + Usa la porta TCP 443 solo per i server preimpostati. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index ab78cf51f3..1103e3a0e6 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,14 @@ הזמן לקבוצה הזמן חברי קבוצה מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה. - מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו. + מחיקת הודעות בלתי הפיכה אסורה. להצטרף בתור %s זה מאפשר חיבורים אנונימיים רבים ללא שום נתונים משותפים ביניהם בפרופיל צ׳אט יחיד. זה יכול לקרות כאשר: \n1. פג תוקפן של ההודעות בלקוח השולח לאחר 2 ימים או בשרת לאחר 30 ימים. \n2. פיענוח הצפנת הודעה נכשל, מכיוון שאתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים. \n3. החיבור נפגע. - ניתן לשנות זאת מאוחר יותר באמצעות ההגדרות. + איך זה משפיע על הסוללה זה יכול לקרות כאשר אתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים. להצטרף לקבוצה\? הצטרף @@ -608,7 +608,7 @@ קישור הזמנה חד־פעמי מרקדאון בהודעות רשת ושרתים - הגדרות רשת + הגדרות מתקדמות ארכיון מסד נתונים חדש הודעות חבר קבוצה @@ -690,7 +690,7 @@ תגובות אמוג׳י להודעות אסורות בקבוצה זו. אפשר לאנשי הקשר להוסיף תגובות אמוג׳י להודעות. אפשר תגובות אמוג׳י להודעות רק אם איש הקשר מאפשר אותן. - חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות. + משתמשים יכולים להוסיף תגובות אמוג׳י להודעות. רק אתם יכולים להוסיף תגובות אמוג׳י להודעות. רק איש הקשר שלכם יכול להוסיף תגובות אמוג׳י להודעות. פתח @@ -737,7 +737,7 @@ שרת מוגדר מראש פרטיות מוגדרת מחדש אנשים יכולים להתחבר אליכם רק דרך הקישורים שאתם משתפים. - פרוטוקול וקוד פתוחים – כל אחד יכול להריץ את השרתים. + כל אחד יכול לארח שרתים. תקופתי נא להזין את הסיסמה הקודמת לאחר שחזור גיבוי מסד הנתונים, לא ניתן לבטל פעולה זו. לאסור מחיקה בלתי הפיכה של הודעות. @@ -746,7 +746,7 @@ אנא בידקו את חיבור האינטרנט שלכם עם %1$s ונסו שוב. ייתכן שטביעת האצבע של התעודה בכתובת השרת שגויה פתיחת מסוף צ׳אט - פתיחת פרופילי צ׳אט + שנה פרופילי צ׳אט ממתין כתובת שרת מוגדר מראש סיסמה להצגה @@ -1016,7 +1016,7 @@ יותר מדי תמונות! תודה שהתקנתם את SimpleX Chat! קישור זה אינו קישור חיבור תקין! - צבעי ערכת נושא + צבעי ממשק התפקיד ישתנה ל־"%s". החבר יקבל הזמנה חדשה. השרתים לחיבורים חדשים של פרופיל הצ׳אט הנוכחי שלך הפלטפורמה הראשונה ללא כל מזהי משתמש - פרטית בעיצובה. @@ -1054,7 +1054,7 @@ שדרג ופתח צ׳אט כדי להגן על אזור הזמן, קובצי תמונה/קול משתמשים ב־UTC. העלה קובץ - שירות רקע SimpleX – הוא משתמש בכמה אחוזים מהסוללה ביום.]]> + SimpleX רץ ברקע במקום להשתמש בpush notifications.]]> כדי לקבל התראות, יש להזין את סיסמת מסד הנתונים בטל נעילה שליחה לא מורשית @@ -1239,15 +1239,15 @@ כיבוי אפליקציה אפשר לשלוח קבצים ומדיה. - מועדף + הוסף למועדפים קבצים ומדיה אין צ\'אטים מסוננים לכבות\? קבצים ומדיה אסורים! - קבצים ומדיה אסורים בקבוצה זו. - חברי הקבוצה יכולים לשלוח קבצים ומדיה. + קבצים ומדיה אסורים. + משתמשים יכולים לשלוח קבצים ומדיה. איתחול - שנוא + הסר מהמועדפים כבוי קו חוצה לאסור שליחת קבצים ומדיה. @@ -1528,8 +1528,8 @@ הצג קריאות API איטיות אפשרויות למפתח צור פרופיל - ו %d שאר האירועים - הגדר כתובת איש קשר חדש + בנוסף ל- %d אירועים אחרים + איש הקשר הגדיר כתובת חדשה לחץ לחיבור דפדפן האינטרנט המוגדר כברירת מחדל נדרש לשיחות. אנא הגדר דפדפן ברירת מחדל במערכת, ושתף מידע נוסף עם המפתחים. השיחה הזו מוגנת באמצעות הצפנה קצה-אל-קצה. @@ -1609,7 +1609,7 @@ החיבור עצר נתיב קובץ לא חוקי שיתפת נתיב קובץ לא חוקי. דווח על הבעיה למפתחי האפליקציה. - %1$d הודעות שנערכו על ידי %2$s + %1$d הודעות נחסמו על ידי %2$s %d הודעות סומנו כנמחקות האם לחזור על בקשת החיבור? חסום @@ -1632,13 +1632,13 @@ טעינה של הקובץ שימוש ממחשב שולחני חסומים %s - מחק איש קשר + איש קשר נמחק %d אירועי קבוצה %s, %s ו-%d חברים איש הקשר %1$s השתנה ל-%2$s כתובת איש קשר הוסרה תמונת פרופיל הוסרה - הגדר תמונת פרופיל חדשה + הגדיר תמונת פרופיל חדשה עדכן פרופיל מצב לא ידוע נוצר ב @@ -1722,7 +1722,7 @@ מרובע, עיגול, או כל דבר ביניהם העבר ושמור הודעות מתי שמתחבר שחיות קוליות ווידאו. - לא מצליח לשלוח הודעה + לא ניתן לשלוח הודעה הודעות קוליות לא מאופשרות שגיאת קובץ זמני קבצים ומדיה לא מאופשרים @@ -1748,7 +1748,7 @@ מצב הקובץ:%s ריק כהה - מצב צבעוני + ערכת נושא שחור בהיר אפס צבע @@ -1767,8 +1767,8 @@ הגדר ערכת נושא ברירת מחדל אפס ערכת נושא למשתמש החל ל - מצב כל הצבעים - חברי הקבוצה יכולים לשלוח קישורי SimpleX + ערכת נושא + משתמשים יכולים לשלוח קישורי SimpleXצ עשה שהצאט\'ים שלך יראו אחרת! הגדרות רשת הקישור הזה שומש כבר במכשיר אחר, אנא צור קישור חדש במחשב. @@ -1918,4 +1918,177 @@ שרת XFTP חלש אנשי קשר בארכיון + דיווח בארכיון + הפרה של הנחיות קהילתיות + %1$d שגיאת קבצים:\n%2$s + %1$d הקבצים עדיין בהורדה. + הסכם לתנאים + %1$d ההורדה של הקובץ/ים עדיין לא הסתיימה. + התנאים המקובלים עלי + שנה + סיבה אחרת + כתובת עסקית + שיפור בסידור של הודעות לפי תאריכים. + שיפור ביצועים לקבוצות + התקשר + לא ניתן להתקשר לחבר קבוצה + %s.]]> + להעביר דיווח לארכיון? + מסד נתונים של הצא\'טים + בדוק עבור הודעות חדשות כל 10 דקות + אישרת את תנאי השימוש ב:%s. + הצ\'אט יימחק עבור כל החברים - לא ניתן לבטל את זה! + לחץ על כפתור מידע ליד שדה כתובת כדי לאפשר שימוש במיקרופון. + %s.]]> + %s.]]> + העבר אנשי קשר לארכיון לשוחח מאוחר יותר + לשנות את מחיקת ההודעה האוטומטית? + הצ\'אט כבר קיים! + הכל + %1$d שגיאה/ות קובץ אחר/ות. + %1$d הקובץ/ים נכשל/ו בהורדה. + כל ההודעות החדשות מחברים אלו יוסתרו! + לחסום את חברי הקבוצה לכולם? + הדיווח הועבר לארכיון ע\"י %s + עם איש קשר אחד בלבד - שתף באופן אישי או באמצעות כל מסנג\'ר.]]> + טשטוש בשביל שיפור הפרטיות. + דיווח 1 + כל הצ\'אטים יוסרו מהרשימת %s, והרשימה תימחק + שנה רשימה + לאפשר שיחות? + הצ\'אט יימחק עבורך - אי אפשר לבטל את זה! + שיחות לא מורשות! + שאל + %s.]]> + מכשירי שיואמי: אנא תאפשר הפעלה אוטומטית בהגדרות הטלפון שלך כדי שההתראות על הודעות חדשות יפעלו.]]> + %1$s ההודעות לא הועברו. + מוצפנים מקצה לקצה, עם אבטחה פוסט-קוונטית בהודעות ישירות.]]> + טישטוש + עסקי + %s, קבל את תנאי השימוש.]]> + כל הדיווחים אצלך יועברו לארכיון. + להעביר לארכיון %d דיווחים? + להעביר לארכיון את כל הדיווחים? + העבר דיווח לארכיון + דיווחים בארכיון + אפשר לדווח על הודעות למנהלים. + %1$d הקובץ/ים נמחקו. + %s.]]> + המסד נתונים יוצא בהצלחה + %1$s כבר באנשי קשר.]]> + לא ניתן לשלוח הודעה לחבר קבוצה + קבל הזמנה + על המפעילים + לא ניתן להתקשר לאיש קשר + שיפור לשיחות + שנה את הסדר + ארכיון + אני מסכים + שיפור בפרטיות ובאבטחה + אבטחה יותר טובה✅ + ממשק משתמש יותר נוח + "גרסאת שרת היעד %1$s אינה תואמת עם שרת ההעברה %2$s." + מחק עד 20 הודעות בבת אחת. + הודעות ישירות בין חברים חסומות. + השבת מחיקת הודעות + אל תשתמש בתעודות עם פרוקסי. + ירד + %d דיווחים + יורד %s (%s) + השבת מחיקת הודעות אוטומטית? + כפילויות + תנאי שימוש + מחק בלי להתריע + הורדה + הודעות ישירות בין חברים אסורות בצ\'אט זה. + אל תחמיץ הודעות חשובות. + הורד גרסאות חדשות מ GitHub. + פרופיל לא הולם + לפרטיות מטא דאטא טובה יותר. + אפשר Flux בהגדרות רשת ושרתים בשביל לשפר את הפרטיות של המטא דאטא + מעביר %1$s הודעות + שגיאה בקריאת משפט-סיסמה של מסד נתונים + משתמשים יכולים לדווח על הודעות לאחראי תוכן + שגיאת אתחול ב WebView, וודא שיש לך WebView מותקן והוא תותך בארכיטקטורה arm64\nשגיאה: %s + שגיאה בשמירת פרוקסי + לפרופיל צ\'אט %s: + למסלול פרטי + שגיאה בשמירת ההגדרות + שגיאה בהעברת ההודעות + שגיאה בהחלפת פרופיל + אפשר לוגים + איך זה משפר את הפרטיות + שיפור בגלילה בצ\'אט + תוכן לא הולם + שגיאה בקבלת תנאי שימוש + שגיאות בתצורת השרתים. + שרת ההעברות %1$s לא הצליח להתחבר לשרת היעד %2$s.נסה שוב במועד מאוחר יותר. + דיווחים מחברי הקבוצה + שרתי קבצים ומדיה + קישור לא תקין + שגיאה ביצירת רשימת צא\'טים חדשה + שגיאת עדכון רשימת צ\'אטים + שגיאה בטעינת רשימות הצא\'טים + ערוך + ודא שתצורת ה- proxy נכונה. + הזמן + זה מגן על כתובת ה- IP והחיבורים שלך. + עזוב צ\'אט + תקן + לתקן חיבור? + מחיקה מהירה יותר של קבוצות. + שליחת הודעות מהירה יותר. + העבר הודעות ללא קבצים? + הזמן לצ\'אט + שרת ההעברות: %1$s\nשגיאת שרת יעד: %2$s + שרת ההעברות: %1$s\nשגיאה: %2$s + למדיה חברתית + הגדל את גודל הגופן. + רשימה + מעודפים + קבוצות + שם הרשימה.. + שם הרשימה והאמוג\'י צריכים להיות שונים משאר הרשימות. + שמור שיחה + הזמן + ממשק בשפה הליטאית + הקובץ נחסם ע\"י מפעיל השרת:\n%1$s. + בשבילי + לכל האחראי תוכן + העבר הודעות… + שגיאה בשמירת המסד נתונים + שגיאה בהוספת שרת + שגיאת עדכון שם שרת + להעביר %1$s הודעה/ות? + שגיאה בשליחת הדיווח + שגיאה בשמירת שרתים + לעזוב את הצ\'אט? + העבר עד 20 הודעות בבת אחת. + קבל התראה כאשר מתייגים אותך + עזור לאחראי תוכן בדיווחים על תוכן בעייתי בקבוצות + הוסף רשימה + הדגשה נוספת 2 + בשימוש שלך ב- SimpleX Chat אתה מסכים ל:\n\n- לשלוח רק תוכן חוקי בקבוצות ציבוריות.\n\n- לכבד את שאר המשתמשים - לא לשלוח ספאם. + %s.]]> + %s.]]> + הוסף חברים + שרתי הודעות שנוספו + שרתי מדיה וקבצים שנוספו + הוסף לרשימה + חיבורים פעילים + הוסף את חברי הצוות שלך לשיחות. + האפליקציה תמיד רצה ברקע + הגדרות כתובת + סרגל הכלים של האפליקציה + סשן לאפליקציה + כתובת או קישור חד פעמי? + הוסף חברי צוות + %s.]]> + צ\'אט + צא\'טים עסקיים + "הזבל נמחק" + אנשי קשר + איש הקשר יימחק - לא ניתן לבטל זאת! + איש הקשר נמחק. + ספאם diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index 8a92168d26..21c04b8473 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -1993,4 +1993,19 @@ 利用条件の承諾 %s の利用条件に承諾しています。]]> 後で作成する場合はメニューから「SimpleXのアドレスを作成」を選択してください。 + 運営者について + 1年 + 1 件のレポート + モデレーターにメッセージを報告することを許可する + これらのメンバーからの新しいメッセージはすべて非表示になります! + リストに追加 + アドレス設定 + チームのメンバーを会話に追加する + 追加されたメッセージサーバー + アドレスか使い捨てのリンク? + 友達を追加 + チームメンバーを追加 + リストを追加 + すべて + ワンタイムリンクを生成 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index f69ec96d7c..651d32518f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -1496,7 +1496,6 @@ 1 보고서 리스트 추가 전부 - 보관된 멤버 리포트 목록 %s의 모든 차트가 제거되었고, 목록도 삭제되었습니다. 1 년 리스트에 추가하기 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 997979d039..d07cb6db39 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -2183,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 @@ -2281,7 +2281,7 @@ Lijst wijzigen Wijzig volgorde ‐Fout bij het opslaan van instellingen - Fout bij maken van rapport + Fout bij het rapporteren Archief Schending van de communityrichtlijnen Een andere reden @@ -2295,7 +2295,7 @@ Alleen jij en moderators zien het Spam Rapport archiveren? - Rapport + rapporteren Reden melding? Het rapport wordt voor u gearchiveerd. Anders melden: alleen groepsmoderators kunnen het zien. @@ -2305,7 +2305,6 @@ Rapport verwijderen gearchiveerd rapport door %s 1 rapport - Gearchiveerde ledenrapporten %d rapporten Ledenrapporten Inhoud schendt de gebruiksvoorwaarden @@ -2342,22 +2341,47 @@ Voor mij Leden kunnen berichten melden bij moderators. Het melden van berichten in deze groep is niet toegestaan - Rapport: %s + 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. + 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. - Verstuur rapporten privé + Rapporteer privé Sneller verzenden van berichten. afgewezen afgewezen + Fout bij het lezen van database wachtwoord + Alle nieuwe berichten van deze leden worden verborgen! + Leden voor iedereen blokkeren? + Leden worden uit de chat verwijderd. Dit kan niet ongedaan worden gemaakt! + Leden worden uit de groep verwijderd. Dit kan niet ongedaan worden gemaakt! + Leden voor iedereen deblokkeren? + Berichten van deze leden worden getoond! + moderatoren + Wachtwoord in Keystore kan niet worden gelezen, voer deze handmatig in. Dit kan zijn gebeurd na een systeemupdate die niet compatibel is met de app. Als dit niet het geval is, neem dan contact op met de ontwikkelaars. + in afwachting van goedkeuring + Leden verwijderen? + Bijgewerkte voorwaarden + Wachtwoord in Keystore kan niet worden gelezen. Dit kan zijn gebeurd na een systeemupdate die niet compatibel is met de app. Als dit niet het geval is, neem dan contact op met de ontwikkelaars. + in behandeling + Accepteer + Door SimpleX Chat te gebruiken, gaat u ermee akkoord:\n- alleen legale content te versturen in openbare groepen.\n- andere gebruikers te respecteren – geen spam. + SimpleX channel link + Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen. + Volledige link + Niet-ondersteunde verbindingslink + Gebruik korte links (BETA) + Korte link + Serveroperators configureren + Privacybeleid en gebruiksvoorwaarden. + Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 8d31873b2d..5102c98563 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -2131,7 +2131,6 @@ Wszystkie Biznesy raport - Zarchiwizowane raporty członków Zapytaj %s.]]> %s.]]> 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 ac3deb84db..a0cce488af 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -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 @@ -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 @@ -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 @@ -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 @@ -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,7 +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 @@ -2055,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 @@ -2065,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. @@ -2121,9 +2121,265 @@ Melhor experiência do usuário %1$s.]]> Arquivar - Pergunte + Perguntar Desfoque - Denúncias arquivadas de membros Endereço comercial denúncia arquivada + Deletar chat + O texto das condições atuais não pôde ser carregado, você pode revisar as condições por meio deste link: + %s.]]> + Formato de mensagem personalizável. + Envio de mensagens mais rápido. + Checar mensagens a cada 10 minutos + Todas novas mensagens destes membros serão ocultadas + Erro ao atualizar servidor + Permitir denunciar mensagens aos moderadores. + Melhorias de privacidade e segurança + Não perca mensagens importantes. + Chat já existente! + Ativar logs + Bloquear membros para todos? + Deletar ou moderar até 200 mensagens. + %s.]]> + %s.]]> + Mensagens diretas entre membros são proibidas neste chat. + Melhor desempenho de grupos + com criptografia de ponta-a-ponta, e com segurança pós-quântica em mensagens diretas.]]> + Chat será deletado para você - essa ação não pode ser desfeita! + Condições aceitas em: %s. + Mensagens diretas entre membros são proibidas. + %s.]]> + Alterar ordem + Erro ao ler a senha do banco de dados + Erro ao aceitar condições + Violação das diretrizes da comunidade + Erro ao salvar servidores + %d denúncias + 1 denúncia + com apenas um contato - compartilhe pessoalmente ou por qualquer aplicativo de mensagens.]]> + Erro ao salvar configurações + Criar link único + Reparar + %s, aceite as condições de uso.]]> + O conteúdo viola as condições de uso + Conexão bloqueada + A conexão está bloqueada pelo operador do servidor:\n%1$s. + O arquivo está bloqueado pelo operador do servidor:\n%1$s. + Deletar denúncia + Empresas + Alterar lista + Continuar + Erro ao salvar banco de dados + %s.]]> + Erro ao inicializar o WebView. Certifique-se de que você tenha o WebView instalado e que sua arquitetura suportada seja arm64.\nErro: %s + Alterar exclusão automática de mensagens? + Desativar exclusão automática de mensagens? + Deletar lista? + 1 ano + padrão (%s) + %s.]]> + %s.]]> + As condições serão aceitas automaticamente para operadores habilitados em: %s. + Segurança de conexão + As condições serão aceitas em: %s. + %s.]]> + Condições de uso + Erro ao adicionar servidor + Chats de empresas + para melhoria da privacidade de metadados. + Exclusão mais rápida de grupos. + Erro ao criar lista de chat + Erro ao carregar lista de chats + Erro ao atualizar a lista de chats + Contatos + Favoritos + Criar lista + Editar + Canto + Ativar o Flux nas Configurações de rede e servidores para melhor privacidade de metadados. + Todas denúncias serão arquivadas para você. + Arquivar todas denúncias? + Arquivar %d denúncias? + Arquivar denúncias + Para todos moderadores + Para mim + Deletar mensagens de chat do seu dispositivo. + Excluir chat? + O chat será deletado para todos os membros - essa ação não pode ser desfeita! + Desativar exclusão de mensagens + Renegociação de criptografia em andamento. + Deletar + Clique no botão de informação perto do campo de endereço para permitir usar o microfone. + As condições serão aceitas para operadores habilitados após 30 dias. + Por exemplo, se o seu contato receber mensagens por meio de um servidor SimpleX Chat, seu aplicativo as entregará por meio de um servidor Flux. + A conexão não está pronta. + Erros nas configurações de servidores. + Para o perfil de chat %s: + A conexão requer renegociação de criptografia. + Reparar conexão? + Erro ao criar denúncia + Chat + Seus servidores + aprovação pendente + pendente + Os membros podem denunciar mensagens aos moderadores. + Operadores da rede + Operador + Nenhum serviço de segundo plano + Abrir mudanças + moderadores + Descentralização da rede + Privacidade para seus clientes. + Mencione membros 👋 + Seja notificado quando mencionado. + Ajude os administradores a moderar seus grupos. + Organize os chats em listas + Nomes de arquivos de mídia privados. + Conteúdo inapropriado + Perfil inapropriado + Nenhuma mensagem de servidores. + Nenhuma mensagem + Denúncias de membros + Ou compartilhe em particular + Nenhum chat não lido + Nenhum chat + Notas + Abrir com %s + O nome da lista e o emoji devem ser diferentes para todas as listas. + Novas credenciais SOCKS serão usadas para cada servidor. + Notificações e bateria + As mensagens desses membros serão exibidas! + Silenciar tudo + Para redes sociais + Servidores predefinidos + Abrir links da lista de bate-papo + Abrir web link? + Convidar ao chat + Abrir condições + Nome da lista... + Novas credenciais SOCKS serão usadas toda vez que você iniciar o aplicativo. + Abrir link + Forma da mensagem + moderador + A mensagem é muito grande! + Por favor, reduza o tamanho da mensagem e a envie novamente. + Operador da rede + Para roteamento privado + Aprimorada a navegação de bate-papo + - Abra o chat na primeira mensagem não lida.\n- Pule para mensagens citadas. + Os membros serão removidos do chat. Essa ação não pode ser desfeita! + Said do chat + Os membros serão removidos do grupo. Essa ação não pode ser desfeita! + Nove servidor + Nenhum chat encontrado + As mensagens neste chat nunca serão excluídas. + A frase-senha na Keystore não pôde ser lida. Isso pode ter acontecido após uma atualização do sistema incompatível com o aplicativo. Se não for o caso, entre em contato com os desenvolvedores. + Somente os proprietários do chat podem alterar as preferências. + A frase-senha na Keystore não pôde ser lida, insira-a manualmente. Isso pode ter acontecido após uma atualização do sistema incompatível com o aplicativo. Se não for o caso, entre em contato com os desenvolvedores. + Grupos + Ou importar arquivo compactado + Lista + Reduza o tamanho da mensagem ou remova a mídia e envie novamente. + Como isso ajuda na privacidade + Não + Sair do chat? + O membro será removido do chat - essa ação não pode ser desfeita! + Encaminhe até 20 mensagens de uma vez. + Nenhuma mídia & nenhum arquivo de servidores. + Nenhum servidor para enviar arquivos. + Nenhum servidor para roteamento de mensagens privadas. + Nenhum servidor para receber arquivos. + Nenhum servidor para receber mensagens. + Abra Configurações do Safari / Websites / Microfone, e escolha Permitir para localhost. + Servidor do operador + Nenhum chat na lista %s. + Somente o remetente e os moderadores podem vê-lo. + Somente você e os moderadores podem ver isso + rejeitado + Denunciar + Proibir a denúncia de mensagens aos moderadores. + Denunciar conteúdo: somente os moderadores do grupo poderão ver. + Denunciar perfil de membro: somente moderadores do grupo poderão ver. + Dispositivos móveis remotos + Remover membros? + Denunciar outro: somente os moderadores do grupo poderão ver. + Qual é a razão da denúncia? + Denúncia: %s + rejeitado + É proibido denunciar mensagens neste grupo. + Barra de ferramentas de chat acessível + Denúncias + Operador do servidor alterado. + Definir nome do chat… + Compartilhar o endereço publicamente + Denunciar violação: somente os moderadores do grupo poderão ver. + %s servidores + Protocolos SimpleX analisados pela Trail of Bits. + Enviar denúncias privadas + Defina a expiração de mensagens em chats. + Spam + Spam + Salvar lista + Compartilhe um link único com um amigo + Endereço SimpleX ou link único? + Som silenciado + Alterne entre áudio e vídeo durante a chamada. + Selecione as operadoras de rede a serem utilizadas. + Revisar condições + Endereços SimpleX e links únicos são seguros para compartilhar por meio de qualquer mensageiro. + Operadores do servidor + Servidor adicionado ao operador %s. + riscar + Denunciar spam: somente os moderadores do grupo poderão ver. + Protocolo do servidor alterado. + Compartilhe o endereço do SimpleX nas redes sociais. + Revisar depois + Servidor + O SimpleX Chat e o Flux fizeram um acordo para incluir servidores operados pelo Flux no aplicativo. + conexão solicitada + The role will be changed to %s. Everyone in the chat will be notified. + Transparência + Alterne o perfil de chat para convites únicos. + Desbloquear membros para todos? + Para enviar + Condições atualizadas + O segundo operador predefinido no aplicativo! + Esta mensagem foi excluída ou ainda não foi recebida. + Ver condições atualizadas + Toque em Criar endereço SimpleX no menu para criá-lo mais tarde. + Usar porta TCP %1$s quando nenhuma porta for especificada. + A denúncia será arquivado para você. + Para receber + Esta ação não pode ser desfeita - as mensagens enviadas e recebidas neste chat antes da selecionada serão excluídas. + Para se proteger contra a substituição do seu link, você pode comparar os códigos de segurança dos contatos. + Menções não lidas + Porta TCP para mensagens + Usar porta web + O aplicativo protege sua privacidade usando diferentes operadores em cada conversa. + Quando mais de um operador está ativado, nenhum deles têm metadados para saber quem se comunica com quem. + Sim + Seu perfil de chat será enviado aos membros do chat + Ver condições + Usar para mensagens + Você pode definir o nome da conexão para lembrar com quem o link foi compartilhado. + Você pode configurar servidores nas configurações. + Usar %s + Usar servidores + Website + Você pode mencionar até %1$s membros por mensagem! + Final + Você pode copiar e reduzir o tamanho da mensagem para enviá-la. + Atualização + Você pode configurar operadores em Configurações de rede & servidores. + Usar para arquivos + Você deixará de receber mensagens deste chat. O histórico do chat será preservado. + Para fazer chamadas, permita usar seu microfone. Encerre a chamada e tente ligar novamente. + Os servidores para novos arquivos do seu perfil de chat atual + A conexão atingiu o limite de mensagens não entregues, seu contato pode estar offline. + Mensagens não entregues + Configurar operadores de servidor + Chats privados, grupos e seus contatos não são acessíveis aos operadores de servidor. + Aceitar + Ao usar o SimpleX Chat, você concorda em:\n- enviar apenas conteúdo legal em grupos públicos.\n- respeitar outros usuários – sem spam. + Política de privacidade e condições de uso. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index c9db7de2e6..544ee8af89 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -971,4 +971,4 @@ Todos os perfis Já conectando! Já entrando no grupo! - \ No newline at end of file + 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 1280c67bb0..97742f82a8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -754,7 +754,7 @@ Вы пытаетесь пригласить инкогнито контакт в группу, где Вы используете свой основной профиль Пригласить членов группы - УЧАСТНИКОВ ГРУППЫ: %1$s + %1$s ЧЛЕНОВ ГРУППЫ Вы: %1$s Удалить группу Удалить группу? @@ -2231,7 +2231,7 @@ Прозрачность Децентрализация сети Второй оператор серверов в приложении! - Включить Flux + Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных. для лучшей конфиденциальности метаданных. Улучшенная навигация в разговоре Посмотреть измененные условия @@ -2296,8 +2296,8 @@ Серверы по умолчанию Роль будет изменена на %s. Все участники разговора получат уведомление. Ваш профиль будет отправлен участникам разговора. - %s.]]> - %s.]]> + %s.]]> + %s.]]> Условия использования Дополнительные серверы файлов и медиа Ошибка сохранения сервера @@ -2354,7 +2354,7 @@ Открыто с %s Создать список Добавить в список - Список изменений + Изменить список Сохранить список Имя списка... Исправить соединение? @@ -2367,27 +2367,104 @@ Название списка и эмодзи должны быть разными для всех списков. Пожаловаться Спам - Пожаловаться на спам: это увидит только модератор группы + Пожаловаться на спам: увидят только модераторы группы. Это действие не может быть отмененено - сообщения, отправленные и полученные в этом чате ранее чем выбранное, будут удалены Получайте уведомления от упоминаний. - Жалобы на сообщения запрещены в этой группе. - Пожаловаться на нарушение: это увидит только модератор группы + Сообщения о нарушениях запрещены в этой группе. + Пожаловаться на нарушение: увидят только модераторы группы. Установить имя чата… Улучшенная производительность групп Приватные названия медиафайлов. Спам - Жалобы + Сообщения о нарушениях Непрочитанные упоминания Да Упоминайте участников 👋 Улучшенная приватность и безопасность - Ускоренно удаление групп. - Ускоренна отправка сообщений. + Ускорено удаление групп. + Ускорена отправка сообщений. Помогайте администраторам модерировать их группы. Организуйте чаты в списки - Отправляйте приватные жалобы + Вы можете сообщить о нарушениях Установите время исчезания сообщений в чатах. Вы можете упомянуть до %1$s пользователей в одном сообщении! - Причина жалобы? - Эта жалоба будет арчивирована для вас. + Причина сообщения? + Эта жалоба будет архивирована для вас. + Разрешить отправлять сообщения о нарушениях модераторам. + Содержание нарушает условия использования + Ошибка чтения пароля базы данных + сообщение о нарушении заархивировано %s + Нарушение правил группы + Неприемлемое сообщение + Другая причина + Неприемлемый профиль + %d сообщений о нарушениях + Ошибка создания сообщения о нарушении + Соединение заблокировано + Соединение заблокировано сервером оператора:\n%1$s. + Спросить + Отключить автоматическое удаление сообщений? + Удалить сообщения с вашего устройства. + Отключить удаление сообщений + по умолчанию (%s) + Все сообщения о нарушениях будут заархивированы для вас. + Архивировать все сообщения о нарушениях? + Архивировать %d сообщений о нарушениях? + Для меня + Архивировать сообщение о нарушении + Архивировать сообщения о нарушениях + Удалить сообщение о нарушении + Файл заблокирован оператором сервера:\n%1$s. + Для всех модераторов + 1 сообщение о нарушении + Измененить автоматическое удаление сообщений? + 1 год + Не пропустите важные сообщения. + Ошибка сохранения настроек + заархивированное сообщение о нарушении + архивировать + Архивировать сообщение о нарушении? + Пароль не может быть прочитан из Keystore. Это могло произойти после обновления системы, несовместимого с приложением. Если это не так, обратитесь к разработчикам. + Пароль не может быть прочитан из Keystore, пожалуйста, введите его. Это могло произойти после обновления системы, несовместимого с приложением. Если это не так, обратитесь к разработчикам. + модератор + ожидает утверждения + ожидает + Обновленные условия + Запретить жаловаться модераторам группы. + Члены группы могут пожаловаться модераторам. + Сообщения в этом чате никогда не будут удалены. + Открыть ссылку из списка чатов + Открыть веб-ссылку? + Пожаловаться на профиль: увидят только модераторы группы. + Сообщения о нарушениях + Пожаловаться: увидят только модераторы группы. + Выключить уведомления для всех + Использовать TCP-порт %1$s, когда порт не указан. + Использовать TCP-порт 443 только для серверов по умолчанию. + Все серверы + Серверы по умолчанию + Нет + Использовать веб-порт + Нет + Пожаловаться на сообщение: увидят только модераторы группы. + отклонён + Сообщение о нарушении: %s + TCP-порт для отправки сообщений + Открыть ссылку + отклонён + Только отправитель и модераторы видят это + Только вы и модераторы видят это + Разблокировать членов для всех? + Сообщения от этих членов группы будут показаны! + Все новые сообщения от этих членов группы будут скрыты! + Заблокировать членов для всех? + Члены группы будут удалены - это действие нельзя отменить! + Участники будут удалены из разговора - это действие нельзя отменить! + модераторы + Удалить членов группы? + Принять + Используя SimpleX Chat, Вы согласны:\n- отправлять только законные сообщения в публичных группах.\n- уважать других пользователей – не отправлять спам. + Частные разговоры, группы и Ваши контакты недоступны для операторов серверов. + Настроить операторов серверов + Политика конфиденциальности и условия использования. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 62d3764940..257daec596 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -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) @@ -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 @@ -648,9 +648,9 @@ 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) + Üyeler, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) Grup üyeleri sesli mesaj gönderebilirler. - Bu toplu konuşmada, dosya ve medya yasaklanmıştır. + Dosya ve medya yasaklanmıştır. Grup üyeleri dosya ve medya paylaşabilir. Grup bağlantıları Konuşmada devre dışı bırakıldığında bile @@ -690,7 +690,7 @@ Android Keystore parolayı güvenli bir şekilde saklamak için kullanılır - bildirim hizmetinin çalışmasını sağlar. Karşılama mesajı Karşılama mesajı - Bu grupta sesli mesajlar yasaktır. + Sesli mesajlar yasaktır. Neler yeni %s sürümünde yeni Daha fazla bilgi edinin @@ -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,7 +835,7 @@ Alıcı adresini değiştir Mesaj tepkileri Tercihleriniz - Mesaj tepkileri bu grupta yasaklıdır + Mesaj tepkileri yasaklıdır. Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız. Zaten %1$s e bağlısınız Doğrulanamadınız; lütfen tekrar deneyin. @@ -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,7 +1022,7 @@ Mesaj taslağı Sohbet profillerini parola ile koru! Daha az pil kullanımı - Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir. + Gizliliği korumak için, SimpleX her bir konuşma için farklı bir ID kullanır. Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s). Alıcılar güncellemeleri siz yazdıkça görürler. Bilgilerinizi kullanarak giriş yapın @@ -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 @@ -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ı @@ -2143,7 +2143,6 @@ Raporu arşivle %s tarafından arşivlenen rapor 1 rapor - Arşivlenmiş üye raporları %s.]]> Sor 1 yıl @@ -2159,4 +2158,69 @@ Arşiv Raporu arşivleyelim mi? uçtan uca şifreli olarak gönderilir ve doğrudan mesajlarda kuantum sonrası güvenlik sağlanır.]]> + Veritabanı şifresini okurken hata oluştu + Sohbeti sil + Sohbet silinsin mi? + Bağlantı hazır değil. + Kullanım şartları + daha iyi üstveri gizliliği için. + Sunucuları kaydederken hata oluştu + Şartlar 30 gün sonra etkin operatörler için kabul edilecektir. + Raporu sil + Listeyi değiştir + %s için de geçerli olacaktır.]]> + Üyeler herkes için engellensin mi? + Topluluk kurallarının ihlali + Şartları kabul ederken hata oluştu + Sil + Liste silinsin mi? + Bağlantı güvenliği + İçerik kullanım şartlarını ihlal ediyor + Rapor oluşturulurken hata oluştu + Bağlantı engellendi + Bağlantı sunucu operatörü tarafından engellendi:\n%1$s. + Dosya sunucu operatörü tarafından engellendi:\n%1$s. + %d rapor + Mesaj çok büyük! + Otomatik silinen mesajlar değiştirilsin mi? + Otomatik silinen mesajlar devre dışı bırakılsın mı? + Liste oluştur + Sırayı değiştir + Düzenle + Sosyal medya için + Tek kullanımlık bağlantı oluştur + Örneğin, eğer kişiniz SimpleX Sohbet sunucusundan mesajları alıyorsa, uygulamanız bu mesajları Flux sunucusundan iletecektir. + varsayılan (%s) + Bu üyelerden gelen yeni mesajların hepsi gizlenecektir. + İyileştirilmiş grup performansı + Sohbet zaten var! + %1$s ile bağlısınız.]]> + Konuşma profili %s için: + Üyeler arası doğrudan mesajlaşma yasaklıdır. + Şartlar %s tarihinde etkin operatörler için otomatik olarak kabul edilecektir. + Sohbet mesajlarını cihazınızdan silin. + Her 10 dakikada mesajları kontrol et + Devam et + İyileştirilmiş gizlilik ve güvenlik + Daha hızlı mesaj gönderme. + Bu sohbette üyeler arası doğrudan mesajlaşma yasaklıdır. + Sohbet listesi yüklenirken hata oluştu + Sohbet listesi oluşturulurken hata oluştu + Sohbet listesini güncellerken hata oluştu + Sık kullanılanlar + Konuşmalar + Veritabanını kaydederken hata oluştu + Sohbet sizin için silinecek - bu geri alınamaz! + %s için de geçerli olacaktır.]]> + Bütün moderatörler için + Benim için + Ayarlar kaydedilirken hata oluştu + Günlükleri etkinleştir + Sohbet + Grupların daha hızlı silinmesi. + Sohbet bütün üyeler için silinecek - bu geri alınamaz! + Sunucu eklerken hata oluştu + Onar + Bağlantı onarılsın mı? + Sunucuyu güncellerken hata oluştu diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 3c6566552d..549cb01b63 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -18,7 +18,7 @@ 1-разове посилання Про SimpleX Chat Додавайте сервери, скануючи QR-коди. - Всі чати і повідомлення будуть видалені - цю дію неможливо скасувати! + Усі чати та повідомлення будуть видалені - цю дію неможливо скасувати! Дозволити дзвінки тільки за умови, що ваш контакт дозволяє їх. Дозволити безповоротне видалення повідомлень, тільки якщо ваш контакт дозволяє вам. (24 години) Дозволити голосові повідомлення\? @@ -50,20 +50,20 @@ Дозволити Розширені налаштування мережі Отримувати доступ до серверів через SOCKS-проксі на порті %d? Проксі має бути запущено до активації цієї опції. - Всі ваші контакти залишаться підключеними. - Всі дані застосунку буде видалено. + Усі ваші контакти залишаться підключеними. + Усі дані застосунку буде видалено. Після перезапуску додатка або зміни ключової фрази буде використано сховище ключів Android для безпечного збереження ключової фрази - це дозволить отримувати сповіщення. Дозвольте вашим контактам надсилати голосові повідомлення. Прийняти інкогніто Додати сервер адміністратор Додати привітання - Всі учасники групи залишаться підключеними. + Усі учасники групи залишаться підключеними. Дозвольте вашим контактам надсилати повідомлення, які зникають. - Всі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас. + Усі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас. Версія додатку Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Оновлення профілю буде відправлено вашим контактам. - Всі ваші контакти залишаться підключеними. Оновлення профілю буде відправлено вашим контактам. + Усі ваші контакти залишаться підключеними. Оновлення профілю буде відправлено вашим контактам. Відповісти на виклик Адреса Додати профіль @@ -496,7 +496,7 @@ Забороняйте надсилання повідомлень, які зникають. Забороняйте невідворотне видалення повідомлень. Учасники можуть надсилати голосові повідомлення. - %dm + %dм Нове в %s Самознищуючий пароль Італійський інтерфейс @@ -528,7 +528,7 @@ Встановити на 1 день Забороняйте реакції на повідомлення. Реакції на повідомлення заборонені в цьому чаті. - %ds + %dс хвилини Китайський та іспанський інтерфейс підключення %1$d @@ -717,7 +717,7 @@ Зникає о: %s (поточне) Вилучити учасника - Роль буде змінено на "%s". Всі учасники групи будуть сповіщені. + Роль буде змінено на %s. Усі учасники групи будуть сповіщені. Роль буде змінено на "%s". Учасник отримає нове запрошення. Група Привітальне повідомлення @@ -756,7 +756,7 @@ %d година %d тиждень %d тижні - %dw + %dтиж запропоновано %s: %2s З опційним вітанням. Приховуйте екран додатка в останніх програмах. @@ -876,7 +876,7 @@ Надіслано о: %s Видалено о: %s %s (поточне) - %dh + %dч %d день %d днів скасовано %s @@ -985,7 +985,7 @@ Змінити роль Ви все ще отримуватимете дзвінки та сповіщення від приглушених профілів, коли вони активні. %d місяці - %dmth + %dміс Надіслані повідомлення будуть видалені після встановленого часу. Відкриття бази даних… Помилка встановлення адреси @@ -1133,7 +1133,7 @@ Підключитися Щоб показати ваш схований профіль, введіть повний пароль у поле пошуку на сторінці Ваші профілі. Підтвердити пароль - %dd + %dд Захистіть свої чат-профілі паролем! Помилка дешифрування Сервер вимагає авторизації для завантаження, перевірте пароль @@ -1226,7 +1226,7 @@ Змінити ключову фразу бази даних? Не вдається отримати доступ до сховища ключів для збереження пароля бази даних Зберегти ключову фразу і відкрити чат - Ключова фраза не знайдена в сховищі ключів, будь ласка, введіть її вручну. Це може трапитися, якщо ви відновили дані додатка за допомогою інструменту резервного копіювання. Якщо це не так, зверніться до розробників. + Пароль не знайдено в сховищі ключів, введіть його вручну. Це могло статися, якщо ви відновили дані додатка за допомогою інструмента резервного копіювання. Якщо це не так, зверніться до розробників. Видалити профіль чату %d сек Пошук @@ -1428,7 +1428,7 @@ Ви вже подали запит на підключення за цією адресою! надіслати приватне повідомлення Показувати консоль в новому вікні - Всі нові повідомлення від %s будуть приховані! + Усі нові повідомлення від %s будуть приховані! підключив(лась) безпосередньо заблоковано Блокувати учасників групи @@ -1674,7 +1674,7 @@ Перевірте підключення до Інтернету та спробуйте ще раз Переконайтеся, що ви пам\'ятаєте пароль до бази даних для її перенесення. Помилка при перевірці парольної фрази: - Всі ваші контакти, розмови та файли будуть надійно зашифровані та завантажені частинами на налаштовані XFTP-реле. + Усі ваші контакти, розмови та файли будуть надійно зашифровані та завантажені частинами на налаштовані XFTP-реле. Please note: використання однієї і тієї ж бази даних на двох пристроях порушить розшифровку повідомлень з ваших з\'єднань, як захист безпеки.]]> Скасувати міграцію Чат перемістився! @@ -2025,7 +2025,7 @@ Проксірований Надіслати помилки Завершено - Всі профілі + Усі профілі Скинути Вивантажено Видалити %d повідомлень учасників? @@ -2179,7 +2179,7 @@ Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням: Помилки в конфігурації серверів. Умови приймаються з: %s. - Увімкнути flux + Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих Умови приймаються до: %s. Продовжити Створити одноразове посилання @@ -2308,7 +2308,6 @@ %d повідомлень Повідомлення учасників Повідомлення - Архівовані повідомлення учасників Вміст порушує умови використання Спам Файл заблоковано оператором сервера: \n%1$s. @@ -2360,4 +2359,33 @@ Згадуйте учасників 👋 Встановлюйте термін придатності повідомлень у чатах. Швидше видалення груп. + Помилка зчитування пароля бази даних + Пароль у сховищі ключів не можна зчитати. Це могло статися після оновлення системи, несумісного з додатком. Якщо це не так, зверніться до розробників. + Пароль у сховищі ключів не можна зчитати, введіть його вручну. Це могло статися після оновлення системи, несумісного з додатком. Якщо це не так, зверніться до розробників. + очікує + очікує на схвалення + Видалити учасників? + Усі нові повідомлення від цих учасників будуть приховані! + Заблокувати учасників для всіх? + Учасників буде видалено з групи – це неможливо скасувати! + модератори + Повідомлення від цих учасників буде показано! + Учасників буде видалено з чату – це неможливо скасувати! + Розблокувати учасників для всіх? + Оновлені умови + Приватні чати, групи та ваші контакти недоступні для операторів сервера. + Прийняти + Використовуючи SimpleX Chat, ви погоджуєтесь на:\n- надсилати тільки легальний контент у публічних групах.\n- поважати інших користувачів – без спаму. + Налаштувати операторів сервера + Політика конфіденційності та умови використання + Використовувати короткі посилання (BETA) + Це посилання вимагає новішої версії додатку. Будь ласка, оновіть додаток або попросіть вашого контакту надіслати сумісне посилання. + Повне посилання + Коротке посилання + Посилання на канал SimpleX + Несумісне посилання для підключення + Усі сервери + Ні + Типові сервери + Використовуйте TCP порт 443 лише для попередньо налаштованих серверів. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index cdc9721282..3cbc54f652 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -226,16 +226,16 @@ đ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 đường dẫn / 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 đường dẫn + 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 @@ -318,18 +318,18 @@ 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 đường dẫn? + 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 đường dẫn 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! @@ -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ỉ? @@ -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,7 +592,7 @@ 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ề @@ -682,7 +682,7 @@ 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 đường dẫn nhóm @@ -694,13 +694,13 @@ 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 @@ -725,8 +725,8 @@ 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 @@ -749,11 +749,11 @@ 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 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 @@ -776,7 +776,7 @@ 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,7 +828,7 @@ Chào buổi chiều! Giao diện tiếng Pháp Đã tìm thấy máy tính - Đầy đủ đường dẫn + 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. @@ -956,7 +956,7 @@ MÀU SẮC GIAO DIỆN đã được mời Đường dẫn không hợp lệ - tác vụ trò chuyệ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 @@ -1066,7 +1066,7 @@ Tin nhắn Lỗi keychain đã rời - đã được mời thông qua đường dẫn 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 @@ -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ở kết nối trò chuyện + 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ữ @@ -1263,7 +1263,7 @@ Đ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 @@ -1393,7 +1393,7 @@ 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ủ @@ -1869,7 +1869,6 @@ 1 báo cáo %d báo cáo Các báo cáo của thành viên - Các báo cáo thành viên đã được lưu trữ 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. @@ -2008,7 +2007,7 @@ Đâ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 thông qua đường dẫ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 @@ -2091,7 +2090,7 @@ 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 - thông qua %1$s + 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 @@ -2104,7 +2103,7 @@ 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 - thông qua đường dẫn dùng một lầ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 @@ -2117,9 +2116,9 @@ 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 - Thông qua giao thức kháng lượng tử an toàn. - thông qua đường dẫn nhóm - Thông qua trình duyệt + 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ố @@ -2132,7 +2131,7 @@ thông qua relay Tin nhắn thoại cuộc gọi video (không được mã hóa đầu cuối) - thông qua đường dẫn địa chỉ liên lạc + 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 @@ -2334,4 +2333,33 @@ đã từ chối Thiết lập giờ hết hạn cho tin nhắn trong các cuộc trò chuyện. Tên các tệp tin đa phương tiện riêng tư. + Lỗi đọc mật khẩu cơ sở dữ liệu + Tất cả các tin nhắn mới từ những thành viên này sẽ bị ẩn! + Chặn các thành viên cho tất cả? + các kiểm duyệt viên + Các tin nhắn từ những thành viên này sẽ được hiển thị! + Các thành viên sẽ bị xóa khỏi nhóm - việc này không thể được hoàn tác! + Các thành viên sẽ bị xóa khỏi cuộc trò chuyện - việc này không thể được hoàn tác! + Không thể đọc mật khẩu trong Keystore. Điều này có thể xảy ra sau bản cập nhật hệ thống không tương thích với ứng dụng. Nếu không phải như vậy, xin vui lòng liên hệ với các nhà phát triển. + đang chờ xử lý + Các điều kiện đã được cập nhật + Không thể đọc mật khẩu trong Keystore, xin vui lòng nhập thủ công. Điều này có thể xảy ra sau khi bản cập nhật hệ thống không tương thích với ứng dụng. Nếu không phải như vậy, vui lòng liên hệ với các nhà phát triển. + đang chờ phê duyệt + Bỏ chặn các thành viên cho tất cả? + Xóa các thành viên? + Chính sách quyền riêng tư và các điều kiện sử dụng. + Bằng việc sử dụng SimpleX Chat, bạn đồng ý:\n- chỉ gửi nội dung hợp pháp trong các nhóm công khai.\n- tôn trọng những người dùng khác - không gửi tin rác. + Các cuộc trò chuyện riêng tư, nhóm và liên hệ của bạn không thể truy cập được đối với các bên vận hành máy chủ. + Chấp nhận + Định cấu hình các bên vận hành máy chủ + Đường dẫn này yêu cầu một phiên bản ứng dụng mới hơn. Vui lòng nâng cấp ứng dụng hoặc yêu cầu liên hệ của một gửi cho một đường dẫn tương thích. + Đường dẫn kênh SimpleX + Đường dẫn kết nối không được hỗ trợ + Sử dụng đường dẫn ngắn (BETA) + Toàn bộ đường dẫn + Đường dẫn ngắn + Tắt + Các máy chủ cài sẵn + Chỉ sử dụng cổng TCP 443 cho các máy chủ cài sẵn. + Tất cả máy chủ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 77918b1c8f..28a5f6f50d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -1059,7 +1059,7 @@ 当你或你的连接使用旧数据库备份时,可能会发生这种情况。 解密错误 请向开发者报告。 - 上一条消息的散列不同。\" + 上条消息的哈希值不同。 下一条消息的 ID 不正确(小于或等于上一条)。 \n它可能是由于某些错误或连接被破坏才发生。 停止文件 @@ -1068,10 +1068,10 @@ 停止接收文件? 即将停止接收文件。 停止 - 撤销文件 - 撤销文件? + 吊销文件 + 吊销文件? 文件将从服务器中删除。 - 撤销 + 吊销 音频/视频通话 " \n在 v5.1 版本中可用" @@ -1330,7 +1330,7 @@ 更改密码或重启应用后,密码将以明文形式保存在设置中。 粘贴你收到的链接以与你的联系人联系… 送达回执 - 没有选择聊天 + 没有选中的聊天 可以加密 重新协商加密 禁用(保留组覆盖) @@ -1961,7 +1961,7 @@ 转发服务器 %1$s 连接目的地服务器 %2$s 失败。请稍后尝试。 转发服务器地址不兼容网络设置:%1$s。 转发服务器版本不兼容网络设置:%1$s。 - %1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置 + %1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置。 连接转发服务器 %1$s 出错。请稍后尝试。 模糊媒体文件 中度 @@ -2061,7 +2061,7 @@ 用户名 分享配置文件 转发消息出错 - 在你选中消息后这些消息被删除。 + 在你选中消息后这些消息已被删除。 %1$d 个文件错误:\n%2$s 其他 %1$d 个文件错误。 %1$d 个文件未被下载。 @@ -2070,29 +2070,29 @@ 没什么可转发的! 仍有 %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 协议。 通话期间切换音频和视频。 对一次性邀请切换聊天配置文件。 @@ -2116,21 +2116,21 @@ 创建一次性链接 用于社交媒体 或者私下分享 - 服务器运营者 - 网络运营者 - 30 天后将接受已启用的运营者的条款。 + 服务器运营方 + 网络运营方 + 30 天后将接受已启用的运营方的条款。 继续 稍后审阅 - 选择要使用的网络运营者。 + 选择要使用的网络运营方。 更新 你可以通过设置配置服务器。 - %s.]]> - 将于下列日期自动接受已启用的运营者的条款:%s。 + %s.]]> + 将于下列日期自动接受已启用的运营方的条款:%s。 预设服务器 你的服务器 接受条款的将来日期为:%s。 - 网络运营者 - 运营者 + 网络运营方 + 运营方 %s 台服务器 网站 无法加载当前条款文本,你可以通过此链接审阅条款: @@ -2147,33 +2147,33 @@ 用于消息 打开更改 打开条款 - 运营者服务器 - 已添加服务器到运营者 %s - 服务器运营者已更改。 + 运营方服务器 + 已添加服务器到运营方 %s + 服务器运营方已更改。 服务器协议已更改。 透明度 网络去中心化 - 应用中的第二个预设运营者! + 应用中的第二个预设运营方! 改进了聊天导航 查看更新后的条款 比如,如果你通过 SimpleX 服务器收到消息,应用会通过 Flux 服务器传送它们。 - 应用通过在每个对话中使用不同运营者保护你的隐私。 + 应用通过在每个对话中使用不同运营方保护你的隐私。 接受条款 模糊 - 地址或一次性链接? + 地址还是一次性链接? 已添加消息服务器 已添加媒体和文件服务器 地址设置 - 已接受条款 + 已接受的条款 应用工具栏 仅用于一名联系人 - 面对面或通过任何消息应用分享.]]> - %s.]]> - %s.]]> - %s.]]> - %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> %s的服务器,请接受使用条款。]]> - %s.]]> - 开启 flux + %s.]]> + 在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。 接受条款出错 为了更好的元数据隐私。 添加服务器出错 @@ -2189,10 +2189,10 @@ 此消息被删除或尚未收到。 连接达到了未送达消息上限,你的联系人可能处于离线状态。 为了防止链接被替换,你可以比较联系人安全代码。 - 你可以在“网络和服务器”设置中配置运营者。 - 接受运营者条款的日期:%s + 你可以在“网络和服务器”设置中配置运营方。 + 接受运营方条款的日期:%s 远程移动设备 - 或者导入压缩文件 + 或者导入存档文件 小米设备:请在系统设置中开启“自动启动”让通知正常工作。]]> 消息太大! 你可以复制并减小消息大小来发送它。 @@ -2228,10 +2228,10 @@ 聊天 将从聊天中删除成员 - 此操作无法撤销! 请减小消息尺寸并再次发送。 - 当启用了超过一个运营者时,没有一个运营者拥有了解谁和谁联络的元数据。 + 当启用了超过一个运营方时,没有一个运营方拥有了解谁和谁联络的元数据。 已接受邀请 被请求连接 - 关于运营者 + 关于运营方 SimpleX Chat 和 Flux 达成协议将 Flux 运营的服务器包括在应用中。 修复 修复连接? @@ -2282,8 +2282,8 @@ 另一个理由 已存档的举报 违反社区指导方针 - 不恰当的内容 - 不恰当的个人资料 + 不当内容 + 不当个人资料 仅发送人和moderators能看到 只有你和moderators能看到 垃圾信息 @@ -2294,13 +2294,12 @@ 举报 1 个举报 成员举报 - 已存档的成员举报 %d 个举报 垃圾信息 连接被阻止 - 连接被服务器运营者阻止:\n%1$s. + 连接被服务器运营方阻止:\n%1$s. 内容违反使用条款 - 文件被服务器运营者阻止:\n%1$s. + 文件被服务器运营方阻止:\n%1$s. 询问 @@ -2338,7 +2337,7 @@ 更好的隐私和安全 不错过重要消息。 更快地删除群。 - 更快死发送消息。 + 更快发送消息。 被提及时收到通知。 帮助管理员管理群组。 将聊天组织到列表 @@ -2347,4 +2346,33 @@ 在聊天中设置消息过期时间。 被拒绝 被拒绝 + 数据库密码短语读取出错 + 无法读取 Keystore 中的密码短语,请手动输入它。这可能在不兼容本应用的系统更新后出现。如果不是这种情况,请联系开发者。 + 无法读取 Keystore 中的密码短语,请手动输入它。这可能在不兼容本应用的系统更新后出现。如果不是这种情况,请联系开发者。 + 待批准 + 待批准 + 条款已更新 + 所有来自这些成员的新消息都将被隐藏! + 为所有其他成员封禁这些成员? + 将从群中移除这些成员 — 此操作无法撤销! + 将显示来自这些成员的消息! + 删除成员吗? + 为所有其他成员解封这些成员吗? + moderators + 将从聊天中移除这些成员 — 此操作无法撤销! + 隐私政策和使用条款。 + 接受 + 使用 SimpleX Chat 代表您同意:\n- 在公开群中只发送合法内容\n- 尊重其他用户 – 没有垃圾信息。 + 服务器运营方无法访问私密聊天、群组和你的联系人。 + 配置服务器运营方 + 不支持的连接链接 + 使用短链接(测试) + SimpleX 频道链接 + 短链接 + 此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。 + 完整链接 + 全部服务器 + 关闭 + 预设服务器 + 仅预设服务器使用 TCP 协议 443 端口。 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 3038dd3d6c..8ae414de00 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -1495,7 +1495,7 @@ %s 的版本。請檢察兩台裝置安裝的是否版本相同]]> 更可靠的網路連接 發現和加入群組 - 裝置 + 設備 新行動裝置 保存設定出錯 導出的檔案不存在 @@ -1628,7 +1628,7 @@ 找到桌面 自動連接 與PC版的連接不穩定 - 桌面 + 桌上電腦 已安裝的PC版本不支援。請確認兩台裝置所安裝的版本相同 PC版邀請碼錯誤 通過連結連接? 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..dfffb826f5 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -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 } } } 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 d0d4fb5e92..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 @@ -44,10 +44,10 @@ 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: (ComposeMessage) -> Unit, @@ -203,16 +203,16 @@ actual fun PlatformTextField( ) 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/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/gradle.properties b/apps/multiplatform/gradle.properties index 7bc1da0b97..aa4c7a7470 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.3-beta.7 -android.version_code=278 +android.version_name=6.3.6 +android.version_code=295 -desktop.version_name=6.3-beta.7 -desktop.version_code=95 +android.bundle=false + +desktop.version_name=6.3.6 +desktop.version_code=106 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 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..8107b664c4 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -75,7 +75,6 @@ mkChatOpts :: BroadcastBotOpts -> ChatOpts mkChatOpts BroadcastBotOpts {coreOptions} = ChatOpts { coreOptions, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatCmdLog = CCLNone, diff --git a/apps/simplex-chat/Server.hs b/apps/simplex-chat/Server.hs index fddad1cf2c..0906d14536 100644 --- a/apps/simplex-chat/Server.hs +++ b/apps/simplex-chat/Server.hs @@ -2,17 +2,23 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} module Server where import Control.Monad import Control.Monad.Except import Control.Monad.Reader -import Data.Aeson (FromJSON, ToJSON) +import Data.Aeson (FromJSON, ToJSON (..)) import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import Data.Bifunctor (first) import Data.Text (Text) import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic) @@ -23,11 +29,44 @@ 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) + +instance ToJSON (CSRBody ChatResponse) where + toJSON = toJSON . first ObjChatCmdError . csrBody + toEncoding = toEncoding . first ObjChatCmdError . csrBody + +instance ToJSON (CSRBody ChatEvent) where + toJSON = toJSON . first ObjChatError . csrBody + toEncoding = toEncoding . first ObjChatError . csrBody + +data AChatSrvResponse = forall r. ToJSON (ChatSrvResponse r) => ACR (ChatSrvResponse r) + +$(pure []) + +instance ToJSON (CSRBody r) => ToJSON (ChatSrvResponse r) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''ChatSrvResponse) + toJSON = $(JQ.mkToJSON defaultJSON ''ChatSrvResponse) + simplexChatServer :: ServiceName -> ChatConfig -> ChatOpts -> IO () simplexChatServer chatPort cfg opts = simplexChatCore cfg opts . const $ runChatServer defaultChatServerConfig {chatPort} @@ -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 (runExceptT $ processChatCommand cmd) cc where - response resp = pure ChatSrvResponse {corrId = Just corrId, resp} + response r = ChatSrvResponse {corrId = Just corrId, resp = CSRBody r} clientDisconnected _ = pure () diff --git a/apps/simplex-directory-service/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 89099ab9df..faaccbd2bf 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -11,11 +11,11 @@ module Directory.Events ( DirectoryEvent (..), DirectoryCmd (..), ADirectoryCmd (..), + DirectoryHelpSection (..), DirectoryRole (..), SDirectoryRole (..), crDirectoryEvent, directoryCmdTag, - viewName, ) where @@ -25,6 +25,7 @@ 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) @@ -45,7 +46,7 @@ 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 @@ -61,41 +62,44 @@ 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_) - CRJoinedGroupMember {groupInfo, member = m} + 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 - CRNewChatItems {chatItems = AChatItem _ _ (GroupChat g) ci : _} -> case ci of + CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g) ci : _} -> case ci of ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t _ -> Nothing - CRMemberRole {groupInfo, member, toRole} + 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 @@ -137,8 +141,11 @@ 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 @@ -180,7 +187,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_ @@ -213,11 +220,19 @@ 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 @@ -228,14 +243,14 @@ directoryCmdP = DCGroupFilter_ -> do (groupId, displayName_) <- gc_ (,) acceptance_ <- - (A.takeWhile (== ' ') >> A.endOfInput) $> Nothing + (A.takeWhile isSpace >> A.endOfInput) $> Nothing <|> Just <$> (acceptancePresetsP <|> acceptanceFiltersP) pure $ DCGroupFilter groupId displayName_ acceptance_ where acceptancePresetsP = spacesP *> A.choice - [ "no" $> noJoinFilter, + [ "off" $> noJoinFilter, "basic" $> basicJoinFilter, ("moderate" <|> "mod") $> moderateJoinFilter, "strong" $> strongJoinFilter @@ -272,15 +287,12 @@ directoryCmdP = where gc f = f <$> (spacesP *> A.decimal) <*> (A.char ':' *> displayNameTextP) gc_ f = f <$> (spacesP *> A.decimal) <*> optional (A.char ':' *> displayNameTextP) - -- wordP = spacesP *> A.takeTill (== ' ') - spacesP = A.takeWhile1 (== ' ') - -viewName :: Text -> Text -viewName n = if T.any (== ' ') n then "'" <> n <> "'" else n + -- wordP = spacesP *> A.takeTill isSpace + spacesP = A.takeWhile1 isSpace directoryCmdTag :: DirectoryCmd r -> Text directoryCmdTag = \case - DCHelp -> "help" + DCHelp _ -> "help" DCSearchGroup _ -> "search" DCSearchNext -> "next" DCAllGroups -> "all" diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 5ee52249ac..2c26905e79 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -158,7 +158,6 @@ mkChatOpts :: DirectoryOpts -> ChatOpts mkChatOpts DirectoryOpts {coreOptions} = ChatOpts { coreOptions, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatCmdLog = CCLNone, diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 7c54f344ad..4517ee9c5b 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -39,6 +39,7 @@ import qualified Data.Text.IO as T import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Directory.BlockedWords +import Directory.Captcha import Directory.Events import Directory.Options import Directory.Search @@ -47,6 +48,7 @@ import Simplex.Chat.Bot import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller import Simplex.Chat.Core +import Simplex.Chat.Markdown (FormattedText (..), Format (..), parseMaybeMarkdownList, viewName) import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) @@ -58,8 +60,10 @@ import Simplex.Chat.Terminal (terminalChatConfig) import Simplex.Chat.Terminal.Main (simplexChatCLI') import Simplex.Chat.Types 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 (..)) import Simplex.Messaging.Agent.Store.Common (withTransaction) +import Simplex.Messaging.Agent.Protocol (SConnectionMode (..), sameConnReqContact, sameShortLinkContact) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) @@ -67,7 +71,6 @@ import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) import System.Process (readProcess) -import System.Random (randomRIO) data GroupProfileUpdate = GPNoServiceLink | GPServiceLinkAdded | GPServiceLinkRemoved | GPHasServiceLink | GPServiceLinkError @@ -150,7 +153,7 @@ directoryService :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> C directoryService st opts@DirectoryOpts {testing} env user cc = do initializeBotAddress' (not testing) cc race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc + (_, resp) <- atomically . readTBQueue $ outputQ cc directoryServiceEvent st opts env user cc resp acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)) @@ -185,22 +188,22 @@ useMemberFilter img_ = \case Nothing -> False readBlockedWordsConfig :: DirectoryOpts -> IO BlockedWordsConfig -readBlockedWordsConfig DirectoryOpts {blockedFragmentsFile, blockedWordsFile, nameSpellingFile, blockedExtensionRules} = do +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 - putStrLn $ "Blocked fragments: " <> show (length blockedFragments) <> ", blocked words: " <> show (length blockedWords) <> ", spelling rules: " <> show (M.size spelling) + unless testing $ putStrLn $ "Blocked fragments: " <> show (length blockedFragments) <> ", blocked words: " <> show (length blockedWords) <> ", spelling rules: " <> show (M.size spelling) pure BlockedWordsConfig {blockedFragments, blockedWords, extensionRules, spelling} -directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> ChatResponse -> IO () +directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> Either ChatError ChatEvent -> IO () directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc event = forM_ (crDirectoryEvent event) $ \case DEContactConnected ct -> deContactConnected ct DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner - DEGroupUpdated {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 @@ -246,31 +249,39 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)]) getGroups_ search_ = sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case - CRGroupsList {groups} -> pure $ Just groups + Right CRGroupsList {groups} -> pure $ Just groups _ -> pure Nothing getDuplicateGroup :: GroupInfo -> IO (Maybe DuplicateGroup) getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName, fullName}} = getGroups fullName >>= mapM duplicateGroup where - sameGroup (GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}}, _) = - gId /= groupId && n == displayName && fn == fullName + sameGroupNotRemoved (g@GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}}, _) = + gId /= groupId && n == displayName && fn == fullName && not (memberRemoved $ membership g) duplicateGroup [] = pure DGUnique duplicateGroup groups = do - let gs = filter sameGroup groups + let gs = filter sameGroupNotRemoved groups if null gs then pure DGUnique else do (lgs, rgs) <- atomically $ (,) <$> readTVar (listedGroups st) <*> readTVar (reservedGroups st) let reserved = any (\(GroupInfo {groupId = gId}, _) -> gId `S.member` lgs || gId `S.member` rgs) gs - pure $ if reserved then DGReserved else DGRegistered + if reserved + then pure DGReserved + else do + removed <- foldM (\r -> fmap (r &&) . isGroupRemoved) True gs + pure $ if removed then DGUnique else DGRegistered + isGroupRemoved (GroupInfo {groupId = gId}, _) = + getGroupReg st gId >>= \case + Just GroupReg {groupRegStatus} -> groupRemoved <$> readTVarIO groupRegStatus + Nothing -> pure True processInvitation :: Contact -> GroupInfo -> IO () processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do void $ addGroupReg st ct g GRSProposed r <- sendChatCmd cc $ APIJoinGroup groupId MFNone sendMessage cc ct $ case r of - CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" + Right CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" _ -> "Error joining group " <> displayName <> ", please re-send the invitation!" deContactConnected :: Contact -> IO () @@ -326,7 +337,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName $>>= \mId -> resp <$> sendChatCmd cc (APIGroupMemberInfo dbGroupId mId) where resp = \case - CRGroupMemberInfo {member} -> Just member + Right CRGroupMemberInfo {member} -> Just member _ -> Nothing deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO () @@ -337,16 +348,16 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName 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 + sendChatCmd cc (APICreateGroupLink groupId GRMember False) >>= \case + Right CRGroupLinkCreated {connLinkContact = CCLink gLink _} -> 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 + notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact gLink) + Left (ChatError e) -> case e of CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." CEGroupNotJoined _ -> notifyOwner gr $ unexpectedError "group not joined" @@ -354,78 +365,97 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName _ -> 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 let userGroupRef = userGroupReference gr toGroup + byMember = case memberContactId m of + Just ctId | ctId `isOwner` gr -> "" -- group registration owner, not any group owner. + _ -> " by " <> mName -- owner notification from directory will include the name. readTVarIO (groupRegStatus gr) >>= \case GRSPendingConfirmation -> pure () GRSProposed -> pure () GRSPendingUpdate -> groupProfileUpdate >>= \case GPNoServiceLink -> - when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message." - GPServiceLinkAdded - | ctId `isOwner` gr -> groupLinkAdded gr - | otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself." - GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it." - GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr + 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 $ n + 1 + GRSActive -> processProfileChange gr byMember 1 + GRSSuspended -> processProfileChange gr byMember 1 + GRSSuspendedBadRoles -> processProfileChange gr byMember 1 GRSRemoved -> pure () where - isInfix l d_ = l `T.isInfixOf` fromMaybe "" d_ GroupInfo {groupId, groupProfile = p} = fromGroup GroupInfo {groupProfile = p'} = toGroup sameProfile GroupProfile {displayName = n, fullName = fn, image = i, description = d} GroupProfile {displayName = n', fullName = fn', image = i', description = d'} = n == n' && fn == fn' && i == i' && d == d' - groupLinkAdded gr = do + groupLinkAdded gr byMember = do getDuplicateGroup toGroup >>= \case Nothing -> notifyOwner gr "Error: getDuplicateGroup. Please notify the developers." Just DGReserved -> notifyOwner gr $ groupAlreadyListed toGroup _ -> do let gaId = 1 setGroupStatus st gr $ GRSPendingApproval gaId - notifyOwner gr $ "Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message.\nYou will be notified once the group is added to the directory - it may take up to 48 hours." + notifyOwner gr $ + ("Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message" <> byMember) + <> ".\nYou will be notified once the group is added to the directory - it may take up to 48 hours." checkRolesSendToApprove gr gaId - processProfileChange gr n' = do + processProfileChange gr byMember n' = do setGroupStatus st gr GRSPendingUpdate let userGroupRef = userGroupReference gr toGroup groupRef = groupReference toGroup groupProfileUpdate >>= \case GPNoServiceLink -> do - notifyOwner gr $ "The group profile is updated " <> userGroupRef <> ", but no link is added to the welcome message.\n\nThe group will remain hidden from the directory until the group link is added and the group is re-approved." + notifyOwner gr $ + ("The group profile is updated for " <> userGroupRef <> byMember <> ", but no link is added to the welcome message.\n\n") + <> "The group will remain hidden from the directory until the group link is added and the group is re-approved." GPServiceLinkRemoved -> do - notifyOwner gr $ "The group link for " <> userGroupRef <> " is removed from the welcome message.\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." + 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 <> "." + notifyOwner gr $ + ("The group link is added to " <> userGroupRef <> byMember) + <> "!\nIt is hidden from the directory until approved." + notifyAdminUsers $ "The group link is added to " <> groupRef <> byMember <> "." checkRolesSendToApprove gr n' GPHasServiceLink -> do setGroupStatus st gr $ GRSPendingApproval n' - notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved." - notifyAdminUsers $ "The group " <> groupRef <> " is updated." + notifyOwner gr $ + ("The group " <> userGroupRef <> " is updated" <> byMember) + <> "!\nIt is hidden from the directory until approved." + notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> "." checkRolesSendToApprove gr n' GPServiceLinkError -> 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' + Right CRGroupLink {connLinkContact = CCLink cr sl_} -> + let hadLinkBefore = profileHasGroupLink fromGroup + hasLinkNow = profileHasGroupLink toGroup + profileHasGroupLink GroupInfo {groupProfile = gp} = + maybe False (any ftHasLink) $ parseMaybeMarkdownList =<< description gp + ftHasLink = \case + FormattedText (Just SimplexLink {simplexUri = ACL SCMContact cLink}) _ -> case cLink of + CLFull cr' -> sameConnReqContact cr' cr + CLShort sl' -> maybe False (sameShortLinkContact sl') sl_ + _ -> False in if | hadLinkBefore && hasLinkNow -> GPHasServiceLink | hadLinkBefore -> GPServiceLinkRemoved @@ -455,12 +485,6 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName atomically $ TM.insert gmId captcha $ pendingCaptchas env sendCaptcha mc where - getCaptchaStr 0 s = pure s - getCaptchaStr n s = do - i <- randomRIO (0, length chars - 1) - let c = chars !! i - getCaptchaStr (n - 1) (c : s) - chars = "23456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghijkmnpqrsty" getCaptcha s = case captchaGenerator opts of Nothing -> pure textMsg Just script -> content <$> readProcess script [s] "" @@ -475,11 +499,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO () approvePendingMember a g@GroupInfo {groupId} m@GroupMember {memberProfile = LocalProfile {displayName, image}} = do - gli_ <- join <$> withDB' cc (\db -> getGroupLinkInfo db userId groupId) + gli_ <- join <$> withDB' "getGroupLinkInfo" cc (\db -> getGroupLinkInfo db userId groupId) let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_ gmId = groupMemberId' m sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case - CRJoinedGroupMember {} -> do + Right CRJoinedGroupMember {} -> do atomically $ TM.delete gmId $ pendingCaptchas env logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g r -> logError $ "unexpected accept member response: " <> tshow r @@ -491,7 +515,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName 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 - | captchaText == msgText -> do + | matchCaptchaStr captchaText msgText -> do sendComposedMessages_ cc (SRGroup groupId $ Just $ groupMemberId' m) [(Just ciId, MCText $ "Correct, you joined the group " <> n)] approvePendingMember a g m | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts @@ -503,8 +527,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName rejectPendingMember rjctNotice = do let gmId = groupMemberId' m sendComposedMessages cc (SRGroup groupId $ Just gmId) [MCText rjctNotice] - sendChatCmd cc (APIRemoveMembers groupId [gmId]) >>= \case - CRUserDeletedMembers _ _ (_ : _) -> do + 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 @@ -623,7 +647,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName 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 " @@ -634,7 +658,16 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, 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" + \Start from inviting the bot to your group as admin - it will guide you through the process." + DCHelp DHSCommands -> + sendMessage cc ct $ + "*/help commands* - receive this help message.\n\ + \*/help* - how to register your group to be added to directory.\n\ + \*/list* - list the groups you registered.\n\ + \*/delete :* - remove the group you submitted from directory, with _ID_ and _name_ as shown by */list* command.\n\ + \*/role * - view and set default member role for your group.\n\ + \*/filter * - view and set spam filter settings for group.\n\n\ + \To search for groups, send the search text." DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s DCSearchNext -> atomically (TM.lookup (contactId' ct) searchRequests) >>= \case @@ -666,24 +699,19 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName DCListUserGroups -> getUserGroupRegs st (contactId' ct) >>= \grs -> do sendReply $ tshow (length grs) <> " registered group(s)" - -- debug how it can be that user has 0 registered groups - when (length grs == 0) $ do - total <- length <$> readTVarIO (groupRegs st) - withSuperUsers $ \ctId -> sendMessage' cc ctId $ - "0 registered groups for " <> localDisplayName' ct <> " (" <> tshow (contactId' ct) <> ") out of " <> tshow total <> " registrations" 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 + DCDeleteGroup gId gName -> + (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} gr -> do delGroupReg st gr - sendReply $ "Your group " <> displayName <> " is deleted from the directory" + sendReply $ (if isAdmin then "The group " else "Your group ") <> displayName <> " is deleted from the directory" DCMemberRole gId gName_ mRole_ -> (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g case mRole_ of Nothing -> getGroupLinkRole cc user g >>= \case - Just (_, gLink, mRole) -> do + Just (_, CCLink gLink _, mRole) -> do let anotherRole = case mRole of GRObserver -> GRMember; _ -> GRObserver sendReply $ initialRole n mRole @@ -704,7 +732,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName case acceptance_ of Just a' | a /= a' -> do let d = toCustomData $ DirectoryGroupData a' - withDB' cc (\db -> setGroupCustomData db user g $ Just d) >>= \case + withDB' "setGroupCustomData" cc (\db -> setGroupCustomData db user g $ Just d) >>= \case Just () -> sendSettigns n a' " set to" Nothing -> sendReply $ "Error changing spam filter settings for group " <> n _ -> sendSettigns n a "" @@ -719,7 +747,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName "", -- "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." - "Or use */filter " <> tshow gId <> " [name] [captcha]* to configure filter." + "Use */filter " <> tshow gId <> " [name] [captcha]* to enable and */filter " <> tshow gId <> " off* to disable filter." ] showCondition = \case Nothing -> "_disabled_" @@ -808,9 +836,12 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName setGroupStatus st gr GRSActive let approved = "The group " <> userGroupReference' gr n <> " is approved" notifyOwner gr $ - (approved <> " and listed in directory!\n") + (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" - <> ("Use */filter " <> tshow ugrId <> "* to configure anti-spam filter and */role " <> tshow ugrId <> "* to set default member role.") + <> "Supported commands:\n" + <> ("- */filter " <> tshow ugrId <> "* - to configure anti-spam filter.\n") + <> ("- */role " <> tshow ugrId <> "* - to set default member role.\n") + <> "- */help commands* - other commands." invited <- forM ownersGroup $ \og@KnownGroup {localDisplayName = ogName} -> do inviteToOwnersGroup og gr $ \case @@ -860,18 +891,21 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let groupRef = groupReference' groupId gName withGroupAndReg sendReply groupId gName $ \_ _ -> sendChatCmd cc (APIGetGroupLink groupId) >>= \case - CRGroupLink {connReqContact, memberRole} -> + Right CRGroupLink {connLinkContact = CCLink cReq _, memberRole} -> sendReply $ T.unlines [ "The link to join the group " <> groupRef <> ":", - strEncodeTxt $ simplexChatContact connReqContact, + strEncodeTxt $ simplexChatContact cReq, "New member role: " <> strEncodeTxt memberRole ] - CRChatCmdError _ (ChatErrorStore (SEGroupLinkNotFound _)) -> + Left (ChatErrorStore (SEGroupLinkNotFound _)) -> sendReply $ "The group " <> groupRef <> " has no public link." - r -> do + Right r -> do ts <- getCurrentTime tz <- getCurrentTimeZone - let resp = T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r + let resp = T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r + sendReply $ "Unexpected error:\n" <> resp + Left e -> do + let resp = T.pack $ serializeChatError True (config cc) e sendReply $ "Unexpected error:\n" <> resp DCSendToGroupOwner groupId gName msg -> do let groupRef = groupReference' groupId gName @@ -913,11 +947,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName 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 @@ -938,10 +972,13 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deSuperUserCommand ct ciId cmd | knownContact ct `elem` superUsers = case cmd of 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 @@ -983,38 +1020,38 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName sendComposedMessage cc ct Nothing $ MCText text getContact' :: ChatController -> User -> ContactId -> IO (Maybe Contact) -getContact' cc user ctId = withDB cc $ \db -> getContact db (vr cc) user ctId +getContact' cc user ctId = withDB "getContact" cc $ \db -> getContact db (vr cc) user ctId getGroup :: ChatController -> User -> GroupId -> IO (Maybe GroupInfo) -getGroup cc user gId = withDB cc $ \db -> getGroupInfo db (vr cc) user gId +getGroup cc user gId = withDB "getGroupInfo" cc $ \db -> getGroupInfo db (vr cc) user gId -withDB' :: ChatController -> (DB.Connection -> IO a) -> IO (Maybe a) -withDB' cc a = withDB cc $ ExceptT . fmap Right . a +withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Maybe a) +withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a -withDB :: ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a) -withDB ChatController {chatStore} action = do +withDB :: Text -> ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a) +withDB cxt ChatController {chatStore} action = do r_ :: Either ChatError a <- withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors case r_ of Right r -> pure $ Just r - Left e -> Nothing <$ logError ("Database error: " <> tshow e) + Left e -> Nothing <$ logError ("Database error: " <> cxt <> " " <> tshow e) getGroupAndSummary :: ChatController -> User -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) getGroupAndSummary cc user gId = - withDB cc $ \db -> (,) <$> getGroupInfo db (vr cc) user gId <*> liftIO (getGroupSummary db user gId) + withDB "getGroupAndSummary" cc $ \db -> (,) <$> getGroupInfo db (vr cc) user gId <*> liftIO (getGroupSummary db user gId) vr :: ChatController -> VersionRangeChat vr ChatController {config = ChatConfig {chatVRange}} = chatVRange {-# INLINE vr #-} -getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, ConnReqContact, GroupMemberRole)) +getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, CreatedLinkContact, GroupMemberRole)) getGroupLinkRole cc user gInfo = - withDB cc $ \db -> getGroupLink db user gInfo + withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe ConnReqContact) setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole) where resp = \case - CRGroupLink _ _ gLink _ -> Just gLink + Right (CRGroupLink _ _ (CCLink gLink _) _) -> Just gLink _ -> 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 fed52f494f..628419ea1d 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -25,6 +25,7 @@ module Directory.Store filterListedGroups, groupRegStatusText, pendingApproval, + groupRemoved, fromCustomData, toCustomData, noJoinFilter, @@ -139,13 +140,19 @@ data GroupRegStatus | GRSSuspended | GRSSuspendedBadRoles | GRSRemoved + deriving (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 groupRegStatusText :: GroupRegStatus -> Text groupRegStatusText = \case @@ -163,6 +170,7 @@ grDirectoryStatus = \case GRSActive -> DSListed GRSSuspended -> DSReserved GRSSuspendedBadRoles -> DSReserved + GRSRemoved -> DSRemoved _ -> DSRegistered $(JQ.deriveJSON (enumJSON $ dropPrefix "PC") ''ProfileCondition) @@ -200,10 +208,11 @@ addGroupReg st ct GroupInfo {groupId} grStatus = do | otherwise = mx delGroupReg :: DirectoryStore -> GroupReg -> IO () -delGroupReg st GroupReg {dbGroupId = gId} = do +delGroupReg st GroupReg {dbGroupId = gId, groupRegStatus} = do logGDelete st gId + atomically $ writeTVar groupRegStatus GRSRemoved atomically $ unlistGroup st gId - atomically $ modifyTVar' (groupRegs st) $ filter ((gId ==) . dbGroupId) + atomically $ modifyTVar' (groupRegs st) $ filter ((gId /=) . dbGroupId) setGroupStatus :: DirectoryStore -> GroupReg -> GroupRegStatus -> IO () setGroupStatus st gr grStatus = do @@ -216,6 +225,7 @@ setGroupStatus st gr grStatus = do DSListed -> listGroup DSReserved -> reserveGroup DSRegistered -> unlistGroup + DSRemoved -> unlistGroup setGroupRegOwner :: DirectoryStore -> GroupReg -> GroupMember -> IO () setGroupRegOwner st gr owner = do @@ -390,6 +400,7 @@ mkDirectoryStore h groups = DSListed -> (grs', S.insert gId listed, reserved) DSReserved -> (grs', listed, S.insert gId reserved) DSRegistered -> (grs', listed, reserved) + DSRemoved -> (grs, listed, reserved) mkDirectoryStore_ :: Maybe Handle -> ([GroupReg], Set GroupId, Set GroupId) -> IO DirectoryStore mkDirectoryStore_ h (grs, listed, reserved) = do 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/README.md b/blog/README.md index 1432d95de5..00a84eca6c 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,25 @@ # Blog +Mar 3, 2025 [SimpleX Chat v6.3: new user experience and safety in public groups](./20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) + +What's new in v6.3: +- preventing spam and abuse in public groups. +- group improvements: mention other members and improved performance. +- better chat navigation: organize chats into lists and jump to found and forwarded messages. +- privacy and security improvements: chat retention period and private media file names. + +Also, we added Catalan interface language to Android and desktop apps, thanks to our users and Weblate. + +The last but not the least - server builds are now reproducible! + +-- + +Jan 14, 2025 [SimpleX network: large groups and privacy-preserving content moderation](./20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md) + +This post explains how server operators can moderate end-to-end encrypted conversations without compromising user privacy or end-to-end encryption. + +-- + Dec 10, 2024 [SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) - SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app to improve metadata privacy in SimpleX network. @@ -19,7 +39,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/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/cabal.project b/cabal.project index 477ba9c601..f406b9820e 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 9fece9ce3df24d1b006d98e44a6b4e654861428b + tag: 3d62a383d5dcae6529d6d866233857182bcb4d47 source-repository-package type: git diff --git a/docs/SERVER.md b/docs/SERVER.md index 88ef74c058..f45403be8a 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -15,6 +15,7 @@ revision: 12.10.2024 - [systemd service](#systemd-service) with [installation script](#installation-script) or [manually](#manual-deployment) - [docker container](#docker-container) - [Linode marketplace](#linode-marketplace) +- [Verifying server binaries](#verifying-server-binaries) - [Configuration](#configuration) - [Interactively](#interactively) - [Via command line options](#via-command-line-options) @@ -32,6 +33,7 @@ revision: 12.10.2024 - [Systemd commands](#systemd-commands) - [Control port](#control-port) - [Daily statistics](#daily-statistics) +- [Reproduce builds](#reproduce-builds) - [Updating your SMP server](#updating-your-smp-server) - [Configuring the app to use the server](#configuring-the-app-to-use-the-server) @@ -515,6 +517,28 @@ This configuration allows you to retain the ability to manage 80 and 443 ports y You can deploy smp-server upon creating new Linode VM. Please refer to: [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) +## Verifying server binaries + +Starting from v6.3 server builds are [reproducible](#reproduce-builds). + +That also allows us to sign server releases, confirming the integrity of GitHub builds. + +To verify server binaries after you downloaded them: + +1. Download `_sha256sums` (hashes of all server binaries) and `_sha256sums.asc` (signature). + +2. Download our key FB44AF81A45BDE327319797C85107E357D4A17FC from [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) + +3. Import the key with `gpg --import FB44AF81A45BDE327319797C85107E357D4A17FC`. Key filename should be the same as its fingerprint, but please change it if necessary. + +4. Run `gpg --verify _sha256sums.asc _sha256sums`. It should print: + +> Good signature from "SimpleX Chat " + +5. Compute the hashes of the binaries you plan to use with `shu256sum ` or with `openssl sha256 ` and compare them with the hashes in the file `_sha256sums` - they must be the same. + +That is it - you now verified authenticity of our GitHub server binaries. + ## Configuration To see which options are available, execute `smp-server` without flags: @@ -1564,6 +1588,56 @@ To update your smp-server to latest version, choose your installation method and docker image prune ``` +## Reproduce builds + +You can locally reproduce server binaries, following these instructions. + +If you are a security expert or researcher, you can help SimpleX network and users community by signing the release checksums – we will [publish your signature](https://github.com/simplex-chat/simplexmq/releases/tag/v6.3.1). Please reach out to us! + +To reproduce the build you must have: + +- Linux machine +- `x86-64` architecture +- Installed `docker`, `curl` and `git` + +1. Download script: + + ```sh + curl -LO 'https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/master/scripts/simplexmq-reproduce-builds.sh' + ``` + +2. Make it executable: + + ```sh + chmod +x simplexmq-reproduce-builds.sh + ``` + +3. Execute the script with the required tag: + + ```sh + ./simplexmq-reproduce-builds.sh 'v6.3.1' + ``` + + The script executes these steps (please review the script to confirm): + + 1) builds all server binaries for the release in docker container. + 2) downloads binaries from the same GitHub release and compares them with the built binaries. + 3) if they all match, generates _sha256sums file with their checksums. + + This will take a while. + +4. After compilation, you should see the folder named as the tag (e.g., `v6.3.1`) with two subfolders: + + ```sh + ls v6.3.1 + ``` + + ```sh + from-source prebuilt _sha256sums + ``` + + The file _sha256sums contains the hashes of all builds - you can compare it with the same file in GitHub release. + ## Configuring the app to use the server To configure the app to use your messaging server copy it's full address, including password, and add it to the app. You have an option to use your server together with preset servers or without them - you can remove or disable them. diff --git a/docs/dependencies/HASKELL.md b/docs/dependencies/HASKELL.md new file mode 100644 index 0000000000..e6b180e33b --- /dev/null +++ b/docs/dependencies/HASKELL.md @@ -0,0 +1,177 @@ +# Dependency License Report + +Bold-faced **`package-name`**s denote standard libraries bundled with `ghc-9.6.3`. + +## Direct dependencies of `simplex-chat:exe:simplex-chat` + +| Name | Version | [SPDX](https://spdx.org/licenses/) License Id | Description | Also depended upon by | +| --- | --- | --- | --- | --- | +| **`base`** | [`4.18.1.0`](http://hackage.haskell.org/package/base-4.18.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/base-4.18.1.0/src/LICENSE) | Basic libraries | *(core library)* | +| `base64-bytestring` | [`1.2.1.0`](http://hackage.haskell.org/package/base64-bytestring-1.2.1.0) | [`BSD-3-Clause`](./licences/haskell/base64-bytestring-1.2.1.0/LICENSE) | Fast base64 encoding and decoding for ByteStrings | `simplexmq`, `websockets` | +| **`bytestring`** | [`0.11.5.2`](http://hackage.haskell.org/package/bytestring-0.11.5.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/bytestring-0.11.5.2/src/LICENSE) | Fast, compact, strict and lazy byte strings with a list interface | `SHA`, `aeson`, `appar`, `asn1-encoding`, `asn1-parse`, `asn1-types`, `attoparsec`, `base64-bytestring`, `binary`, `bitvec`, `blaze-builder`, `blaze-textual`, `bytestring-builder`, `case-insensitive`, `cereal`, `conduit`, `conduit-extra`, `cryptohash-md5`, `cryptohash-sha1`, `crypton`, `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `digest`, `direct-sqlcipher`, `email-validate`, `entropy`, `fast-logger`, `file-embed`, `filepath`, `hashable`, `http-types`, `http2`, `integer-conversion`, `iproute`, `libyaml`, `memory`, `monad-logger`, `mono-traversable`, `network`, `network-byte-order`, `network-transport`, `network-udp`, `parsec`, `pem`, `random`, `scientific`, `simplexmq`, `socks`, `sqlcipher-simple`, `streaming-commons`, `strict`, `terminal`, `text`, `text-short`, `tls`, `typed-process`, `unix`, `unix-time`, `unliftio`, `uuid`, `uuid-types`, `vector-algorithms`, `websockets`, `yaml`, `zip`, `zlib`, `zstd` | +| `composition` | [`1.0.2.2`](http://hackage.haskell.org/package/composition-1.0.2.2) | [`BSD-3-Clause`](./licences/haskell/composition-1.0.2.2/LICENSE) | Combinators for unorthodox function composition | `simplexmq` | +| `constraints` | [`0.14`](http://hackage.haskell.org/package/constraints-0.14) | [`BSD-2-Clause`](./licences/haskell/constraints-0.14/LICENSE) | Constraint manipulation | `simplexmq` | +| **`containers`** | [`0.6.7`](http://hackage.haskell.org/package/containers-0.6.7) | [`BSD-3-Clause`](http://hackage.haskell.org/package/containers-0.6.7/src/LICENSE) | Assorted concrete container types | `QuickCheck`, `aeson`, `attoparsec`, `bifunctors`, `binary`, `cereal`, `comonad`, `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `data-default-instances-containers`, `hashable`, `http2`, `indexed-traversable`, `iproute`, `mono-traversable`, `network-udp`, `resourcet`, `scientific`, `semialign`, `semigroupoids`, `simplexmq`, `sqlcipher-simple`, `th-abstraction`, `websockets`, `witherable`, `yaml`, `zip` | +| `crypton` | [`0.34`](http://hackage.haskell.org/package/crypton-0.34) | [`BSD-3-Clause`](./licences/haskell/crypton-0.34/LICENSE) | Cryptography Primitives sink | `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `simplexmq`, `tls` | +| `uuid` | [`1.3.15`](http://hackage.haskell.org/package/uuid-1.3.15) | [`BSD-3-Clause`](./licences/haskell/uuid-1.3.15/LICENSE) | For creating, comparing, parsing and printing Universally Unique Identifiers | | +| **`directory`** | [`1.3.8.1`](http://hackage.haskell.org/package/directory-1.3.8.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/directory-1.3.8.1/src/LICENSE) | Platform-agnostic library for filesystem operations | `conduit`, `conduit-extra`, `crypton-x509-store`, `easy-file`, `fast-logger`, `file-embed`, `network`, `process`, `simplexmq`, `streaming-commons`, `temporary`, `unliftio`, `yaml`, `zip` | +| `direct-sqlcipher` | [`2.3.28`](http://hackage.haskell.org/package/direct-sqlcipher-2.3.28) | *MISSING* | *MISSING* | `simplexmq`, `sqlcipher-simple` | +| `data-default` | [`0.7.1.1`](http://hackage.haskell.org/package/data-default-0.7.1.1) | [`BSD-3-Clause`](./licences/haskell/data-default-0.7.1.1/LICENSE) | A class for types with a default value | `simplexmq` | +| **`exceptions`** | [`0.10.7`](http://hackage.haskell.org/package/exceptions-0.10.7) | [`BSD-3-Clause`](http://hackage.haskell.org/package/exceptions-0.10.7/src/LICENSE) | Extensible optionally-pure exceptions | `aeson`, `conduit`, `filepath`, `monad-logger`, `resourcet`, `safe-exceptions`, `sqlcipher-simple`, `temporary`, `terminal`, `zip` | +| **`filepath`** | [`1.4.100.4`](http://hackage.haskell.org/package/filepath-1.4.100.4) | [`BSD-3-Clause`](http://hackage.haskell.org/package/filepath-1.4.100.4/src/LICENSE) | Library for manipulating FilePaths in a cross platform way. | `conduit`, `conduit-extra`, `crypton-x509-store`, `directory`, `easy-file`, `fast-logger`, `file-embed`, `hashable`, `process`, `simplexmq`, `temporary`, `unix`, `unliftio`, `yaml`, `zip` | +| `file-embed` | [`0.0.15.0`](http://hackage.haskell.org/package/file-embed-0.0.15.0) | [`BSD-2-Clause`](./licences/haskell/file-embed-0.0.15.0/LICENSE) | Use Template Haskell to embed file contents directly. | | +| `http-types` | [`0.12.4`](http://hackage.haskell.org/package/http-types-0.12.4) | [`BSD-3-Clause`](./licences/haskell/http-types-0.12.4/LICENSE) | Generic HTTP types for Haskell (for both client and server code). | `http2`, `simplexmq` | +| `http2` | [`5.0.0`](http://hackage.haskell.org/package/http2-5.0.0) | [`BSD-3-Clause`](./licences/haskell/http2-5.0.0/LICENSE) | HTTP/2 library | `simplexmq` | +| `email-validate` | [`2.3.2.19`](http://hackage.haskell.org/package/email-validate-2.3.2.19) | [`BSD-3-Clause`](./licences/haskell/email-validate-2.3.2.19/LICENSE) | Email address validation | | +| `memory` | [`0.18.0`](http://hackage.haskell.org/package/memory-0.18.0) | [`BSD-3-Clause`](./licences/haskell/memory-0.18.0/LICENSE) | memory and related abstraction stuff | `asn1-types`, `crypton`, `crypton-x509`, `crypton-x509-validation`, `cryptostore`, `pem`, `simplexmq`, `tls` | +| **`mtl`** | [`2.3.1`](http://hackage.haskell.org/package/mtl-2.3.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/mtl-2.3.1/src/LICENSE) | Monad classes for transformers, using functional dependencies | `conduit`, `constraints`, `crypton-x509-store`, `crypton-x509-validation`, `exceptions`, `monad-logger`, `parsec`, `random`, `resourcet`, `simple-logger`, `simplexmq`, `tls`, `yaml`, `zip` | +| `unliftio` | [`0.2.25.0`](http://hackage.haskell.org/package/unliftio-0.2.25.0) | [`MIT`](./licences/haskell/unliftio-0.2.25.0/LICENSE) | The MonadUnliftIO typeclass for unlifting monads to IO (batteries included) | `http2`, `simplexmq`, `time-manager` | +| `unliftio-core` | [`0.2.1.0`](http://hackage.haskell.org/package/unliftio-core-0.2.1.0) | [`MIT`](./licences/haskell/unliftio-core-0.2.1.0/LICENSE) | The MonadUnliftIO typeclass for unlifting monads to IO | `conduit`, `conduit-extra`, `monad-logger`, `resourcet`, `simplexmq`, `typed-process`, `unliftio` | +| `ansi-terminal` | [`1.0`](http://hackage.haskell.org/package/ansi-terminal-1.0) | [`BSD-3-Clause`](./licences/haskell/ansi-terminal-1.0/LICENSE) | Simple ANSI terminal support | `prettyprinter-ansi-terminal` | +| `network` | [`3.1.4.0`](http://hackage.haskell.org/package/network-3.1.4.0) | [`BSD-3-Clause`](./licences/haskell/network-3.1.4.0/LICENSE) | Low-level networking interface | `conduit-extra`, `http2`, `iproute`, `network-udp`, `simplexmq`, `socks`, `streaming-commons`, `tls`, `websockets` | +| `network-transport` | [`0.5.6`](http://hackage.haskell.org/package/network-transport-0.5.6) | [`BSD-3-Clause`](./licences/haskell/network-transport-0.5.6/LICENSE) | Network abstraction layer | `simplexmq` | +| **`process`** | [`1.6.17.0`](http://hackage.haskell.org/package/process-1.6.17.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/process-1.6.17.0/src/LICENSE) | Process libraries | `conduit-extra`, `optparse-applicative`, `simplexmq`, `streaming-commons`, `typed-process`, `unliftio` | +| `optparse-applicative` | [`0.18.1.0`](http://hackage.haskell.org/package/optparse-applicative-0.18.1.0) | [`BSD-3-Clause`](./licences/haskell/optparse-applicative-0.18.1.0/LICENSE) | Utilities and combinators for parsing command line options | `simplexmq` | +| `record-hasfield` | [`1.0`](http://hackage.haskell.org/package/record-hasfield-1.0) | [`BSD-3-Clause`](./licences/haskell/record-hasfield-1.0/LICENSE) | A version of GHC.Records as available in future GHCs. | | +| `random` | [`1.2.1.1`](http://hackage.haskell.org/package/random-1.2.1.1) | [`BSD-3-Clause`](./licences/haskell/random-1.2.1.1/LICENSE) | Pseudo-random number generation | `QuickCheck`, `simplexmq`, `streaming-commons`, `temporary`, `uuid`, `uuid-types`, `websockets` | +| `socks` | [`0.6.1`](http://hackage.haskell.org/package/socks-0.6.1) | [`BSD-3-Clause`](./licences/haskell/socks-0.6.1/LICENSE) | Socks proxy (ver 5) | `simplexmq` | +| `scientific` | [`0.3.7.0`](http://hackage.haskell.org/package/scientific-0.3.7.0) | [`BSD-3-Clause`](./licences/haskell/scientific-0.3.7.0/LICENSE) | Numbers represented using scientific notation | `aeson`, `attoparsec`, `yaml` | +| `simple-logger` | [`0.1.1`](http://hackage.haskell.org/package/simple-logger-0.1.1) | [`MIT`](./licences/haskell/simple-logger-0.1.1/LICENSE) | A very simple but efficient logging framework | `simplexmq` | +| `simplexmq` | [`6.4.0.1`](http://hackage.haskell.org/package/simplexmq-6.4.0.1) | *MISSING* | *MISSING* | | +| `aeson` | [`2.2.1.0`](http://hackage.haskell.org/package/aeson-2.2.1.0) | [`BSD-3-Clause`](./licences/haskell/aeson-2.2.1.0/LICENSE) | Fast JSON parsing and encoding | `simplexmq`, `yaml` | +| `sqlcipher-simple` | [`0.4.18.1`](http://hackage.haskell.org/package/sqlcipher-simple-0.4.18.1) | *MISSING* | *MISSING* | `simplexmq` | +| **`stm`** | [`2.5.1.0`](http://hackage.haskell.org/package/stm-2.5.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/stm-2.5.1.0/src/LICENSE) | Software Transactional Memory | `StateVar`, `async`, `conduit-extra`, `exceptions`, `fast-logger`, `http2`, `monad-control`, `monad-logger`, `simplexmq`, `stm-chans`, `streaming-commons`, `terminal`, `transformers-base`, `typed-process`, `unliftio` | +| `async` | [`2.2.5`](http://hackage.haskell.org/package/async-2.2.5) | [`BSD-3-Clause`](./licences/haskell/async-2.2.5/LICENSE) | Run IO operations asynchronously and wait for their results | `conduit-extra`, `http2`, `simplexmq`, `streaming-commons`, `terminal`, `tls`, `typed-process`, `unliftio`, `websockets` | +| **`template-haskell`** | [`2.20.0.0`](http://hackage.haskell.org/package/template-haskell-2.20.0.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/template-haskell-2.20.0.0/src/LICENSE) | Support library for Template Haskell | `OneTuple`, `QuickCheck`, `aeson`, `bifunctors`, `bytestring`, `containers`, `email-validate`, `exceptions`, `file-embed`, `filepath`, `monad-logger`, `network-uri`, `primitive`, `scientific`, `semigroupoids`, `sqlcipher-simple`, `tagged`, `text`, `text-short`, `th-abstraction`, `th-compat`, `unordered-containers`, `uuid-types`, `yaml` | +| **`text`** | [`2.0.2`](http://hackage.haskell.org/package/text-2.0.2) | [`BSD-2-Clause`](http://hackage.haskell.org/package/text-2.0.2/src/LICENSE) | An efficient packed Unicode text type. | `aeson`, `attoparsec`, `blaze-builder`, `blaze-textual`, `case-insensitive`, `conduit`, `conduit-extra`, `direct-sqlcipher`, `fast-logger`, `hashable`, `http-types`, `ini`, `integer-conversion`, `monad-logger`, `mono-traversable`, `optparse-applicative`, `parsec`, `prettyprinter`, `prettyprinter-ansi-terminal`, `scientific`, `simple-logger`, `simplexmq`, `sqlcipher-simple`, `streaming-commons`, `strict`, `terminal`, `text-iso8601`, `text-short`, `uuid`, `uuid-types`, `websockets`, `yaml`, `zip` | +| **`time`** | [`1.12.2`](http://hackage.haskell.org/package/time-1.12.2) | [`BSD-2-Clause`](http://hackage.haskell.org/package/time-1.12.2/src/LICENSE) | A time library | `aeson`, `directory`, `easy-file`, `iso8601-time`, `simplexmq`, `sqlcipher-simple`, `text-iso8601`, `time-compat`, `unix`, `unliftio`, `uuid`, `zip` | +| `tls` | [`1.9.0`](http://hackage.haskell.org/package/tls-1.9.0) | [`BSD-3-Clause`](./licences/haskell/tls-1.9.0/LICENSE) | TLS/SSL protocol native implementation (Server and Client) | `simplexmq` | +| `terminal` | [`0.2.0.0`](http://hackage.haskell.org/package/terminal-0.2.0.0) | [`BSD-3-Clause`](./licences/haskell/terminal-0.2.0.0/LICENSE) | Portable terminal interaction library | | +| `attoparsec` | [`0.14.4`](http://hackage.haskell.org/package/attoparsec-0.14.4) | [`BSD-3-Clause`](./licences/haskell/attoparsec-0.14.4/LICENSE) | Fast combinator parsing for bytestrings and text | `conduit-extra`, `email-validate`, `ini`, `simplexmq`, `sqlcipher-simple`, `websockets`, `yaml` | +| `websockets` | [`0.12.7.3`](http://hackage.haskell.org/package/websockets-0.12.7.3) | [`BSD-3-Clause`](./licences/haskell/websockets-0.12.7.3/LICENCE) | A sensible and clean way to write WebSocket-capable servers in Haskell. | `simplexmq` | +| `zip` | [`2.0.0`](http://hackage.haskell.org/package/zip-2.0.0) | [`BSD-3-Clause`](./licences/haskell/zip-2.0.0/LICENSE.md) | Operations on zip archives | | + +## Indirect transitive dependencies + +| Name | Version | [SPDX](https://spdx.org/licenses/) License Id | Description | Depended upon by | +| --- | --- | --- | --- | --- | +| `OneTuple` | [`0.4.1.1`](http://hackage.haskell.org/package/OneTuple-0.4.1.1) | [`BSD-3-Clause`](./licences/haskell/OneTuple-0.4.1.1/LICENSE) | Singleton Tuple | `aeson`, `indexed-traversable-instances` | +| `Only` | [`0.1`](http://hackage.haskell.org/package/Only-0.1) | [`BSD-3-Clause`](./licences/haskell/Only-0.1/LICENSE) | The 1-tuple type or single-value "collection" | `sqlcipher-simple` | +| `QuickCheck` | [`2.14.3`](http://hackage.haskell.org/package/QuickCheck-2.14.3) | [`BSD-3-Clause`](./licences/haskell/QuickCheck-2.14.3/LICENSE) | Automatic testing of Haskell programs | `aeson` | +| `SHA` | [`1.6.4.4`](http://hackage.haskell.org/package/SHA-1.6.4.4) | [`BSD-3-Clause`](./licences/haskell/SHA-1.6.4.4/LICENSE) | Implementations of the SHA suite of message digest functions | `websockets` | +| `StateVar` | [`1.2.2`](http://hackage.haskell.org/package/StateVar-1.2.2) | [`BSD-3-Clause`](./licences/haskell/StateVar-1.2.2/LICENSE) | State variables | `contravariant` | +| **`array`** | [`0.5.5.0`](http://hackage.haskell.org/package/array-0.5.5.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/array-0.5.5.0/src/LICENSE) | Mutable and immutable arrays | `SHA`, `attoparsec`, `binary`, `cereal`, `containers`, `deepseq`, `fast-logger`, `http-types`, `http2`, `indexed-traversable`, `integer-logarithms`, `stm`, `streaming-commons`, `text` | +| `bifunctors` | [`5.6.1`](http://hackage.haskell.org/package/bifunctors-5.6.1) | [`BSD-3-Clause`](./licences/haskell/bifunctors-5.6.1/LICENSE) | Bifunctors | `semigroupoids` | +| **`binary`** | [`0.8.9.1`](http://hackage.haskell.org/package/binary-0.8.9.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/binary-0.8.9.1/src/LICENSE) | Binary serialisation for Haskell values using lazy ByteStrings | `SHA`, `constraints`, `network-transport`, `scientific`, `strict`, `text`, `text-short`, `these`, `unix-time`, `uuid`, `uuid-types`, `websockets` | +| `blaze-builder` | [`0.4.2.3`](http://hackage.haskell.org/package/blaze-builder-0.4.2.3) | [`BSD-3-Clause`](./licences/haskell/blaze-builder-0.4.2.3/LICENSE) | Efficient buffered output. | `blaze-textual`, `sqlcipher-simple` | +| `blaze-textual` | [`0.2.3.1`](http://hackage.haskell.org/package/blaze-textual-0.2.3.1) | [`BSD-3-Clause`](./licences/haskell/blaze-textual-0.2.3.1/LICENSE) | Fast rendering of common datatypes | `sqlcipher-simple` | +| `boring` | [`0.2.1`](http://hackage.haskell.org/package/boring-0.2.1) | [`BSD-3-Clause`](./licences/haskell/boring-0.2.1/LICENSE) | Boring and Absurd types | `constraints` | +| `base-orphans` | [`0.9.1`](http://hackage.haskell.org/package/base-orphans-0.9.1) | [`MIT`](./licences/haskell/base-orphans-0.9.1/LICENSE) | Backwards-compatible orphan instances for base | `distributive`, `semigroupoids`, `time-compat`, `transformers-base`, `witherable` | +| `basement` | [`0.0.16`](http://hackage.haskell.org/package/basement-0.0.16) | [`BSD-3-Clause`](./licences/haskell/basement-0.0.16/LICENSE) | Foundation scrap box of array & string | `crypton`, `cryptostore`, `memory`, `pem`, `socks` | +| `bitvec` | [`1.1.5.0`](http://hackage.haskell.org/package/bitvec-1.1.5.0) | [`BSD-3-Clause`](./licences/haskell/bitvec-1.1.5.0/LICENSE) | Space-efficient bit vectors | `vector-algorithms` | +| `byteorder` | [`1.0.4`](http://hackage.haskell.org/package/byteorder-1.0.4) | [`BSD-3-Clause`](./licences/haskell/byteorder-1.0.4/LICENSE) | Exposes the native endianness or byte ordering of the system. | `iproute` | +| `bytestring-builder` | [`0.10.8.2.0`](http://hackage.haskell.org/package/bytestring-builder-0.10.8.2.0) | [`BSD-3-Clause`](./licences/haskell/bytestring-builder-0.10.8.2.0/LICENSE) | The new bytestring builder, packaged outside of GHC | `websockets` | +| `clock` | [`0.8.4`](http://hackage.haskell.org/package/clock-0.8.4) | [`BSD-3-Clause`](./licences/haskell/clock-0.8.4/LICENSE) | High-resolution clock functions: monotonic, realtime, cputime. | `websockets` | +| `colour` | [`2.3.6`](http://hackage.haskell.org/package/colour-2.3.6) | [`MIT`](./licences/haskell/colour-2.3.6/LICENSE) | A model for human colour/color perception | `ansi-terminal`, `ansi-terminal-types` | +| `comonad` | [`5.0.8`](http://hackage.haskell.org/package/comonad-5.0.8) | [`BSD-3-Clause`](./licences/haskell/comonad-5.0.8/LICENSE) | Comonads | `bifunctors`, `semigroupoids` | +| `conduit` | [`1.3.5`](http://hackage.haskell.org/package/conduit-1.3.5) | [`MIT`](./licences/haskell/conduit-1.3.5/LICENSE) | Streaming data processing library. | `conduit-extra`, `libyaml`, `monad-logger`, `yaml`, `zip` | +| `conduit-extra` | [`1.3.6`](http://hackage.haskell.org/package/conduit-extra-1.3.6) | [`MIT`](./licences/haskell/conduit-extra-1.3.6/LICENSE) | Batteries included conduit: adapters for common libraries. | `monad-logger`, `zip` | +| `contravariant` | [`1.5.5`](http://hackage.haskell.org/package/contravariant-1.5.5) | [`BSD-3-Clause`](./licences/haskell/contravariant-1.5.5/LICENSE) | Contravariant functors | `semigroupoids` | +| `cereal` | [`0.5.8.3`](http://hackage.haskell.org/package/cereal-0.5.8.3) | [`BSD-3-Clause`](./licences/haskell/cereal-0.5.8.3/LICENSE) | A binary serialization library | `socks`, `tls`, `zip` | +| `cryptohash-md5` | [`0.11.101.0`](http://hackage.haskell.org/package/cryptohash-md5-0.11.101.0) | [`BSD-3-Clause`](./licences/haskell/cryptohash-md5-0.11.101.0/LICENSE) | Fast, pure and practical MD5 implementation | `uuid` | +| `cryptohash-sha1` | [`0.11.101.0`](http://hackage.haskell.org/package/cryptohash-sha1-0.11.101.0) | [`BSD-3-Clause`](./licences/haskell/cryptohash-sha1-0.11.101.0/LICENSE) | Fast, pure and practical SHA-1 implementation | `uuid` | +| `crypton-x509` | [`1.7.6`](http://hackage.haskell.org/package/crypton-x509-1.7.6) | [`BSD-3-Clause`](./licences/haskell/crypton-x509-1.7.6/LICENSE) | X509 reader and writer | `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `simplexmq`, `tls` | +| `crypton-x509-store` | [`1.6.9`](http://hackage.haskell.org/package/crypton-x509-store-1.6.9) | [`BSD-3-Clause`](./licences/haskell/crypton-x509-store-1.6.9/LICENSE) | X.509 collection accessing and storing methods | `crypton-x509-validation`, `simplexmq`, `tls` | +| `crypton-x509-validation` | [`1.6.12`](http://hackage.haskell.org/package/crypton-x509-validation-1.6.12) | [`BSD-3-Clause`](./licences/haskell/crypton-x509-validation-1.6.12/LICENSE) | X.509 Certificate and CRL validation | `cryptostore`, `simplexmq`, `tls` | +| `cryptostore` | [`0.3.0.1`](http://hackage.haskell.org/package/cryptostore-0.3.0.1) | [`BSD-3-Clause`](./licences/haskell/cryptostore-0.3.0.1/LICENSE) | Serialization of cryptographic data types | `simplexmq` | +| `case-insensitive` | [`1.2.1.0`](http://hackage.haskell.org/package/case-insensitive-1.2.1.0) | [`BSD-3-Clause`](./licences/haskell/case-insensitive-1.2.1.0/LICENSE) | Case insensitive string comparison | `http-types`, `http2`, `simplexmq`, `websockets`, `zip` | +| `uuid-types` | [`1.0.5.1`](http://hackage.haskell.org/package/uuid-types-1.0.5.1) | [`BSD-3-Clause`](./licences/haskell/uuid-types-1.0.5.1/LICENSE) | Type definitions for Universally Unique Identifiers | `aeson`, `uuid` | +| **`deepseq`** | [`1.4.8.1`](http://hackage.haskell.org/package/deepseq-1.4.8.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/deepseq-1.4.8.1/src/LICENSE) | Deep evaluation of data structures | `Only`, `QuickCheck`, `aeson`, `attoparsec`, `bitvec`, `blaze-builder`, `bytestring`, `bytestring-builder`, `case-insensitive`, `constraints`, `containers`, `crypton`, `data-fix`, `dlist`, `filepath`, `hashable`, `hourglass`, `memory`, `network`, `network-transport`, `network-uri`, `pretty`, `primitive`, `process`, `psqueues`, `random`, `safe-exceptions`, `scientific`, `splitmix`, `strict`, `tagged`, `text`, `text-short`, `these`, `time`, `time-compat`, `unliftio`, `unordered-containers`, `uuid-types`, `vector`, `zstd` | +| `digest` | [`0.0.1.7`](http://hackage.haskell.org/package/digest-0.0.1.7) | [`BSD-2-Clause`](./licences/haskell/digest-0.0.1.7/LICENSE) | Various hashes for bytestrings; CRC32 and Adler32 for now. | `zip` | +| `dlist` | [`1.0`](http://hackage.haskell.org/package/dlist-1.0) | [`BSD-3-Clause`](./licences/haskell/dlist-1.0/license.md) | Difference lists | `aeson`, `data-default-instances-dlist`, `zip` | +| `distributive` | [`0.6.2.1`](http://hackage.haskell.org/package/distributive-0.6.2.1) | [`BSD-3-Clause`](./licences/haskell/distributive-0.6.2.1/LICENSE) | Distributive functors -- Dual to Traversable | `comonad`, `semigroupoids` | +| `data-default-class` | [`0.1.2.0`](http://hackage.haskell.org/package/data-default-class-0.1.2.0) | [`BSD-3-Clause`](./licences/haskell/data-default-class-0.1.2.0/LICENSE) | A class for types with a default value | `crypton-x509-validation`, `data-default`, `data-default-instances-containers`, `data-default-instances-dlist`, `data-default-instances-old-locale`, `tls` | +| `data-default-instances-containers` | [`0.0.1`](http://hackage.haskell.org/package/data-default-instances-containers-0.0.1) | [`BSD-3-Clause`](./licences/haskell/data-default-instances-containers-0.0.1/LICENSE) | Default instances for types in containers | `data-default` | +| `data-default-instances-dlist` | [`0.0.1`](http://hackage.haskell.org/package/data-default-instances-dlist-0.0.1) | [`BSD-3-Clause`](./licences/haskell/data-default-instances-dlist-0.0.1/LICENSE) | Default instances for types in dlist | `data-default` | +| `data-default-instances-old-locale` | [`0.0.1`](http://hackage.haskell.org/package/data-default-instances-old-locale-0.0.1) | [`BSD-3-Clause`](./licences/haskell/data-default-instances-old-locale-0.0.1/LICENSE) | Default instances for types in old-locale | `data-default` | +| `data-fix` | [`0.3.2`](http://hackage.haskell.org/package/data-fix-0.3.2) | [`BSD-3-Clause`](./licences/haskell/data-fix-0.3.2/LICENSE) | Fixpoint data types | `aeson` | +| `fast-logger` | [`3.2.2`](http://hackage.haskell.org/package/fast-logger-3.2.2) | [`BSD-3-Clause`](./licences/haskell/fast-logger-3.2.2/LICENSE) | A fast logging system | `monad-logger`, `simple-logger` | +| **`ghc-bignum`** | [`1.3`](http://hackage.haskell.org/package/ghc-bignum-1.3) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-bignum-1.3/src/LICENSE) | GHC BigNum library | `base`, `bitvec`, `hashable`, `integer-gmp`, `integer-logarithms` | +| **`ghc-boot-th`** | [`9.6.3`](http://hackage.haskell.org/package/ghc-boot-th-9.6.3) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-boot-th-9.6.3/src/LICENSE) | Shared functionality between GHC and the @template-haskell@ library | `template-haskell` | +| **`ghc-prim`** | [`0.10.0`](http://hackage.haskell.org/package/ghc-prim-0.10.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-prim-0.10.0/src/LICENSE) | GHC primitives | *(core library)* | +| `generically` | [`0.1.1`](http://hackage.haskell.org/package/generically-0.1.1) | [`BSD-3-Clause`](./licences/haskell/generically-0.1.1/LICENSE) | Generically newtype to use with DerivingVia | `aeson` | +| `hourglass` | [`0.2.12`](http://hackage.haskell.org/package/hourglass-0.2.12) | [`BSD-3-Clause`](./licences/haskell/hourglass-0.2.12/LICENSE) | simple performant time related library | `asn1-encoding`, `asn1-types`, `crypton-x509`, `crypton-x509-validation`, `cryptostore`, `simplexmq` | +| `hashable` | [`1.4.3.0`](http://hackage.haskell.org/package/hashable-1.4.3.0) | [`BSD-3-Clause`](./licences/haskell/hashable-1.4.3.0/LICENSE) | A class for types that can be converted to a hash value | `aeson`, `async`, `case-insensitive`, `constraints`, `data-fix`, `mono-traversable`, `network-transport`, `psqueues`, `scientific`, `semialign`, `semigroupoids`, `simplexmq`, `strict`, `text-short`, `these`, `time-compat`, `unordered-containers`, `uuid-types`, `witherable` | +| **`integer-gmp`** | [`1.1`](http://hackage.haskell.org/package/integer-gmp-1.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/integer-gmp-1.1/src/LICENSE) | Integer library based on GMP | *(core library)* | +| `libyaml` | [`0.1.2`](http://hackage.haskell.org/package/libyaml-0.1.2) | [`BSD-3-Clause`](./licences/haskell/libyaml-0.1.2/LICENSE) | Low-level, streaming YAML interface. | `yaml` | +| `old-locale` | [`1.0.0.7`](http://hackage.haskell.org/package/old-locale-1.0.0.7) | [`BSD-3-Clause`](./licences/haskell/old-locale-1.0.0.7/LICENSE) | locale library | `blaze-textual`, `data-default-instances-old-locale`, `old-time` | +| `old-time` | [`1.1.0.4`](http://hackage.haskell.org/package/old-time-1.1.0.4) | [`BSD-3-Clause`](./licences/haskell/old-time-1.1.0.4/LICENSE) | Time library | `unix-time` | +| `lifted-base` | [`0.2.3.12`](http://hackage.haskell.org/package/lifted-base-0.2.3.12) | [`BSD-3-Clause`](./licences/haskell/lifted-base-0.2.3.12/LICENSE) | lifted IO operations from the base library | `monad-logger` | +| `mono-traversable` | [`1.0.15.3`](http://hackage.haskell.org/package/mono-traversable-1.0.15.3) | [`MIT`](./licences/haskell/mono-traversable-1.0.15.3/LICENSE) | Type classes for mapping, folding, and traversing monomorphic containers | `conduit` | +| `monad-control` | [`1.0.3.1`](http://hackage.haskell.org/package/monad-control-1.0.3.1) | [`BSD-3-Clause`](./licences/haskell/monad-control-1.0.3.1/LICENSE) | Lift control operations, like exception catching, through monad transformers | `lifted-base`, `monad-logger`, `zip` | +| `monad-logger` | [`0.3.40`](http://hackage.haskell.org/package/monad-logger-0.3.40) | [`MIT`](./licences/haskell/monad-logger-0.3.40/LICENSE) | A class of monads which can log messages. | `simple-logger` | +| `monad-loops` | [`0.4.3`](http://hackage.haskell.org/package/monad-loops-0.4.3) | [`LicenseRef-PublicDomain`](http://hackage.haskell.org/package/monad-loops-0.4.3) | Monadic loops | `monad-logger` | +| `ini` | [`0.4.2`](http://hackage.haskell.org/package/ini-0.4.2) | [`BSD-3-Clause`](./licences/haskell/ini-0.4.2/LICENSE) | Configuration files in the INI format. | `simplexmq` | +| `indexed-traversable` | [`0.1.3`](http://hackage.haskell.org/package/indexed-traversable-0.1.3) | [`BSD-2-Clause`](./licences/haskell/indexed-traversable-0.1.3/LICENSE) | FunctorWithIndex, FoldableWithIndex, TraversableWithIndex | `aeson`, `comonad`, `indexed-traversable-instances`, `semialign`, `witherable` | +| `indexed-traversable-instances` | [`0.1.1.2`](http://hackage.haskell.org/package/indexed-traversable-instances-0.1.1.2) | [`BSD-2-Clause`](./licences/haskell/indexed-traversable-instances-0.1.1.2/LICENSE) | More instances of FunctorWithIndex, FoldableWithIndex, TraversableWithIndex | `semialign`, `witherable` | +| `unordered-containers` | [`0.2.19.1`](http://hackage.haskell.org/package/unordered-containers-0.2.19.1) | [`BSD-3-Clause`](./licences/haskell/unordered-containers-0.2.19.1/LICENSE) | Efficient hashing-based container types | `aeson`, `indexed-traversable-instances`, `ini`, `mono-traversable`, `semialign`, `semigroupoids`, `witherable`, `yaml` | +| `ansi-terminal-types` | [`0.11.5`](http://hackage.haskell.org/package/ansi-terminal-types-0.11.5) | [`BSD-3-Clause`](./licences/haskell/ansi-terminal-types-0.11.5/LICENSE) | Types and functions used to represent SGR aspects | `ansi-terminal` | +| `integer-conversion` | [`0.1.0.1`](http://hackage.haskell.org/package/integer-conversion-0.1.0.1) | [`BSD-3-Clause`](./licences/haskell/integer-conversion-0.1.0.1/LICENSE) | Conversion from strings to Integer | `aeson`, `text-iso8601` | +| `integer-logarithms` | [`1.0.3.1`](http://hackage.haskell.org/package/integer-logarithms-1.0.3.1) | [`MIT`](./licences/haskell/integer-logarithms-1.0.3.1/LICENSE) | Integer logarithms. | `aeson`, `scientific` | +| `entropy` | [`0.4.1.10`](http://hackage.haskell.org/package/entropy-0.4.1.10) | [`BSD-3-Clause`](./licences/haskell/entropy-0.4.1.10/LICENSE) | A platform independent entropy source | `uuid`, `websockets` | +| `network-byte-order` | [`0.1.7`](http://hackage.haskell.org/package/network-byte-order-0.1.7) | [`BSD-3-Clause`](./licences/haskell/network-byte-order-0.1.7/LICENSE) | Network byte order utilities | `http2` | +| `network-control` | [`0.0.2`](http://hackage.haskell.org/package/network-control-0.0.2) | [`BSD-3-Clause`](./licences/haskell/network-control-0.0.2/LICENSE) | Library to control network protocols | `http2` | +| `network-udp` | [`0.0.0`](http://hackage.haskell.org/package/network-udp-0.0.0) | [`BSD-3-Clause`](./licences/haskell/network-udp-0.0.0/LICENSE) | UDP library | `simplexmq` | +| `network-info` | [`0.2.1`](http://hackage.haskell.org/package/network-info-0.2.1) | [`BSD-3-Clause`](./licences/haskell/network-info-0.2.1/LICENSE) | Access the local computer's basic network configuration | `simplexmq`, `uuid` | +| `network-uri` | [`2.6.4.2`](http://hackage.haskell.org/package/network-uri-2.6.4.2) | [`BSD-3-Clause`](./licences/haskell/network-uri-2.6.4.2/LICENSE) | URI manipulation | `aeson` | +| `unix-compat` | [`0.7.1`](http://hackage.haskell.org/package/unix-compat-0.7.1) | [`BSD-3-Clause`](./licences/haskell/unix-compat-0.7.1/LICENSE) | Portable POSIX-compatibility layer. | `fast-logger` | +| `unix-time` | [`0.4.11`](http://hackage.haskell.org/package/unix-time-0.4.11) | [`BSD-3-Clause`](./licences/haskell/unix-time-0.4.11/LICENSE) | Unix time parser/formatter and utilities | `fast-logger`, `http2`, `network-control`, `tls` | +| **`parsec`** | [`3.1.16.1`](http://hackage.haskell.org/package/parsec-3.1.16.1) | [`BSD-2-Clause`](http://hackage.haskell.org/package/parsec-3.1.16.1/src/LICENSE) | Monadic parser combinators | `network-uri` | +| `pem` | [`0.2.4`](http://hackage.haskell.org/package/pem-0.2.4) | [`BSD-3-Clause`](./licences/haskell/pem-0.2.4/LICENSE) | Privacy Enhanced Mail (PEM) format reader and writer. | `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore` | +| `appar` | [`0.1.8`](http://hackage.haskell.org/package/appar-0.1.8) | [`BSD-3-Clause`](./licences/haskell/appar-0.1.8/LICENSE) | A simple applicative parser | `iproute` | +| **`pretty`** | [`1.1.3.6`](http://hackage.haskell.org/package/pretty-1.1.3.6) | [`BSD-3-Clause`](http://hackage.haskell.org/package/pretty-1.1.3.6/src/LICENSE) | Pretty-printing library | `template-haskell` | +| `primitive` | [`0.9.0.0`](http://hackage.haskell.org/package/primitive-0.9.0.0) | [`BSD-3-Clause`](./licences/haskell/primitive-0.9.0.0/LICENSE) | Primitive memory-related operations | `aeson`, `bitvec`, `conduit`, `conduit-extra`, `integer-conversion`, `resourcet`, `scientific`, `vector`, `vector-algorithms` | +| `iproute` | [`1.7.12`](http://hackage.haskell.org/package/iproute-1.7.12) | [`BSD-3-Clause`](./licences/haskell/iproute-1.7.12/LICENSE) | IP Routing Table | `network-udp`, `simplexmq` | +| `prettyprinter` | [`1.7.1`](http://hackage.haskell.org/package/prettyprinter-1.7.1) | [`BSD-2-Clause`](./licences/haskell/prettyprinter-1.7.1/LICENSE.md) | A modern, easy to use, well-documented, extensible pretty-printer. | `optparse-applicative`, `prettyprinter-ansi-terminal`, `terminal` | +| `prettyprinter-ansi-terminal` | [`1.1.3`](http://hackage.haskell.org/package/prettyprinter-ansi-terminal-1.1.3) | [`BSD-2-Clause`](./licences/haskell/prettyprinter-ansi-terminal-1.1.3/LICENSE.md) | ANSI terminal backend for the »prettyprinter« package. | `optparse-applicative` | +| `psqueues` | [`0.2.8.0`](http://hackage.haskell.org/package/psqueues-0.2.8.0) | [`BSD-3-Clause`](./licences/haskell/psqueues-0.2.8.0/LICENSE) | Pure priority search queues | `network-control` | +| `resourcet` | [`1.3.0`](http://hackage.haskell.org/package/resourcet-1.3.0) | [`BSD-3-Clause`](./licences/haskell/resourcet-1.3.0/LICENSE) | Deterministic allocation and freeing of scarce resources. | `conduit`, `conduit-extra`, `libyaml`, `monad-logger`, `yaml`, `zip` | +| `iso8601-time` | [`0.1.5`](http://hackage.haskell.org/package/iso8601-time-0.1.5) | [`MIT`](http://hackage.haskell.org/package/iso8601-time-0.1.5) | Convert to/from the ISO 8601 time format | `simplexmq` | +| `safe-exceptions` | [`0.1.7.4`](http://hackage.haskell.org/package/safe-exceptions-0.1.7.4) | [`MIT`](./licences/haskell/safe-exceptions-0.1.7.4/LICENSE) | Safe, consistent, and easy exception handling | `unliftio` | +| `semigroupoids` | [`6.0.0.1`](http://hackage.haskell.org/package/semigroupoids-6.0.0.1) | [`BSD-2-Clause`](./licences/haskell/semigroupoids-6.0.0.1/LICENSE) | Semigroupoids: Category sans id | `semialign` | +| `semialign` | [`1.3`](http://hackage.haskell.org/package/semialign-1.3) | [`BSD-3-Clause`](./licences/haskell/semialign-1.3/LICENSE) | Align and Zip type-classes from the common Semialign ancestor. | `aeson` | +| `asn1-encoding` | [`0.9.6`](http://hackage.haskell.org/package/asn1-encoding-0.9.6) | [`BSD-3-Clause`](./licences/haskell/asn1-encoding-0.9.6/LICENSE) | ASN1 data reader and writer in RAW, BER and DER forms | `asn1-parse`, `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `simplexmq`, `tls` | +| `asn1-parse` | [`0.9.5`](http://hackage.haskell.org/package/asn1-parse-0.9.5) | [`BSD-3-Clause`](./licences/haskell/asn1-parse-0.9.5/LICENSE) | Simple monadic parser for ASN1 stream types. | `crypton-x509` | +| `asn1-types` | [`0.3.4`](http://hackage.haskell.org/package/asn1-types-0.3.4) | [`BSD-3-Clause`](./licences/haskell/asn1-types-0.3.4/LICENSE) | ASN.1 types | `asn1-encoding`, `asn1-parse`, `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `simplexmq`, `tls` | +| `split` | [`0.2.4`](http://hackage.haskell.org/package/split-0.2.4) | [`BSD-3-Clause`](./licences/haskell/split-0.2.4/LICENSE) | Combinator library for splitting lists. | `mono-traversable` | +| `splitmix` | [`0.1.0.5`](http://hackage.haskell.org/package/splitmix-0.1.0.5) | [`BSD-3-Clause`](./licences/haskell/splitmix-0.1.0.5/LICENSE) | Fast Splittable PRNG | `QuickCheck`, `random` | +| `assoc` | [`1.1`](http://hackage.haskell.org/package/assoc-1.1) | [`BSD-3-Clause`](./licences/haskell/assoc-1.1/LICENSE) | swap and assoc: Symmetric and Semigroupy Bifunctors | `bifunctors`, `strict`, `these` | +| `stm-chans` | [`3.0.0.9`](http://hackage.haskell.org/package/stm-chans-3.0.0.9) | [`BSD-3-Clause`](./licences/haskell/stm-chans-3.0.0.9/LICENSE) | Additional types of channels for STM. | `monad-logger` | +| `strict` | [`0.5`](http://hackage.haskell.org/package/strict-0.5) | [`BSD-3-Clause`](./licences/haskell/strict-0.5/LICENSE) | Strict data types and String IO. | `aeson` | +| `streaming-commons` | [`0.2.2.6`](http://hackage.haskell.org/package/streaming-commons-0.2.2.6) | [`MIT`](./licences/haskell/streaming-commons-0.2.2.6/LICENSE) | Common lower-level functions needed by various streaming data libraries | `conduit-extra`, `websockets` | +| `easy-file` | [`0.2.5`](http://hackage.haskell.org/package/easy-file-0.2.5) | [`BSD-3-Clause`](./licences/haskell/easy-file-0.2.5/LICENSE) | Cross-platform File handling | `fast-logger` | +| `auto-update` | [`0.1.6`](http://hackage.haskell.org/package/auto-update-0.1.6) | [`MIT`](./licences/haskell/auto-update-0.1.6/LICENSE) | Efficiently run periodic, on-demand actions | `fast-logger`, `time-manager` | +| `tagged` | [`0.8.8`](http://hackage.haskell.org/package/tagged-0.8.8) | [`BSD-3-Clause`](./licences/haskell/tagged-0.8.8/LICENSE) | Haskell 98 phantom types to avoid unsafely passing dummy arguments | `aeson`, `assoc`, `bifunctors`, `boring`, `comonad`, `distributive`, `indexed-traversable-instances`, `semialign`, `semigroupoids` | +| `th-abstraction` | [`0.6.0.0`](http://hackage.haskell.org/package/th-abstraction-0.6.0.0) | [`ISC`](./licences/haskell/th-abstraction-0.6.0.0/LICENSE) | Nicer interface for reified information about data types | `aeson`, `bifunctors` | +| `th-compat` | [`0.1.4`](http://hackage.haskell.org/package/th-compat-0.1.4) | [`BSD-3-Clause`](./licences/haskell/th-compat-0.1.4/LICENSE) | Backward- (and forward-)compatible Quote and Code types | `network-uri` | +| `these` | [`1.2`](http://hackage.haskell.org/package/these-1.2) | [`BSD-3-Clause`](./licences/haskell/these-1.2/LICENSE) | An either-or-both data type. | `aeson`, `semialign`, `strict` | +| `time-compat` | [`1.9.6.1`](http://hackage.haskell.org/package/time-compat-1.9.6.1) | [`BSD-3-Clause`](./licences/haskell/time-compat-1.9.6.1/LICENSE) | Compatibility package for time | `aeson`, `text-iso8601` | +| `time-manager` | [`0.0.1`](http://hackage.haskell.org/package/time-manager-0.0.1) | [`MIT`](./licences/haskell/time-manager-0.0.1/LICENSE) | Scalable timer | `http2`, `simplexmq` | +| `temporary` | [`1.3`](http://hackage.haskell.org/package/temporary-1.3) | [`BSD-3-Clause`](./licences/haskell/temporary-1.3/LICENSE) | Portable temporary file and directory support | `simplexmq` | +| **`transformers`** | [`0.6.1.0`](http://hackage.haskell.org/package/transformers-0.6.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/transformers-0.6.1.0/src/LICENSE) | Concrete functor and monad transformers | `QuickCheck`, `StateVar`, `attoparsec`, `bifunctors`, `boring`, `comonad`, `conduit`, `conduit-extra`, `constraints`, `contravariant`, `crypton-x509`, `distributive`, `exceptions`, `indexed-traversable`, `monad-control`, `monad-logger`, `mono-traversable`, `mtl`, `network-transport`, `optparse-applicative`, `primitive`, `resourcet`, `safe-exceptions`, `semialign`, `semigroupoids`, `simplexmq`, `sqlcipher-simple`, `streaming-commons`, `strict`, `tagged`, `temporary`, `terminal`, `tls`, `transformers-base`, `transformers-compat`, `typed-process`, `unliftio`, `unliftio-core`, `witherable`, `yaml`, `zip` | +| `transformers-base` | [`0.4.6`](http://hackage.haskell.org/package/transformers-base-0.4.6) | [`BSD-3-Clause`](./licences/haskell/transformers-base-0.4.6/LICENSE) | Lift computations from the bottom of a transformer stack | `lifted-base`, `monad-control`, `monad-logger`, `zip` | +| `transformers-compat` | [`0.7.2`](http://hackage.haskell.org/package/transformers-compat-0.7.2) | [`BSD-3-Clause`](./licences/haskell/transformers-compat-0.7.2/LICENSE) | A small compatibility shim for the transformers library | `comonad`, `monad-control`, `monad-logger`, `optparse-applicative`, `semigroupoids`, `transformers-base` | +| `attoparsec` | [`0.14.4`](http://hackage.haskell.org/package/attoparsec-0.14.4) | [`BSD-3-Clause`](./licences/haskell/attoparsec-0.14.4/LICENSE) | Fast combinator parsing for bytestrings and text | `attoparsec` | +| `text-iso8601` | [`0.1`](http://hackage.haskell.org/package/text-iso8601-0.1) | [`BSD-3-Clause`](./licences/haskell/text-iso8601-0.1/LICENSE) | Converting time to and from ISO 8601 text. | `aeson` | +| `text-short` | [`0.1.5`](http://hackage.haskell.org/package/text-short-0.1.5) | [`BSD-3-Clause`](./licences/haskell/text-short-0.1.5/LICENSE) | Memory-efficient representation of Unicode text strings | `aeson` | +| `type-equality` | [`1`](http://hackage.haskell.org/package/type-equality-1) | [`BSD-3-Clause`](./licences/haskell/type-equality-1/LICENSE) | Data.Type.Equality compat package | `constraints` | +| `typed-process` | [`0.2.11.1`](http://hackage.haskell.org/package/typed-process-0.2.11.1) | [`MIT`](./licences/haskell/typed-process-0.2.11.1/LICENSE) | Run external processes, with strong typing of streams | `conduit-extra` | +| **`unix`** | [`2.8.1.0`](http://hackage.haskell.org/package/unix-2.8.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/unix-2.8.1.0/src/LICENSE) | POSIX functionality | `conduit`, `directory`, `easy-file`, `entropy`, `process`, `streaming-commons`, `temporary`, `unix-compat`, `unliftio`, `zip` | +| `vector` | [`0.13.1.0`](http://hackage.haskell.org/package/vector-0.13.1.0) | [`BSD-3-Clause`](./licences/haskell/vector-0.13.1.0/LICENSE) | Efficient Arrays | `aeson`, `bitvec`, `blaze-textual`, `conduit`, `indexed-traversable-instances`, `mono-traversable`, `semialign`, `vector-algorithms`, `witherable`, `yaml` | +| `vector-algorithms` | [`0.9.0.1`](http://hackage.haskell.org/package/vector-algorithms-0.9.0.1) | [`BSD-3-Clause`](./licences/haskell/vector-algorithms-0.9.0.1/LICENSE) | Efficient algorithms for vector arrays | `mono-traversable` | +| `vector-stream` | [`0.1.0.0`](http://hackage.haskell.org/package/vector-stream-0.1.0.0) | [`BSD-3-Clause`](./licences/haskell/vector-stream-0.1.0.0/LICENSE) | Efficient Streams | `vector` | +| `witherable` | [`0.4.2`](http://hackage.haskell.org/package/witherable-0.4.2) | [`BSD-3-Clause`](./licences/haskell/witherable-0.4.2/LICENSE) | filterable traversable | `aeson` | +| `yaml` | [`0.11.11.2`](http://hackage.haskell.org/package/yaml-0.11.11.2) | [`BSD-3-Clause`](./licences/haskell/yaml-0.11.11.2/LICENSE) | Support for parsing and rendering YAML documents. | `simplexmq` | +| `zlib` | [`0.6.3.0`](http://hackage.haskell.org/package/zlib-0.6.3.0) | [`BSD-3-Clause`](./licences/haskell/zlib-0.6.3.0/LICENSE) | Compression and decompression in the gzip and zlib formats | `streaming-commons` | +| `zstd` | [`0.1.3.0`](http://hackage.haskell.org/package/zstd-0.1.3.0) | [`BSD-3-Clause`](./licences/haskell/zstd-0.1.3.0/LICENSE) | Haskell bindings to the Zstandard compression algorithm | `simplexmq` | + diff --git a/docs/dependencies/README.md b/docs/dependencies/README.md new file mode 100644 index 0000000000..bec369c126 --- /dev/null +++ b/docs/dependencies/README.md @@ -0,0 +1,9 @@ +# SimpleX Chat and SimpleX servers dependencies + +[SQLCipher](https://github.com/sqlcipher/sqlcipher): Extension of [SQLite](https://sqlite.org) with encryption ([BSD-style](./licences/apps/sqlcipher/LICENSE.md)) + +[vlc](https://github.com/videolan/vlc): VLC media player library ([LGPLv2](./licences/apps/vlc/COPYING.LIB)) + +[WebRTC](https://webrtc.googlesource.com/src/): RTC for calls ([BSD-3-clause](./licences/apps/webrtc/LICENSE)) + +[Haskell dependencies](./HASKELL.md). diff --git a/docs/dependencies/licences/apps/sqlcipher/LICENSE.md b/docs/dependencies/licences/apps/sqlcipher/LICENSE.md new file mode 100644 index 0000000000..3f71443161 --- /dev/null +++ b/docs/dependencies/licences/apps/sqlcipher/LICENSE.md @@ -0,0 +1,24 @@ +Copyright (c) 2025, ZETETIC LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the ZETETIC LLC nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/apps/vlc/COPYING.LIB b/docs/dependencies/licences/apps/vlc/COPYING.LIB new file mode 100644 index 0000000000..4362b49151 --- /dev/null +++ b/docs/dependencies/licences/apps/vlc/COPYING.LIB @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/docs/dependencies/licences/apps/webrtc/LICENSE b/docs/dependencies/licences/apps/webrtc/LICENSE new file mode 100644 index 0000000000..8868dcdf45 --- /dev/null +++ b/docs/dependencies/licences/apps/webrtc/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2011, The WebRTC project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/OneTuple-0.4.1.1/LICENSE b/docs/dependencies/licences/haskell/OneTuple-0.4.1.1/LICENSE new file mode 100644 index 0000000000..405f9161e9 --- /dev/null +++ b/docs/dependencies/licences/haskell/OneTuple-0.4.1.1/LICENSE @@ -0,0 +1,34 @@ + +Copyright (c) 2008, John A. Dorsey. +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of John Dorsey nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/dependencies/licences/haskell/Only-0.1/LICENSE b/docs/dependencies/licences/haskell/Only-0.1/LICENSE new file mode 100644 index 0000000000..98d4b1ad78 --- /dev/null +++ b/docs/dependencies/licences/haskell/Only-0.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Herbert Valerio Riedel + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Herbert Valerio Riedel nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/QuickCheck-2.14.3/LICENSE b/docs/dependencies/licences/haskell/QuickCheck-2.14.3/LICENSE new file mode 100644 index 0000000000..8b0fdbfe4e --- /dev/null +++ b/docs/dependencies/licences/haskell/QuickCheck-2.14.3/LICENSE @@ -0,0 +1,29 @@ +(The following is the 3-clause BSD license.) + +Copyright (c) 2000-2019, Koen Claessen +Copyright (c) 2006-2008, Björn Bringert +Copyright (c) 2009-2019, Nick Smallbone + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the names of the copyright owners nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/SHA-1.6.4.4/LICENSE b/docs/dependencies/licences/haskell/SHA-1.6.4.4/LICENSE new file mode 100644 index 0000000000..8e49100207 --- /dev/null +++ b/docs/dependencies/licences/haskell/SHA-1.6.4.4/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2008, Galois, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the Galois, Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/StateVar-1.2.2/LICENSE b/docs/dependencies/licences/haskell/StateVar-1.2.2/LICENSE new file mode 100644 index 0000000000..e71299ba0e --- /dev/null +++ b/docs/dependencies/licences/haskell/StateVar-1.2.2/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2014-2015, Edward Kmett +Copyright (c) 2009-2021, Sven Panne +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/aeson-2.2.1.0/LICENSE b/docs/dependencies/licences/haskell/aeson-2.2.1.0/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/ansi-terminal-1.0/LICENSE b/docs/dependencies/licences/haskell/ansi-terminal-1.0/LICENSE new file mode 100644 index 0000000000..814a83d4fc --- /dev/null +++ b/docs/dependencies/licences/haskell/ansi-terminal-1.0/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2008, Maximilian Bolingbroke +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + * Neither the name of Maximilian Bolingbroke nor the names of other contributors may be used to + endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/ansi-terminal-types-0.11.5/LICENSE b/docs/dependencies/licences/haskell/ansi-terminal-types-0.11.5/LICENSE new file mode 100644 index 0000000000..413047009d --- /dev/null +++ b/docs/dependencies/licences/haskell/ansi-terminal-types-0.11.5/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2008, Maximilian Bolingbroke +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Maximilian Bolingbroke nor the names of other contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/appar-0.1.8/LICENSE b/docs/dependencies/licences/haskell/appar-0.1.8/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/appar-0.1.8/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/asn1-encoding-0.9.6/LICENSE b/docs/dependencies/licences/haskell/asn1-encoding-0.9.6/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/asn1-encoding-0.9.6/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/asn1-parse-0.9.5/LICENSE b/docs/dependencies/licences/haskell/asn1-parse-0.9.5/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/asn1-parse-0.9.5/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/asn1-types-0.3.4/LICENSE b/docs/dependencies/licences/haskell/asn1-types-0.3.4/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/asn1-types-0.3.4/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/assoc-1.1/LICENSE b/docs/dependencies/licences/haskell/assoc-1.1/LICENSE new file mode 100644 index 0000000000..96445e5484 --- /dev/null +++ b/docs/dependencies/licences/haskell/assoc-1.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/async-2.2.5/LICENSE b/docs/dependencies/licences/haskell/async-2.2.5/LICENSE new file mode 100644 index 0000000000..0acbb121f4 --- /dev/null +++ b/docs/dependencies/licences/haskell/async-2.2.5/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, Simon Marlow + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Simon Marlow nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/attoparsec-0.14.4/LICENSE b/docs/dependencies/licences/haskell/attoparsec-0.14.4/LICENSE new file mode 100644 index 0000000000..97392a6264 --- /dev/null +++ b/docs/dependencies/licences/haskell/attoparsec-0.14.4/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Lennart Kolmodin + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/auto-update-0.1.6/LICENSE b/docs/dependencies/licences/haskell/auto-update-0.1.6/LICENSE new file mode 100644 index 0000000000..d2a7d05797 --- /dev/null +++ b/docs/dependencies/licences/haskell/auto-update-0.1.6/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Michael Snoyman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/base-orphans-0.9.1/LICENSE b/docs/dependencies/licences/haskell/base-orphans-0.9.1/LICENSE new file mode 100644 index 0000000000..4751ca3f63 --- /dev/null +++ b/docs/dependencies/licences/haskell/base-orphans-0.9.1/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-2017 Simon Hengel , João Cristóvão , Ryan Scott + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/base64-bytestring-1.2.1.0/LICENSE b/docs/dependencies/licences/haskell/base64-bytestring-1.2.1.0/LICENSE new file mode 100644 index 0000000000..89d2a1b35f --- /dev/null +++ b/docs/dependencies/licences/haskell/base64-bytestring-1.2.1.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2010 Bryan O'Sullivan + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/basement-0.0.16/LICENSE b/docs/dependencies/licences/haskell/basement-0.0.16/LICENSE new file mode 100644 index 0000000000..75e0288c26 --- /dev/null +++ b/docs/dependencies/licences/haskell/basement-0.0.16/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2015-2017 Vincent Hanquez +Copyright (c) 2017-2019 Foundation Maintainers + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/bifunctors-5.6.1/LICENSE b/docs/dependencies/licences/haskell/bifunctors-5.6.1/LICENSE new file mode 100644 index 0000000000..531684e87f --- /dev/null +++ b/docs/dependencies/licences/haskell/bifunctors-5.6.1/LICENSE @@ -0,0 +1,26 @@ +Copyright 2008-2016 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/bitvec-1.1.5.0/LICENSE b/docs/dependencies/licences/haskell/bitvec-1.1.5.0/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/blaze-builder-0.4.2.3/LICENSE b/docs/dependencies/licences/haskell/blaze-builder-0.4.2.3/LICENSE new file mode 100644 index 0000000000..a8bcabfd42 --- /dev/null +++ b/docs/dependencies/licences/haskell/blaze-builder-0.4.2.3/LICENSE @@ -0,0 +1,30 @@ +Copyright Jasper Van der Jeugt 2010, Simon Meier 2010 & 2011 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Jasper Van der Jeugt nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/blaze-textual-0.2.3.1/LICENSE b/docs/dependencies/licences/haskell/blaze-textual-0.2.3.1/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/boring-0.2.1/LICENSE b/docs/dependencies/licences/haskell/boring-0.2.1/LICENSE new file mode 100644 index 0000000000..96445e5484 --- /dev/null +++ b/docs/dependencies/licences/haskell/boring-0.2.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/byteorder-1.0.4/LICENSE b/docs/dependencies/licences/haskell/byteorder-1.0.4/LICENSE new file mode 100644 index 0000000000..81b771a3b8 --- /dev/null +++ b/docs/dependencies/licences/haskell/byteorder-1.0.4/LICENSE @@ -0,0 +1,30 @@ +Copyright 2009, Antoine Latter + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the author nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/bytestring-builder-0.10.8.2.0/LICENSE b/docs/dependencies/licences/haskell/bytestring-builder-0.10.8.2.0/LICENSE new file mode 100644 index 0000000000..5106f1309a --- /dev/null +++ b/docs/dependencies/licences/haskell/bytestring-builder-0.10.8.2.0/LICENSE @@ -0,0 +1,30 @@ +Copyright Jasper Van der Jeugt 2010, Simon Meier 2010-2013 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Jasper Van der Jeugt nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/case-insensitive-1.2.1.0/LICENSE b/docs/dependencies/licences/haskell/case-insensitive-1.2.1.0/LICENSE new file mode 100644 index 0000000000..c81fb8bf9d --- /dev/null +++ b/docs/dependencies/licences/haskell/case-insensitive-1.2.1.0/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2011-2013 Bas van Dijk + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The name of Bas van Dijk and the names of contributors may NOT + be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/cereal-0.5.8.3/LICENSE b/docs/dependencies/licences/haskell/cereal-0.5.8.3/LICENSE new file mode 100644 index 0000000000..6dfb133efb --- /dev/null +++ b/docs/dependencies/licences/haskell/cereal-0.5.8.3/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Lennart Kolmodin, Galois, Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/clock-0.8.4/LICENSE b/docs/dependencies/licences/haskell/clock-0.8.4/LICENSE new file mode 100644 index 0000000000..c2451c8c0a --- /dev/null +++ b/docs/dependencies/licences/haskell/clock-0.8.4/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2009-2022, Clock Contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of contributors may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/colour-2.3.6/LICENSE b/docs/dependencies/licences/haskell/colour-2.3.6/LICENSE new file mode 100644 index 0000000000..61aede888a --- /dev/null +++ b/docs/dependencies/licences/haskell/colour-2.3.6/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008, 2009 +Russell O'Connor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/comonad-5.0.8/LICENSE b/docs/dependencies/licences/haskell/comonad-5.0.8/LICENSE new file mode 100644 index 0000000000..f8e92e04df --- /dev/null +++ b/docs/dependencies/licences/haskell/comonad-5.0.8/LICENSE @@ -0,0 +1,27 @@ +Copyright 2008-2014 Edward Kmett +Copyright 2004-2008 Dave Menendez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/composition-1.0.2.2/LICENSE b/docs/dependencies/licences/haskell/composition-1.0.2.2/LICENSE new file mode 100644 index 0000000000..e191e7ab64 --- /dev/null +++ b/docs/dependencies/licences/haskell/composition-1.0.2.2/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2011-2012, Dan Burton + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Dan Burton nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/conduit-1.3.5/LICENSE b/docs/dependencies/licences/haskell/conduit-1.3.5/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/conduit-extra-1.3.6/LICENSE b/docs/dependencies/licences/haskell/conduit-extra-1.3.6/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/constraints-0.14/LICENSE b/docs/dependencies/licences/haskell/constraints-0.14/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/contravariant-1.5.5/LICENSE b/docs/dependencies/licences/haskell/contravariant-1.5.5/LICENSE new file mode 100644 index 0000000000..f357c7c429 --- /dev/null +++ b/docs/dependencies/licences/haskell/contravariant-1.5.5/LICENSE @@ -0,0 +1,30 @@ +Copyright 2007-2015 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/cryptohash-md5-0.11.101.0/LICENSE b/docs/dependencies/licences/haskell/cryptohash-md5-0.11.101.0/LICENSE new file mode 100644 index 0000000000..467bf18f27 --- /dev/null +++ b/docs/dependencies/licences/haskell/cryptohash-md5-0.11.101.0/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2010-2014 Vincent Hanquez + 2016 Herbert Valerio Riedel + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/cryptohash-sha1-0.11.101.0/LICENSE b/docs/dependencies/licences/haskell/cryptohash-sha1-0.11.101.0/LICENSE new file mode 100644 index 0000000000..467bf18f27 --- /dev/null +++ b/docs/dependencies/licences/haskell/cryptohash-sha1-0.11.101.0/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2010-2014 Vincent Hanquez + 2016 Herbert Valerio Riedel + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/crypton-0.34/LICENSE b/docs/dependencies/licences/haskell/crypton-0.34/LICENSE new file mode 100644 index 0000000000..a61c84945b --- /dev/null +++ b/docs/dependencies/licences/haskell/crypton-0.34/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2006-2015 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/crypton-x509-1.7.6/LICENSE b/docs/dependencies/licences/haskell/crypton-x509-1.7.6/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/crypton-x509-1.7.6/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/crypton-x509-store-1.6.9/LICENSE b/docs/dependencies/licences/haskell/crypton-x509-store-1.6.9/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/crypton-x509-store-1.6.9/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/crypton-x509-validation-1.6.12/LICENSE b/docs/dependencies/licences/haskell/crypton-x509-validation-1.6.12/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/crypton-x509-validation-1.6.12/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/cryptostore-0.3.0.1/LICENSE b/docs/dependencies/licences/haskell/cryptostore-0.3.0.1/LICENSE new file mode 100644 index 0000000000..b85926a265 --- /dev/null +++ b/docs/dependencies/licences/haskell/cryptostore-0.3.0.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2018-2023, Olivier Chéron + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Olivier Chéron nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-0.7.1.1/LICENSE b/docs/dependencies/licences/haskell/data-default-0.7.1.1/LICENSE new file mode 100644 index 0000000000..cd44640be9 --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-0.7.1.1/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-class-0.1.2.0/LICENSE b/docs/dependencies/licences/haskell/data-default-class-0.1.2.0/LICENSE new file mode 100644 index 0000000000..cd44640be9 --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-class-0.1.2.0/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-instances-containers-0.0.1/LICENSE b/docs/dependencies/licences/haskell/data-default-instances-containers-0.0.1/LICENSE new file mode 100644 index 0000000000..4f9075d76b --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-instances-containers-0.0.1/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY LUKAS MAI AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-instances-dlist-0.0.1/LICENSE b/docs/dependencies/licences/haskell/data-default-instances-dlist-0.0.1/LICENSE new file mode 100644 index 0000000000..4f9075d76b --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-instances-dlist-0.0.1/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY LUKAS MAI AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-instances-old-locale-0.0.1/LICENSE b/docs/dependencies/licences/haskell/data-default-instances-old-locale-0.0.1/LICENSE new file mode 100644 index 0000000000..4f9075d76b --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-instances-old-locale-0.0.1/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY LUKAS MAI AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-fix-0.3.2/LICENSE b/docs/dependencies/licences/haskell/data-fix-0.3.2/LICENSE new file mode 100644 index 0000000000..0576170924 --- /dev/null +++ b/docs/dependencies/licences/haskell/data-fix-0.3.2/LICENSE @@ -0,0 +1,30 @@ +Copyright Anton Kholomiov 2010 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Anton Kholomiov nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/digest-0.0.1.7/LICENSE b/docs/dependencies/licences/haskell/digest-0.0.1.7/LICENSE new file mode 100644 index 0000000000..48326b0815 --- /dev/null +++ b/docs/dependencies/licences/haskell/digest-0.0.1.7/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2008-2009, Eugene Kirpichov +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/distributive-0.6.2.1/LICENSE b/docs/dependencies/licences/haskell/distributive-0.6.2.1/LICENSE new file mode 100644 index 0000000000..d6c9cbe4a4 --- /dev/null +++ b/docs/dependencies/licences/haskell/distributive-0.6.2.1/LICENSE @@ -0,0 +1,26 @@ +Copyright 2011-2016 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/dlist-1.0/license.md b/docs/dependencies/licences/haskell/dlist-1.0/license.md new file mode 100644 index 0000000000..32ac978e90 --- /dev/null +++ b/docs/dependencies/licences/haskell/dlist-1.0/license.md @@ -0,0 +1,28 @@ +Copyright © 2006-2009 Don Stewart, 2013-2020 Sean Leather, contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of other contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/easy-file-0.2.5/LICENSE b/docs/dependencies/licences/haskell/easy-file-0.2.5/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/easy-file-0.2.5/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/email-validate-2.3.2.19/LICENSE b/docs/dependencies/licences/haskell/email-validate-2.3.2.19/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/entropy-0.4.1.10/LICENSE b/docs/dependencies/licences/haskell/entropy-0.4.1.10/LICENSE new file mode 100644 index 0000000000..6d24e89d0b --- /dev/null +++ b/docs/dependencies/licences/haskell/entropy-0.4.1.10/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Thomas DuBuisson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/fast-logger-3.2.2/LICENSE b/docs/dependencies/licences/haskell/fast-logger-3.2.2/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/fast-logger-3.2.2/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/file-embed-0.0.15.0/LICENSE b/docs/dependencies/licences/haskell/file-embed-0.0.15.0/LICENSE new file mode 100644 index 0000000000..11dc17a16c --- /dev/null +++ b/docs/dependencies/licences/haskell/file-embed-0.0.15.0/LICENSE @@ -0,0 +1,25 @@ +The following license covers this documentation, and the source code, except +where otherwise indicated. + +Copyright 2008, Michael Snoyman. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/generically-0.1.1/LICENSE b/docs/dependencies/licences/haskell/generically-0.1.1/LICENSE new file mode 100644 index 0000000000..58c573a1e2 --- /dev/null +++ b/docs/dependencies/licences/haskell/generically-0.1.1/LICENSE @@ -0,0 +1,28 @@ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Herbert Valerio Riedel nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/hashable-1.4.3.0/LICENSE b/docs/dependencies/licences/haskell/hashable-1.4.3.0/LICENSE new file mode 100644 index 0000000000..7130957b4d --- /dev/null +++ b/docs/dependencies/licences/haskell/hashable-1.4.3.0/LICENSE @@ -0,0 +1,30 @@ +Copyright Milan Straka 2010 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Milan Straka nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/hourglass-0.2.12/LICENSE b/docs/dependencies/licences/haskell/hourglass-0.2.12/LICENSE new file mode 100644 index 0000000000..36c942d4a8 --- /dev/null +++ b/docs/dependencies/licences/haskell/hourglass-0.2.12/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/http-types-0.12.4/LICENSE b/docs/dependencies/licences/haskell/http-types-0.12.4/LICENSE new file mode 100644 index 0000000000..f77a7b24dd --- /dev/null +++ b/docs/dependencies/licences/haskell/http-types-0.12.4/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2011, Aristid Breitkreuz +Copyright (c) 2011, Michael Snoyman + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Aristid Breitkreuz nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/http2-5.0.0/LICENSE b/docs/dependencies/licences/haskell/http2-5.0.0/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/indexed-traversable-0.1.3/LICENSE b/docs/dependencies/licences/haskell/indexed-traversable-0.1.3/LICENSE new file mode 100644 index 0000000000..476166addc --- /dev/null +++ b/docs/dependencies/licences/haskell/indexed-traversable-0.1.3/LICENSE @@ -0,0 +1,26 @@ +Copyright 2012-2016 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/indexed-traversable-instances-0.1.1.2/LICENSE b/docs/dependencies/licences/haskell/indexed-traversable-instances-0.1.1.2/LICENSE new file mode 100644 index 0000000000..476166addc --- /dev/null +++ b/docs/dependencies/licences/haskell/indexed-traversable-instances-0.1.1.2/LICENSE @@ -0,0 +1,26 @@ +Copyright 2012-2016 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/ini-0.4.2/LICENSE b/docs/dependencies/licences/haskell/ini-0.4.2/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/integer-conversion-0.1.0.1/LICENSE b/docs/dependencies/licences/haskell/integer-conversion-0.1.0.1/LICENSE new file mode 100644 index 0000000000..3f74492595 --- /dev/null +++ b/docs/dependencies/licences/haskell/integer-conversion-0.1.0.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2023, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/integer-logarithms-1.0.3.1/LICENSE b/docs/dependencies/licences/haskell/integer-logarithms-1.0.3.1/LICENSE new file mode 100644 index 0000000000..9430f484d0 --- /dev/null +++ b/docs/dependencies/licences/haskell/integer-logarithms-1.0.3.1/LICENSE @@ -0,0 +1,16 @@ +Copyright (c) 2011 Daniel Fischer, 2017 Oleg Grenrus + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + associated documentation files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, publish, distribute, + sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/iproute-1.7.12/LICENSE b/docs/dependencies/licences/haskell/iproute-1.7.12/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/iproute-1.7.12/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/libyaml-0.1.2/LICENSE b/docs/dependencies/licences/haskell/libyaml-0.1.2/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/lifted-base-0.2.3.12/LICENSE b/docs/dependencies/licences/haskell/lifted-base-0.2.3.12/LICENSE new file mode 100644 index 0000000000..b8b5ed95c0 --- /dev/null +++ b/docs/dependencies/licences/haskell/lifted-base-0.2.3.12/LICENSE @@ -0,0 +1,29 @@ +Copyright © 2010-2012, Bas van Dijk, Anders Kaseorg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +• Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +• Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +• Neither the name of the author nor the names of other contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/memory-0.18.0/LICENSE b/docs/dependencies/licences/haskell/memory-0.18.0/LICENSE new file mode 100644 index 0000000000..8eafd52555 --- /dev/null +++ b/docs/dependencies/licences/haskell/memory-0.18.0/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2015-2018 Vincent Hanquez +Copyright (c) 2017-2018 Nicolas Di Prima + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/monad-control-1.0.3.1/LICENSE b/docs/dependencies/licences/haskell/monad-control-1.0.3.1/LICENSE new file mode 100644 index 0000000000..f4e1bfab44 --- /dev/null +++ b/docs/dependencies/licences/haskell/monad-control-1.0.3.1/LICENSE @@ -0,0 +1,29 @@ +Copyright © 2010, Bas van Dijk, Anders Kaseorg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +• Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +• Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +• Neither the name of the author nor the names of other contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/monad-logger-0.3.40/LICENSE b/docs/dependencies/licences/haskell/monad-logger-0.3.40/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/mono-traversable-1.0.15.3/LICENSE b/docs/dependencies/licences/haskell/mono-traversable-1.0.15.3/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/network-3.1.4.0/LICENSE b/docs/dependencies/licences/haskell/network-3.1.4.0/LICENSE new file mode 100644 index 0000000000..40bba626f8 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-3.1.4.0/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2002-2010, The University Court of the University of Glasgow. +Copyright (c) 2007-2010, Johan Tibell + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/network-byte-order-0.1.7/LICENSE b/docs/dependencies/licences/haskell/network-byte-order-0.1.7/LICENSE new file mode 100644 index 0000000000..ed342e54a0 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-byte-order-0.1.7/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Kazu Yamamoto + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Kazu Yamamoto nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/network-control-0.0.2/LICENSE b/docs/dependencies/licences/haskell/network-control-0.0.2/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/network-info-0.2.1/LICENSE b/docs/dependencies/licences/haskell/network-info-0.2.1/LICENSE new file mode 100644 index 0000000000..be005bd412 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-info-0.2.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2010, Jacob Stanley + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Jacob Stanley nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/network-transport-0.5.6/LICENSE b/docs/dependencies/licences/haskell/network-transport-0.5.6/LICENSE new file mode 100644 index 0000000000..f3459e449d --- /dev/null +++ b/docs/dependencies/licences/haskell/network-transport-0.5.6/LICENSE @@ -0,0 +1,31 @@ +Copyright Well-Typed LLP, 2011-2012 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/dependencies/licences/haskell/network-udp-0.0.0/LICENSE b/docs/dependencies/licences/haskell/network-udp-0.0.0/LICENSE new file mode 100644 index 0000000000..73957a7a32 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-udp-0.0.0/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2022, Internet Initiative Japan Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/network-uri-2.6.4.2/LICENSE b/docs/dependencies/licences/haskell/network-uri-2.6.4.2/LICENSE new file mode 100644 index 0000000000..40bba626f8 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-uri-2.6.4.2/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2002-2010, The University Court of the University of Glasgow. +Copyright (c) 2007-2010, Johan Tibell + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/old-locale-1.0.0.7/LICENSE b/docs/dependencies/licences/haskell/old-locale-1.0.0.7/LICENSE new file mode 100644 index 0000000000..06bb641487 --- /dev/null +++ b/docs/dependencies/licences/haskell/old-locale-1.0.0.7/LICENSE @@ -0,0 +1,63 @@ +This library (libraries/base) is derived from code from two +sources: + + * Code from the GHC project which is largely (c) The University of + Glasgow, and distributable under a BSD-style license (see below), + + * Code from the Haskell 98 Report which is (c) Simon Peyton Jones + and freely redistributable (but see the full license for + restrictions). + +The full text of these licenses is reproduced below. Both of the +licenses are BSD-style or compatible. + +----------------------------------------------------------------------------- + +The Glasgow Haskell Compiler License + +Copyright 2004, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------------------- + +Code derived from the document "Report on the Programming Language +Haskell 98", is distributed under the following license: + + Copyright (c) 2002 Simon Peyton Jones + + The authors intend this Report to belong to the entire Haskell + community, and so we grant permission to copy and distribute it for + any purpose, provided that it is reproduced in its entirety, + including this Notice. Modified versions of this Report may also be + copied and distributed for any purpose, provided that the modified + version is clearly presented as such, and that it does not claim to + be a definition of the Haskell 98 Language. + +----------------------------------------------------------------------------- diff --git a/docs/dependencies/licences/haskell/old-time-1.1.0.4/LICENSE b/docs/dependencies/licences/haskell/old-time-1.1.0.4/LICENSE new file mode 100644 index 0000000000..06bb641487 --- /dev/null +++ b/docs/dependencies/licences/haskell/old-time-1.1.0.4/LICENSE @@ -0,0 +1,63 @@ +This library (libraries/base) is derived from code from two +sources: + + * Code from the GHC project which is largely (c) The University of + Glasgow, and distributable under a BSD-style license (see below), + + * Code from the Haskell 98 Report which is (c) Simon Peyton Jones + and freely redistributable (but see the full license for + restrictions). + +The full text of these licenses is reproduced below. Both of the +licenses are BSD-style or compatible. + +----------------------------------------------------------------------------- + +The Glasgow Haskell Compiler License + +Copyright 2004, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------------------- + +Code derived from the document "Report on the Programming Language +Haskell 98", is distributed under the following license: + + Copyright (c) 2002 Simon Peyton Jones + + The authors intend this Report to belong to the entire Haskell + community, and so we grant permission to copy and distribute it for + any purpose, provided that it is reproduced in its entirety, + including this Notice. Modified versions of this Report may also be + copied and distributed for any purpose, provided that the modified + version is clearly presented as such, and that it does not claim to + be a definition of the Haskell 98 Language. + +----------------------------------------------------------------------------- diff --git a/docs/dependencies/licences/haskell/optparse-applicative-0.18.1.0/LICENSE b/docs/dependencies/licences/haskell/optparse-applicative-0.18.1.0/LICENSE new file mode 100644 index 0000000000..0507cc2a81 --- /dev/null +++ b/docs/dependencies/licences/haskell/optparse-applicative-0.18.1.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, Paolo Capriotti + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Paolo Capriotti nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/pem-0.2.4/LICENSE b/docs/dependencies/licences/haskell/pem-0.2.4/LICENSE new file mode 100644 index 0000000000..15c41788af --- /dev/null +++ b/docs/dependencies/licences/haskell/pem-0.2.4/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2018 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/prettyprinter-1.7.1/LICENSE.md b/docs/dependencies/licences/haskell/prettyprinter-1.7.1/LICENSE.md new file mode 100644 index 0000000000..39592102dc --- /dev/null +++ b/docs/dependencies/licences/haskell/prettyprinter-1.7.1/LICENSE.md @@ -0,0 +1,23 @@ +Copyright 2008, Daan Leijen and Max Bolingbroke, 2016 David Luposchainsky. All +rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +This software is provided by the copyright holders "as is" and any express or +implied warranties, including, but not limited to, the implied warranties of +merchantability and fitness for a particular purpose are disclaimed. In no event +shall the copyright holders be liable for any direct, indirect, incidental, +special, exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or profits; or +business interruption) however caused and on any theory of liability, whether in +contract, strict liability, or tort (including negligence or otherwise) arising +in any way out of the use of this software, even if advised of the possibility +of such damage. diff --git a/docs/dependencies/licences/haskell/prettyprinter-ansi-terminal-1.1.3/LICENSE.md b/docs/dependencies/licences/haskell/prettyprinter-ansi-terminal-1.1.3/LICENSE.md new file mode 100644 index 0000000000..39592102dc --- /dev/null +++ b/docs/dependencies/licences/haskell/prettyprinter-ansi-terminal-1.1.3/LICENSE.md @@ -0,0 +1,23 @@ +Copyright 2008, Daan Leijen and Max Bolingbroke, 2016 David Luposchainsky. All +rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +This software is provided by the copyright holders "as is" and any express or +implied warranties, including, but not limited to, the implied warranties of +merchantability and fitness for a particular purpose are disclaimed. In no event +shall the copyright holders be liable for any direct, indirect, incidental, +special, exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or profits; or +business interruption) however caused and on any theory of liability, whether in +contract, strict liability, or tort (including negligence or otherwise) arising +in any way out of the use of this software, even if advised of the possibility +of such damage. diff --git a/docs/dependencies/licences/haskell/primitive-0.9.0.0/LICENSE b/docs/dependencies/licences/haskell/primitive-0.9.0.0/LICENSE new file mode 100644 index 0000000000..fc213a6ffb --- /dev/null +++ b/docs/dependencies/licences/haskell/primitive-0.9.0.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2008-2009, Roman Leshchinskiy +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + diff --git a/docs/dependencies/licences/haskell/psqueues-0.2.8.0/LICENSE b/docs/dependencies/licences/haskell/psqueues-0.2.8.0/LICENSE new file mode 100644 index 0000000000..92337b951e --- /dev/null +++ b/docs/dependencies/licences/haskell/psqueues-0.2.8.0/LICENSE @@ -0,0 +1,31 @@ +The Glasgow Haskell Compiler License + +Copyright 2004, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/random-1.2.1.1/LICENSE b/docs/dependencies/licences/haskell/random-1.2.1.1/LICENSE new file mode 100644 index 0000000000..06bb641487 --- /dev/null +++ b/docs/dependencies/licences/haskell/random-1.2.1.1/LICENSE @@ -0,0 +1,63 @@ +This library (libraries/base) is derived from code from two +sources: + + * Code from the GHC project which is largely (c) The University of + Glasgow, and distributable under a BSD-style license (see below), + + * Code from the Haskell 98 Report which is (c) Simon Peyton Jones + and freely redistributable (but see the full license for + restrictions). + +The full text of these licenses is reproduced below. Both of the +licenses are BSD-style or compatible. + +----------------------------------------------------------------------------- + +The Glasgow Haskell Compiler License + +Copyright 2004, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------------------- + +Code derived from the document "Report on the Programming Language +Haskell 98", is distributed under the following license: + + Copyright (c) 2002 Simon Peyton Jones + + The authors intend this Report to belong to the entire Haskell + community, and so we grant permission to copy and distribute it for + any purpose, provided that it is reproduced in its entirety, + including this Notice. Modified versions of this Report may also be + copied and distributed for any purpose, provided that the modified + version is clearly presented as such, and that it does not claim to + be a definition of the Haskell 98 Language. + +----------------------------------------------------------------------------- diff --git a/docs/dependencies/licences/haskell/record-hasfield-1.0/LICENSE b/docs/dependencies/licences/haskell/record-hasfield-1.0/LICENSE new file mode 100644 index 0000000000..f6a9dc60f7 --- /dev/null +++ b/docs/dependencies/licences/haskell/record-hasfield-1.0/LICENSE @@ -0,0 +1,30 @@ +Copyright Adam Gundry and Neil Mitchell 2018-2019. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Neil Mitchell nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/resourcet-1.3.0/LICENSE b/docs/dependencies/licences/haskell/resourcet-1.3.0/LICENSE new file mode 100644 index 0000000000..fe6d4e9b3f --- /dev/null +++ b/docs/dependencies/licences/haskell/resourcet-1.3.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c)2011, Michael Snoyman + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Michael Snoyman nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/safe-exceptions-0.1.7.4/LICENSE b/docs/dependencies/licences/haskell/safe-exceptions-0.1.7.4/LICENSE new file mode 100644 index 0000000000..9a69c0901b --- /dev/null +++ b/docs/dependencies/licences/haskell/safe-exceptions-0.1.7.4/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 FP Complete + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/scientific-0.3.7.0/LICENSE b/docs/dependencies/licences/haskell/scientific-0.3.7.0/LICENSE new file mode 100644 index 0000000000..c1015843aa --- /dev/null +++ b/docs/dependencies/licences/haskell/scientific-0.3.7.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2013, Bas van Dijk + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Bas van Dijk nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/semialign-1.3/LICENSE b/docs/dependencies/licences/haskell/semialign-1.3/LICENSE new file mode 100644 index 0000000000..9d6d650165 --- /dev/null +++ b/docs/dependencies/licences/haskell/semialign-1.3/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, C. McCann, 2015-2019 Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of C. McCann nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/semigroupoids-6.0.0.1/LICENSE b/docs/dependencies/licences/haskell/semigroupoids-6.0.0.1/LICENSE new file mode 100644 index 0000000000..1812ac961c --- /dev/null +++ b/docs/dependencies/licences/haskell/semigroupoids-6.0.0.1/LICENSE @@ -0,0 +1,26 @@ +Copyright 2011-2015 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/simple-logger-0.1.1/LICENSE b/docs/dependencies/licences/haskell/simple-logger-0.1.1/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/socks-0.6.1/LICENSE b/docs/dependencies/licences/haskell/socks-0.6.1/LICENSE new file mode 100644 index 0000000000..ed6f548a98 --- /dev/null +++ b/docs/dependencies/licences/haskell/socks-0.6.1/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2019 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/split-0.2.4/LICENSE b/docs/dependencies/licences/haskell/split-0.2.4/LICENSE new file mode 100644 index 0000000000..648167ba61 --- /dev/null +++ b/docs/dependencies/licences/haskell/split-0.2.4/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2008 Brent Yorgey, Louis Wasserman + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of other contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/splitmix-0.1.0.5/LICENSE b/docs/dependencies/licences/haskell/splitmix-0.1.0.5/LICENSE new file mode 100644 index 0000000000..96445e5484 --- /dev/null +++ b/docs/dependencies/licences/haskell/splitmix-0.1.0.5/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/stm-chans-3.0.0.9/LICENSE b/docs/dependencies/licences/haskell/stm-chans-3.0.0.9/LICENSE new file mode 100644 index 0000000000..55157e8352 --- /dev/null +++ b/docs/dependencies/licences/haskell/stm-chans-3.0.0.9/LICENSE @@ -0,0 +1,35 @@ +=== stm-chans license === + +Copyright (c) 2011--2013, wren gayle romano. +ALL RIGHTS RESERVED. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the copyright holders nor the names of + other contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/dependencies/licences/haskell/streaming-commons-0.2.2.6/LICENSE b/docs/dependencies/licences/haskell/streaming-commons-0.2.2.6/LICENSE new file mode 100644 index 0000000000..7198785aaf --- /dev/null +++ b/docs/dependencies/licences/haskell/streaming-commons-0.2.2.6/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 FP Complete + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/dependencies/licences/haskell/strict-0.5/LICENSE b/docs/dependencies/licences/haskell/strict-0.5/LICENSE new file mode 100644 index 0000000000..4782550806 --- /dev/null +++ b/docs/dependencies/licences/haskell/strict-0.5/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) Roman Leshchinskiy 2006-2007 + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/tagged-0.8.8/LICENSE b/docs/dependencies/licences/haskell/tagged-0.8.8/LICENSE new file mode 100644 index 0000000000..db046152dc --- /dev/null +++ b/docs/dependencies/licences/haskell/tagged-0.8.8/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2009-2015 Edward Kmett +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Edward Kmett nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/temporary-1.3/LICENSE b/docs/dependencies/licences/haskell/temporary-1.3/LICENSE new file mode 100644 index 0000000000..db55f34db3 --- /dev/null +++ b/docs/dependencies/licences/haskell/temporary-1.3/LICENSE @@ -0,0 +1,27 @@ +Copyright + (c) 2003-2006, Isaac Jones + (c) 2005-2009, Duncan Coutts + (c) 2008, Maximilian Bolingbroke + ... and other contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + * Neither the name of Maximilian Bolingbroke nor the names of other contributors may be used to + endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/terminal-0.2.0.0/LICENSE b/docs/dependencies/licences/haskell/terminal-0.2.0.0/LICENSE new file mode 100644 index 0000000000..d45361ccf6 --- /dev/null +++ b/docs/dependencies/licences/haskell/terminal-0.2.0.0/LICENSE @@ -0,0 +1,30 @@ +Copyright Lars Petersen (c) 2018 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Lars Petersen nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/text-iso8601-0.1/LICENSE b/docs/dependencies/licences/haskell/text-iso8601-0.1/LICENSE new file mode 100644 index 0000000000..fc90c466c7 --- /dev/null +++ b/docs/dependencies/licences/haskell/text-iso8601-0.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2023 Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/text-short-0.1.5/LICENSE b/docs/dependencies/licences/haskell/text-short-0.1.5/LICENSE new file mode 100644 index 0000000000..98d4b1ad78 --- /dev/null +++ b/docs/dependencies/licences/haskell/text-short-0.1.5/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Herbert Valerio Riedel + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Herbert Valerio Riedel nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/th-abstraction-0.6.0.0/LICENSE b/docs/dependencies/licences/haskell/th-abstraction-0.6.0.0/LICENSE new file mode 100644 index 0000000000..70f6077c10 --- /dev/null +++ b/docs/dependencies/licences/haskell/th-abstraction-0.6.0.0/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2017-2020 Eric Mertens + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/docs/dependencies/licences/haskell/th-compat-0.1.4/LICENSE b/docs/dependencies/licences/haskell/th-compat-0.1.4/LICENSE new file mode 100644 index 0000000000..879f215afc --- /dev/null +++ b/docs/dependencies/licences/haskell/th-compat-0.1.4/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2020, Ryan Scott + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Ryan Scott nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/these-1.2/LICENSE b/docs/dependencies/licences/haskell/these-1.2/LICENSE new file mode 100644 index 0000000000..9d6d650165 --- /dev/null +++ b/docs/dependencies/licences/haskell/these-1.2/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, C. McCann, 2015-2019 Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of C. McCann nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/time-compat-1.9.6.1/LICENSE b/docs/dependencies/licences/haskell/time-compat-1.9.6.1/LICENSE new file mode 100644 index 0000000000..910ef51bb4 --- /dev/null +++ b/docs/dependencies/licences/haskell/time-compat-1.9.6.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2019 time contibutors, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/time-manager-0.0.1/LICENSE b/docs/dependencies/licences/haskell/time-manager-0.0.1/LICENSE new file mode 100644 index 0000000000..d9f041796e --- /dev/null +++ b/docs/dependencies/licences/haskell/time-manager-0.0.1/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2012 Michael Snoyman, http://www.yesodweb.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/tls-1.9.0/LICENSE b/docs/dependencies/licences/haskell/tls-1.9.0/LICENSE new file mode 100644 index 0000000000..96ec822eb9 --- /dev/null +++ b/docs/dependencies/licences/haskell/tls-1.9.0/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2015 Vincent Hanquez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/transformers-base-0.4.6/LICENSE b/docs/dependencies/licences/haskell/transformers-base-0.4.6/LICENSE new file mode 100644 index 0000000000..9d51261b52 --- /dev/null +++ b/docs/dependencies/licences/haskell/transformers-base-0.4.6/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2011, Mikhail Vorozhtsov, Bas van Dijk +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the names of the copyright owners nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/dependencies/licences/haskell/transformers-compat-0.7.2/LICENSE b/docs/dependencies/licences/haskell/transformers-compat-0.7.2/LICENSE new file mode 100644 index 0000000000..50586d197c --- /dev/null +++ b/docs/dependencies/licences/haskell/transformers-compat-0.7.2/LICENSE @@ -0,0 +1,30 @@ +Copyright 2012-2015 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/type-equality-1/LICENSE b/docs/dependencies/licences/haskell/type-equality-1/LICENSE new file mode 100644 index 0000000000..c30f3e7d31 --- /dev/null +++ b/docs/dependencies/licences/haskell/type-equality-1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2009 Erik Hesselink, 2019 Oleg Grenrus, Ryan Scott + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of authors nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/typed-process-0.2.11.1/LICENSE b/docs/dependencies/licences/haskell/typed-process-0.2.11.1/LICENSE new file mode 100644 index 0000000000..50bb8ea0cf --- /dev/null +++ b/docs/dependencies/licences/haskell/typed-process-0.2.11.1/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 FP Complete, https://www.fpcomplete.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/unix-compat-0.7.1/LICENSE b/docs/dependencies/licences/haskell/unix-compat-0.7.1/LICENSE new file mode 100644 index 0000000000..cc3be3e947 --- /dev/null +++ b/docs/dependencies/licences/haskell/unix-compat-0.7.1/LICENSE @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2007-2008, Björn Bringert +Copyright (c) 2007-2009, Duncan Coutts +Copyright (c) 2010-2011, Jacob Stanley +Copyright (c) 2011, Bryan O'Sullivan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the names of the copyright owners nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/unix-time-0.4.11/LICENSE b/docs/dependencies/licences/haskell/unix-time-0.4.11/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/unix-time-0.4.11/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/unliftio-0.2.25.0/LICENSE b/docs/dependencies/licences/haskell/unliftio-0.2.25.0/LICENSE new file mode 100644 index 0000000000..1349134e01 --- /dev/null +++ b/docs/dependencies/licences/haskell/unliftio-0.2.25.0/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017 FP Complete + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/unliftio-core-0.2.1.0/LICENSE b/docs/dependencies/licences/haskell/unliftio-core-0.2.1.0/LICENSE new file mode 100644 index 0000000000..1349134e01 --- /dev/null +++ b/docs/dependencies/licences/haskell/unliftio-core-0.2.1.0/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017 FP Complete + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/unordered-containers-0.2.19.1/LICENSE b/docs/dependencies/licences/haskell/unordered-containers-0.2.19.1/LICENSE new file mode 100644 index 0000000000..5eb7e1bdda --- /dev/null +++ b/docs/dependencies/licences/haskell/unordered-containers-0.2.19.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2010, Johan Tibell + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Johan Tibell nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/uuid-1.3.15/LICENSE b/docs/dependencies/licences/haskell/uuid-1.3.15/LICENSE new file mode 100644 index 0000000000..dc46bca24c --- /dev/null +++ b/docs/dependencies/licences/haskell/uuid-1.3.15/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2008, Antoine Latter + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + * The names of the authors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/uuid-types-1.0.5.1/LICENSE b/docs/dependencies/licences/haskell/uuid-types-1.0.5.1/LICENSE new file mode 100644 index 0000000000..dc46bca24c --- /dev/null +++ b/docs/dependencies/licences/haskell/uuid-types-1.0.5.1/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2008, Antoine Latter + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + * The names of the authors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/vector-0.13.1.0/LICENSE b/docs/dependencies/licences/haskell/vector-0.13.1.0/LICENSE new file mode 100644 index 0000000000..ef2a5268d6 --- /dev/null +++ b/docs/dependencies/licences/haskell/vector-0.13.1.0/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 2008-2012, Roman Leshchinskiy + 2020-2022, Alexey Kuleshevich + 2020-2022, Aleksey Khudyakov + 2020-2022, Andrew Lelechenko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/vector-algorithms-0.9.0.1/LICENSE b/docs/dependencies/licences/haskell/vector-algorithms-0.9.0.1/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/vector-stream-0.1.0.0/LICENSE b/docs/dependencies/licences/haskell/vector-stream-0.1.0.0/LICENSE new file mode 100644 index 0000000000..ef2a5268d6 --- /dev/null +++ b/docs/dependencies/licences/haskell/vector-stream-0.1.0.0/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 2008-2012, Roman Leshchinskiy + 2020-2022, Alexey Kuleshevich + 2020-2022, Aleksey Khudyakov + 2020-2022, Andrew Lelechenko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/websockets-0.12.7.3/LICENCE b/docs/dependencies/licences/haskell/websockets-0.12.7.3/LICENCE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/witherable-0.4.2/LICENSE b/docs/dependencies/licences/haskell/witherable-0.4.2/LICENSE new file mode 100644 index 0000000000..1994a4c649 --- /dev/null +++ b/docs/dependencies/licences/haskell/witherable-0.4.2/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2014, Fumiaki Kinoshita + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Fumiaki Kinoshita nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/yaml-0.11.11.2/LICENSE b/docs/dependencies/licences/haskell/yaml-0.11.11.2/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/zip-2.0.0/LICENSE.md b/docs/dependencies/licences/haskell/zip-2.0.0/LICENSE.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/zlib-0.6.3.0/LICENSE b/docs/dependencies/licences/haskell/zlib-0.6.3.0/LICENSE new file mode 100644 index 0000000000..e4d25e0e0a --- /dev/null +++ b/docs/dependencies/licences/haskell/zlib-0.6.3.0/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2006-2016, Duncan Coutts +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. This clause is intentionally left blank. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/zstd-0.1.3.0/LICENSE b/docs/dependencies/licences/haskell/zstd-0.1.3.0/LICENSE new file mode 100644 index 0000000000..a793a80289 --- /dev/null +++ b/docs/dependencies/licences/haskell/zstd-0.1.3.0/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For Zstandard software + +Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flake.nix b/flake.nix index 1a1043c5f2..9ee0c52020 100644 --- a/flake.nix +++ b/flake.nix @@ -198,6 +198,7 @@ packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [ pkgs.pkgsCross.mingwW64.openssl ]; + packages.simplex-chat.flags.client_library = true; packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ pkgs.pkgsCross.mingwW64.openssl @@ -336,6 +337,7 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-android-log.patch ]; + packages.simplex-chat.flags.client_library = true; packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (android32Pkgs.openssl.override { static = true; enableKTLS = false; }) @@ -445,6 +447,7 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-android-log.patch ]; + packages.simplex-chat.flags.client_library = true; packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (androidPkgs.openssl.override { static = true; }) @@ -550,6 +553,7 @@ packages.simplexmq.flags.swift = true; packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplex-chat.flags.client_library = true; packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ # TODO: have a cross override for iOS, that sets this. @@ -565,6 +569,7 @@ extra-modules = [{ packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplex-chat.flags.client_library = true; packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ ((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;})) @@ -583,6 +588,7 @@ packages.simplexmq.flags.swift = true; packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplex-chat.flags.client_library = true; packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (pkgs.openssl.override { static = true; }) @@ -597,6 +603,7 @@ extra-modules = [{ packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplex-chat.flags.client_library = true; packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (pkgs.openssl.override { static = true; }) diff --git a/images/privacy-guides.jpg b/images/privacy-guides.jpg index f15a8862f7..5876d10c02 100644 Binary files a/images/privacy-guides.jpg and b/images/privacy-guides.jpg differ diff --git a/images/whonix-logo.jpg b/images/whonix-logo.jpg new file mode 100644 index 0000000000..c6fc4729b2 Binary files /dev/null and b/images/whonix-logo.jpg differ diff --git a/packages/simplex-chat-client/typescript/src/response.ts b/packages/simplex-chat-client/typescript/src/response.ts index 2e92e335df..5f91baa7db 100644 --- a/packages/simplex-chat-client/typescript/src/response.ts +++ b/packages/simplex-chat-client/typescript/src/response.ts @@ -84,7 +84,6 @@ export type ChatResponse = | CRGroupRemoved | CRGroupDeleted | CRGroupUpdated - | CRUserContactLinkSubscribed | CRUserContactLinkSubError | CRContactConnectionDeleted | CRMessageError @@ -182,7 +181,6 @@ type ChatResponseTag = | "groupRemoved" | "groupDeleted" | "groupUpdated" - | "userContactLinkSubscribed" | "userContactLinkSubError" | "newContactConnection" | "contactConnectionDeleted" @@ -721,10 +719,6 @@ export interface CRGroupUpdated extends CR { member_?: GroupMember } -export interface CRUserContactLinkSubscribed extends CR { - type: "userContactLinkSubscribed" -} - export interface CRUserContactLinkSubError extends CR { type: "userContactLinkSubError" chatError: ChatError diff --git a/scripts/ci/linux_util_free_space.sh b/scripts/ci/linux_util_free_space.sh new file mode 100755 index 0000000000..ef00eb886e --- /dev/null +++ b/scripts/ci/linux_util_free_space.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +# Taken from: https://github.com/apache/arrow/blob/main/ci/scripts/util_free_space.sh + +set -eux + +df -h +echo "::group::/usr/local/*" +du -hsc /usr/local/* +echo "::endgroup::" +# ~1GB +sudo rm -rf \ + /usr/local/aws-sam-cil \ + /usr/local/julia* || : +echo "::group::/usr/local/bin/*" +du -hsc /usr/local/bin/* +echo "::endgroup::" +# ~1GB (From 1.2GB to 214MB) +sudo rm -rf \ + /usr/local/bin/aliyun \ + /usr/local/bin/azcopy \ + /usr/local/bin/bicep \ + /usr/local/bin/cmake-gui \ + /usr/local/bin/cpack \ + /usr/local/bin/helm \ + /usr/local/bin/hub \ + /usr/local/bin/kubectl \ + /usr/local/bin/minikube \ + /usr/local/bin/node \ + /usr/local/bin/packer \ + /usr/local/bin/pulumi* \ + /usr/local/bin/sam \ + /usr/local/bin/stack \ + /usr/local/bin/terraform || : +# 142M +sudo rm -rf /usr/local/bin/oc || : \ +echo "::group::/usr/local/share/*" +du -hsc /usr/local/share/* +echo "::endgroup::" +# 506MB +sudo rm -rf /usr/local/share/chromium || : +# 1.3GB +sudo rm -rf /usr/local/share/powershell || : +echo "::group::/usr/local/lib/*" +du -hsc /usr/local/lib/* +echo "::endgroup::" +# 15GB +sudo rm -rf /usr/local/lib/android || : +# 341MB +sudo rm -rf /usr/local/lib/heroku || : +# 1.2GB +sudo rm -rf /usr/local/lib/node_modules || : +echo "::group::/opt/*" +du -hsc /opt/* +echo "::endgroup::" +# 679MB +sudo rm -rf /opt/az || : +echo "::group::/opt/microsoft/*" +du -hsc /opt/microsoft/* +echo "::endgroup::" +# 197MB +sudo rm -rf /opt/microsoft/powershell || : +echo "::group::/opt/hostedtoolcache/*" +du -hsc /opt/hostedtoolcache/* +echo "::endgroup::" +# 5.3GB +sudo rm -rf /opt/hostedtoolcache/CodeQL || : +# 1.4GB +sudo rm -rf /opt/hostedtoolcache/go || : +# 489MB +sudo rm -rf /opt/hostedtoolcache/PyPy || : +# 376MB +sudo rm -rf /opt/hostedtoolcache/node || : +# Remove Web browser packages +sudo apt purge -y \ + firefox \ + google-chrome-stable \ + microsoft-edge-stable +df -h diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index 80ae9fa82e..1db2755926 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -25,7 +25,7 @@ for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done rm -rf $BUILD_DIR -cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' --constraint 'simplexmq +client_library' +cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' cd $BUILD_DIR/build #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so #patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 66af5cbb0c..934d9b8d7d 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -28,10 +28,10 @@ rm -rf $BUILD_DIR if [[ "$DATABASE_BACKEND" == "postgres" ]]; then echo "Building with postgres backend..." - cabal build -f client_postgres lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' + cabal build -f client_postgres lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' else echo "Building with sqlite backend..." - cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' + cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' fi cd $BUILD_DIR/build diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index cbb886ccb3..af408d4054 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -51,7 +51,7 @@ echo " ghc-options: -shared -threaded -optl-L$openssl_windows_style_path -opt # Very important! Without it the build fails on linking step since the linker can't find exported symbols. # It looks like GHC bug because with such random path the build ends successfully sed -i "s/ld.lld.exe/abracadabra.exe/" `ghc --print-libdir`/settings -cabal build lib:simplex-chat --constraint 'simplexmq +client_library' +cabal build lib:simplex-chat --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index db84b67e63..b15e382207 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,114 @@ + + https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html + +

New in v6.3.1-4:

+
    +
  • fixes mentions with trailing punctuation (e.g., hello @name!).
  • +
  • recognizes domain names as links (e.g., simplex.chat).
  • +
  • forward compatibility with "knocking" (a feature for group admins to review and to chat with the new members prior to admitting them to groups, it will be released in 6.4)
  • +
  • support for connecting via short connection links.
  • +
  • fix related to backward/forward compatibility of the app in some rare cases.
  • +
  • scrolling/navigation improvements.
  • +
  • faster onboarding (conditions and operators are combined to one screen).
  • +
+

New in v6.3.0:

+
    +
  • Mention members and get notified when mentioned.
  • +
  • Send private reports to moderators.
  • +
  • Delete, block and change role for multiple members at once
  • +
  • Faster sending messages and faster deletion.
  • +
  • Organize chats into lists to keep track of what's important.
  • +
  • Jump to found and forwarded messages.
  • +
  • Private media file names.
  • +
  • Message expiration in chats.
  • +
+
+
+ + https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html + +

New in v6.3.1-3:

+
    +
  • support for connecting via short connection links.
  • +
  • fix related to backward/forward compatibility of the app in some rare cases.
  • +
  • scrolling/navigation improvements.
  • +
  • faster onboarding (conditions and operators are combined to one screen).
  • +
+

New in v6.3.0:

+
    +
  • Mention members and get notified when mentioned.
  • +
  • Send private reports to moderators.
  • +
  • Delete, block and change role for multiple members at once
  • +
  • Faster sending messages and faster deletion.
  • +
  • Organize chats into lists to keep track of what's important.
  • +
  • Jump to found and forwarded messages.
  • +
  • Private media file names.
  • +
  • Message expiration in chats.
  • +
+
+
+ + https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html + +

New in v6.3.1-2:

+
    +
  • fix related to backward/forward compatibility of the app in some rare cases.
  • +
  • scrolling/navigation improvements.
  • +
  • faster onboarding (conditions and operators are combined to one screen).
  • +
+

New in v6.3.0:

+
    +
  • Mention members and get notified when mentioned.
  • +
  • Send private reports to moderators.
  • +
  • Delete, block and change role for multiple members at once
  • +
  • Faster sending messages and faster deletion.
  • +
  • Organize chats into lists to keep track of what's important.
  • +
  • Jump to found and forwarded messages.
  • +
  • Private media file names.
  • +
  • Message expiration in chats.
  • +
+
+
+ + https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html + +

New in v6.3.1:

+
    +
  • scrolling/navigation improvements.
  • +
  • faster onboarding (conditions and operators are combined to one screen).
  • +
+

New in v6.3.0:

+
    +
  • Mention members and get notified when mentioned.
  • +
  • Send private reports to moderators.
  • +
  • Delete, block and change role for multiple members at once
  • +
  • Faster sending messages and faster deletion.
  • +
  • Organize chats into lists to keep track of what's important.
  • +
  • Jump to found and forwarded messages.
  • +
  • Private media file names.
  • +
  • Message expiration in chats.
  • +
+
+
+ + https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html + +

New in v6.3.0:

+
    +
  • Mention members and get notified when mentioned.
  • +
  • Send private reports to moderators.
  • +
  • Delete, block and change role for multiple members at once
  • +
  • Faster sending messages and faster deletion.
  • +
  • Organize chats into lists to keep track of what's important.
  • +
  • Jump to found and forwarded messages.
  • +
  • Private media file names.
  • +
  • Message expiration in chats.
  • +
+
+
https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 1a9de205f3..84f9d0db34 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."9fece9ce3df24d1b006d98e44a6b4e654861428b" = "0vf4kkj2jc4bg3bc9vsl0gv2z0l7z48626sqlk5wh7il6k14pn17"; + "https://github.com/simplex-chat/simplexmq.git"."3d62a383d5dcae6529d6d866233857182bcb4d47" = "133xm8jkim7agd6drwm3lbx1z7v8nf4l3asrm46ag3n2q201yfxc"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/scripts/simplex-chat-reproduce-builds.sh b/scripts/simplex-chat-reproduce-builds.sh new file mode 100755 index 0000000000..408d1d4a53 --- /dev/null +++ b/scripts/simplex-chat-reproduce-builds.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env sh +set -eu + +TAG="$1" + +tempdir="$(mktemp -d)" +init_dir="$PWD" + +repo_name="simplex-chat" +repo="https://github.com/simplex-chat/${repo_name}" + +cabal_local='ignore-project: False +package direct-sqlcipher + flags: +openssl' + +export DOCKER_BUILDKIT=1 + +cleanup() { + docker exec -t builder sh -c 'rm -rf ./dist-newstyle' 2>/dev/null || : + rm -rf -- "$tempdir" + docker rm --force builder 2>/dev/null || : + docker image rm local 2>/dev/null || : + cd "$init_dir" +} +trap 'cleanup' EXIT INT + +mkdir -p "$init_dir/$TAG-$repo_name/from-source" "$init_dir/$TAG-$repo_name/prebuilt" + +git -C "$tempdir" clone "$repo.git" &&\ + cd "$tempdir/${repo_name}" &&\ + git checkout "$TAG" + +for os in 22.04 24.04; do + os_url="$(printf '%s' "$os" | tr '.' '_')" + + # Build image + docker build \ + --no-cache \ + --build-arg TAG=${os} \ + --build-arg GHC=9.6.3 \ + -f "$tempdir/${repo_name}/Dockerfile.build" \ + -t local \ + . + + printf '%s' "$cabal_local" > "$tempdir/${repo_name}/cabal.project.local" + + # Run container in background + docker run -t -d \ + --name builder \ + -v "$tempdir/${repo_name}:/project" \ + local + + docker exec \ + -t \ + builder \ + sh -c 'cabal clean && cabal update && cabal build -j --enable-tests && mkdir -p /out && for i in simplex-chat; do bin=$(find /project/dist-newstyle -name "$i" -type f -executable) && chmod +x "$bin" && mv "$bin" /out/; done && strip /out/simplex-chat' + + docker cp \ + builder:/out/simplex-chat \ + "$init_dir/$TAG-$repo_name/from-source/simplex-chat-ubuntu-${os_url}-x86-64" + + # Download prebuilt postgresql binary + curl -L \ + --output-dir "$init_dir/$TAG-$repo_name/prebuilt/" \ + -O \ + "$repo/releases/download/${TAG}/simplex-chat-ubuntu-${os_url}-x86-64" + + # Important! Remove dist-newstyle for the next interation + docker exec \ + -t \ + builder \ + sh -c 'rm -rf ./dist-newstyle' + + # Also restore git to previous state + git reset --hard && git clean -dfx + + # Stop containers, delete images + docker stop builder + docker rm --force builder + docker image rm local +done + +# Cleanup +rm -rf -- "$tempdir" +cd "$init_dir" + +# Final stage: compare hashes + +# Path to binaries +path_bin="$init_dir/$TAG-$repo_name" + +# Assume everything is okay for now +bad=0 + +# Check hashes for all binaries +for file in "$path_bin"/from-source/*; do + # Extract binary name + app="$(basename $file)" + + # Compute hash for compiled binary + compiled=$(sha256sum "$path_bin/from-source/$app" | awk '{print $1}') + # Compute hash for prebuilt binary + prebuilt=$(sha256sum "$path_bin/prebuilt/$app" | awk '{print $1}') + + # Compare + if [ "$compiled" != "$prebuilt" ]; then + # If hashes doesn't match, set bad... + bad=1 + + # ... and print affected binary + printf "%s - sha256sum hash doesn't match\n" "$app" + fi +done + +# If everything is still okay, compute checksums file +if [ "$bad" = 0 ]; then + sha256sum "$path_bin"/from-source/* | sed -e "s|$PWD/||g" -e 's|from-source/||g' -e "s|-$repo_name||g" > "$path_bin/_sha256sums" + + printf 'Checksums computed - %s\n' "$path_bin/_sha256sums" +fi diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 950b689234..96b2f941c7 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.3.0.7 +version: 6.3.6.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -24,6 +24,11 @@ flag swift manual: True default: False +flag client_library + description: Don't build server- and CLI-related code. + manual: True + default: False + flag client_postgres description: Build with PostgreSQL instead of SQLite. manual: True @@ -33,13 +38,9 @@ library exposed-modules: Simplex.Chat Simplex.Chat.AppSettings - Simplex.Chat.Bot - Simplex.Chat.Bot.KnownContacts Simplex.Chat.Call Simplex.Chat.Controller - Simplex.Chat.Core Simplex.Chat.Files - Simplex.Chat.Help Simplex.Chat.Library.Commands Simplex.Chat.Library.Internal Simplex.Chat.Library.Subscriber @@ -54,6 +55,7 @@ library Simplex.Chat.Mobile.WebRTC Simplex.Chat.Operators Simplex.Chat.Operators.Conditions + Simplex.Chat.Operators.Presets Simplex.Chat.Options Simplex.Chat.Options.DB Simplex.Chat.ProfileGenerator @@ -78,23 +80,31 @@ library Simplex.Chat.Store.Remote Simplex.Chat.Store.Shared Simplex.Chat.Styled - Simplex.Chat.Terminal - Simplex.Chat.Terminal.Input - Simplex.Chat.Terminal.Main - Simplex.Chat.Terminal.Notification - Simplex.Chat.Terminal.Output Simplex.Chat.Types Simplex.Chat.Types.Preferences Simplex.Chat.Types.Shared Simplex.Chat.Types.UITheme Simplex.Chat.Types.Util Simplex.Chat.Util - Simplex.Chat.View + if !flag(client_library) + exposed-modules: + Simplex.Chat.Bot + Simplex.Chat.Bot.KnownContacts + Simplex.Chat.Core + Simplex.Chat.Help + Simplex.Chat.Terminal + Simplex.Chat.Terminal.Input + Simplex.Chat.Terminal.Main + Simplex.Chat.Terminal.Notification + Simplex.Chat.Terminal.Output + Simplex.Chat.View if flag(client_postgres) exposed-modules: Simplex.Chat.Options.Postgres Simplex.Chat.Store.Postgres.Migrations Simplex.Chat.Store.Postgres.Migrations.M20241220_initial + Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links + Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission else exposed-modules: Simplex.Chat.Archive @@ -224,6 +234,8 @@ library Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes + Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links + Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission other-modules: Paths_simplex_chat hs-source-dirs: @@ -296,6 +308,8 @@ library , text >=1.2.4.0 && <1.3 executable simplex-bot + if flag(client_library) + buildable: False main-is: Main.hs other-modules: Paths_simplex_chat @@ -313,6 +327,8 @@ executable simplex-bot cpp-options: -DdbPostgres executable simplex-bot-advanced + if flag(client_library) + buildable: False main-is: Main.hs other-modules: Paths_simplex_chat @@ -339,6 +355,8 @@ executable simplex-bot-advanced text >=1.2.4.0 && <1.3 executable simplex-broadcast-bot + if flag(client_library) + buildable: False main-is: Main.hs hs-source-dirs: apps/simplex-broadcast-bot @@ -369,6 +387,8 @@ executable simplex-broadcast-bot text >=1.2.4.0 && <1.3 executable simplex-chat + if flag(client_library) + buildable: False main-is: Main.hs other-modules: Server @@ -400,6 +420,8 @@ executable simplex-chat text >=1.2.4.0 && <1.3 executable simplex-directory-service + if flag(client_library) + buildable: False main-is: Main.hs hs-source-dirs: apps/simplex-directory-service @@ -408,6 +430,7 @@ executable simplex-directory-service StrictData other-modules: Directory.BlockedWords + Directory.Captcha Directory.Events Directory.Options Directory.Search @@ -446,6 +469,10 @@ executable simplex-directory-service , text >=1.2.4.0 && <1.3 test-suite simplex-chat-test + if flag(swift) + cpp-options: -DswiftJSON + if flag(client_library) + buildable: False type: exitcode-stdio-1.0 main-is: Test.hs other-modules: @@ -475,6 +502,7 @@ test-suite simplex-chat-test Broadcast.Bot Broadcast.Options Directory.BlockedWords + Directory.Captcha Directory.Events Directory.Options Directory.Search diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index bf07e4ae51..6b554d29c4 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -30,6 +30,7 @@ import Data.Time.Clock (getCurrentTime) import Simplex.Chat.Controller import Simplex.Chat.Library.Commands import Simplex.Chat.Operators +import Simplex.Chat.Operators.Presets import Simplex.Chat.Options import Simplex.Chat.Options.DB import Simplex.Chat.Protocol @@ -39,7 +40,7 @@ import Simplex.Chat.Types import Simplex.Chat.Util (shuffle) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore (dbNew)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -51,34 +52,6 @@ import qualified Simplex.Messaging.TMap as TM import qualified UnliftIO.Exception as E import UnliftIO.STM -operatorSimpleXChat :: NewServerOperator -operatorSimpleXChat = - ServerOperator - { operatorId = DBNewEntity, - operatorTag = Just OTSimplex, - tradeName = "SimpleX Chat", - legalName = Just "SimpleX Chat Ltd", - serverDomains = ["simplex.im"], - conditionsAcceptance = CARequired Nothing, - enabled = True, - smpRoles = allRoles, - xftpRoles = allRoles - } - -operatorFlux :: NewServerOperator -operatorFlux = - ServerOperator - { operatorId = DBNewEntity, - operatorTag = Just OTFlux, - tradeName = "Flux", - legalName = Just "InFlux Technologies Limited", - serverDomains = ["simplexonflux.com"], - conditionsAcceptance = CARequired Nothing, - enabled = False, - smpRoles = ServerRoles {storage = False, proxy = True}, - xftpRoles = ServerRoles {storage = False, proxy = True} - } - defaultChatConfig :: ChatConfig defaultChatConfig = ChatConfig @@ -112,6 +85,11 @@ defaultChatConfig = ntf = _defaultNtfServers, netCfg = defaultNetworkConfig }, + -- please note: if these servers are changed, this option needs to be split to two, + -- to have a different set of servers on the receiving end and on the sending end. + -- To preserve backward compatibility receiving end should update before the sending. + shortLinkPresetServers = allPresetServers, + presetDomains = [".simplex.im", ".simplexonflux.com"], tbqSize = 1024, fileChunkSize = 15780, -- do not change xftpDescrPartSize = 14000, @@ -133,53 +111,6 @@ defaultChatConfig = chatHooks = defaultChatHooks } -simplexChatSMPServers :: [NewUserServer 'PSMP] -simplexChatSMPServers = - map - (presetServer True) - [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", - "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", - "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", - "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", - "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", - "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", - "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", - "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", - "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", - "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", - "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" - ] - <> map - (presetServer False) - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" - ] - -fluxSMPServers :: [NewUserServer 'PSMP] -fluxSMPServers = - map - (presetServer True) - [ "smp://xQW_ufMkGE20UrTlBl8QqceG1tbuylXhr9VOLPyRJmw=@smp1.simplexonflux.com,qb4yoanyl4p7o33yrknv4rs6qo7ugeb2tu2zo66sbebezs4cpyosarid.onion", - "smp://LDnWZVlAUInmjmdpQQoIo6FUinRXGe0q3zi5okXDE4s=@smp2.simplexonflux.com,yiqtuh3q4x7hgovkomafsod52wvfjucdljqbbipg5sdssnklgongxbqd.onion", - "smp://1jne379u7IDJSxAvXbWb_JgoE7iabcslX0LBF22Rej0=@smp3.simplexonflux.com,a5lm4k7ufei66cdck6fy63r4lmkqy3dekmmb7jkfdm5ivi6kfaojshad.onion", - "smp://xmAmqj75I9mWrUihLUlI0ZuNLXlIwFIlHRq5Pb6cHAU=@smp4.simplexonflux.com,qpcz2axyy66u26hfdd2e23uohcf3y6c36mn7dcuilcgnwjasnrvnxjqd.onion", - "smp://rWvBYyTamuRCBYb_KAn-nsejg879ndhiTg5Sq3k0xWA=@smp5.simplexonflux.com,4ao347qwiuluyd45xunmii4skjigzuuox53hpdsgbwxqafd4yrticead.onion", - "smp://PN7-uqLBToqlf1NxHEaiL35lV2vBpXq8Nj8BW11bU48=@smp6.simplexonflux.com,hury6ot3ymebbr2535mlp7gcxzrjpc6oujhtfxcfh2m4fal4xw5fq6qd.onion" - ] - -fluxXFTPServers :: [NewUserServer 'PXFTP] -fluxXFTPServers = - map - (presetServer True) - [ "xftp://92Sctlc09vHl_nAqF2min88zKyjdYJ9mgxRCJns5K2U=@xftp1.simplexonflux.com,apl3pumq3emwqtrztykyyoomdx4dg6ysql5zek2bi3rgznz7ai3odkid.onion", - "xftp://YBXy4f5zU1CEhnbbCzVWTNVNsaETcAGmYqGNxHntiE8=@xftp2.simplexonflux.com,c5jjecisncnngysah3cz2mppediutfelco4asx65mi75d44njvua3xid.onion", - "xftp://ARQO74ZSvv2OrulRF3CdgwPz_AMy27r0phtLSq5b664=@xftp3.simplexonflux.com,dc4mohiubvbnsdfqqn7xhlhpqs5u4tjzp7xpz6v6corwvzvqjtaqqiqd.onion", - "xftp://ub2jmAa9U0uQCy90O-fSUNaYCj6sdhl49Jh3VpNXP58=@xftp4.simplexonflux.com,4qq5pzier3i4yhpuhcrhfbl6j25udc4czoyascrj4yswhodhfwev3nyd.onion", - "xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion", - "xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion" - ] - logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} @@ -194,7 +125,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} - ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} + ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations @@ -310,12 +241,12 @@ newChatController randomServerCfgs name p opDomains rndSrvs = toJustOrError name $ L.nonEmpty $ agentServerCfgs p opDomains $ concatMap (pServers p) rndSrvs agentServers :: DB.Connection -> ChatConfig -> NonEmpty PresetOperator -> RandomAgentServers -> IO InitialAgentServers - agentServers db ChatConfig {presetServers = PresetServers {ntf, netCfg}} presetOps as = do + agentServers db ChatConfig {presetServers = PresetServers {ntf, netCfg}, presetDomains} presetOps as = do users <- getUsers db ops <- getUpdateServerOperators db presetOps (null users) let opDomains = operatorDomains $ mapMaybe snd ops (smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users - pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg} + pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains} where optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))] optServers srvs overrides_ = case L.nonEmpty overrides_ of diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs index 23b5f2ddad..1efa69fad4 100644 --- a/src/Simplex/Chat/AppSettings.hs +++ b/src/Simplex/Chat/AppSettings.hs @@ -25,8 +25,6 @@ data NotificationPreviewMode = NPMHidden | NPMContact | NPMMessage deriving (Sho data LockScreenCalls = LSCDisable | LSCShow | LSCAccept deriving (Show) -data OpenLinksSetting = OLSYes | OLSNo | OLSAsk deriving (Show) - data AppSettings = AppSettings { appPlatform :: Maybe AppPlatform, networkConfig :: Maybe NetworkConfig, @@ -35,7 +33,6 @@ data AppSettings = AppSettings privacyAskToApproveRelays :: Maybe Bool, privacyAcceptImages :: Maybe Bool, privacyLinkPreviews :: Maybe Bool, - privacyChatListOpenLinks :: Maybe OpenLinksSetting, privacyShowChatPreviews :: Maybe Bool, privacySaveLastDraft :: Maybe Bool, privacyProtectScreen :: Maybe Bool, @@ -86,7 +83,6 @@ defaultAppSettings = privacyAskToApproveRelays = Just True, privacyAcceptImages = Just True, privacyLinkPreviews = Just True, - privacyChatListOpenLinks = Just OLSAsk, privacyShowChatPreviews = Just True, privacySaveLastDraft = Just True, privacyProtectScreen = Just False, @@ -124,7 +120,6 @@ defaultParseAppSettings = privacyAskToApproveRelays = Nothing, privacyAcceptImages = Nothing, privacyLinkPreviews = Nothing, - privacyChatListOpenLinks = Nothing, privacyShowChatPreviews = Nothing, privacySaveLastDraft = Nothing, privacyProtectScreen = Nothing, @@ -162,7 +157,6 @@ combineAppSettings platformDefaults storedSettings = privacyAskToApproveRelays = p privacyAskToApproveRelays, privacyAcceptImages = p privacyAcceptImages, privacyLinkPreviews = p privacyLinkPreviews, - privacyChatListOpenLinks = p privacyChatListOpenLinks, privacyShowChatPreviews = p privacyShowChatPreviews, privacySaveLastDraft = p privacySaveLastDraft, privacyProtectScreen = p privacyProtectScreen, @@ -203,8 +197,6 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "LSC") ''LockScreenCalls) $(JQ.deriveJSON (enumJSON $ dropPrefix "NPA") ''NetworkProxyAuth) -$(JQ.deriveJSON (enumJSON $ dropPrefix "OLS") ''OpenLinksSetting) - $(JQ.deriveJSON defaultJSON ''NetworkProxy) $(JQ.deriveToJSON defaultJSON ''AppSettings) @@ -218,7 +210,6 @@ instance FromJSON AppSettings where privacyAskToApproveRelays <- p "privacyAskToApproveRelays" privacyAcceptImages <- p "privacyAcceptImages" privacyLinkPreviews <- p "privacyLinkPreviews" - privacyChatListOpenLinks <- p "privacyChatListOpenLinks" privacyShowChatPreviews <- p "privacyShowChatPreviews" privacySaveLastDraft <- p "privacySaveLastDraft" privacyProtectScreen <- p "privacyProtectScreen" @@ -253,7 +244,6 @@ instance FromJSON AppSettings where privacyAskToApproveRelays, privacyAcceptImages, privacyLinkPreviews, - privacyChatListOpenLinks, privacyShowChatPreviews, privacySaveLastDraft, privacyProtectScreen, diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 54e7baa194..73a2970c61 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -15,6 +15,7 @@ import qualified Data.ByteString.Char8 as B import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M +import Data.Maybe (isJust) import Data.Text (Text) import qualified Data.Text as T import Simplex.Chat.Controller @@ -24,6 +25,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Store import Simplex.Chat.Types (Contact (..), ContactId, IsContact (..), User (..)) +import Simplex.Messaging.Agent.Protocol (CreatedConnLink (..)) import Simplex.Messaging.Encoding.String (strEncode) import System.Exit (exitFailure) @@ -31,12 +33,12 @@ chatBotRepl :: String -> (Contact -> String -> IO String) -> User -> ChatControl chatBotRepl welcome answer _user cc = do initializeBotAddress cc race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - case resp of - CRContactConnected _ contact _ -> do + (_, event) <- atomically . readTBQueue $ outputQ cc + case event of + Right (CEvtContactConnected _ contact _) -> do contactConnected contact void $ sendMessage cc contact $ T.pack welcome - 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 = T.unpack $ ciContentToText mc void $ sendMessage cc contact . T.pack =<< answer contact msg _ -> pure () @@ -49,16 +51,18 @@ initializeBotAddress = initializeBotAddress' True initializeBotAddress' :: Bool -> ChatController -> IO () initializeBotAddress' logAddress cc = do sendChatCmd cc ShowMyAddress >>= \case - CRUserContactLink _ UserContactLink {connReqContact} -> showBotAddress connReqContact - CRChatCmdError _ (ChatErrorStore SEUserContactLinkNotFound) -> do + Right (CRUserContactLink _ UserContactLink {connLinkContact}) -> showBotAddress connLinkContact + Left (ChatErrorStore SEUserContactLinkNotFound) -> do when logAddress $ putStrLn "No bot address, creating..." - sendChatCmd cc CreateMyAddress >>= \case - CRUserContactLinkCreated _ uri -> showBotAddress uri + -- TODO [short links] create short link by default + sendChatCmd cc (CreateMyAddress False) >>= \case + Right (CRUserContactLinkCreated _ ccLink) -> showBotAddress ccLink _ -> putStrLn "can't create bot address" >> exitFailure _ -> putStrLn "unexpected response" >> exitFailure where - showBotAddress uri = do - when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) + showBotAddress (CCLink uri shortUri) = do + when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (maybe (strEncode uri) strEncode shortUri) + when (isJust shortUri) $ putStrLn $ "Full contact address for old clients: " <> B.unpack (strEncode uri) void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {businessAddress = False, acceptIncognito = False, autoReply = Nothing} sendMessage :: ChatController -> Contact -> Text -> IO () @@ -80,14 +84,14 @@ sendComposedMessages_ :: ChatController -> SendRef -> NonEmpty (Maybe ChatItemId sendComposedMessages_ cc sendRef qmcs = do let cms = L.map (\(qiId, mc) -> ComposedMessage {fileSource = Nothing, quotedItemId = qiId, msgContent = mc, mentions = M.empty}) qmcs sendChatCmd cc (APISendMessages sendRef False Nothing cms) >>= \case - CRNewChatItems {} -> printLog cc CLLInfo $ "sent " <> show (length cms) <> " messages to " <> show sendRef + Right (CRNewChatItems {}) -> printLog cc CLLInfo $ "sent " <> show (length cms) <> " messages to " <> show sendRef r -> putStrLn $ "unexpected send message response: " <> show r deleteMessage :: ChatController -> Contact -> ChatItemId -> IO () deleteMessage cc ct chatItemId = do let cmd = APIDeleteChatItem (contactRef ct) [chatItemId] CIDMInternal sendChatCmd cc cmd >>= \case - CRChatItemsDeleted {} -> printLog cc CLLInfo $ "deleted message(s) from " <> contactInfo ct + Right (CRChatItemsDeleted {}) -> printLog cc CLLInfo $ "deleted message(s) from " <> contactInfo ct r -> putStrLn $ "unexpected delete message response: " <> show r contactRef :: Contact -> ChatRef diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs index 3b1f28dd27..18398ecb3e 100644 --- a/src/Simplex/Chat/Call.hs +++ b/src/Simplex/Chat/Call.hs @@ -23,10 +23,10 @@ import Data.Text (Text) import Data.Time.Clock (UTCTime) import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types (Contact, ContactId, User) -import Simplex.Messaging.Agent.Store.DB (Binary (..)) +import Simplex.Messaging.Agent.Store.DB (Binary (..), fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, singleFieldJSON) import Simplex.Messaging.Util (decodeJSON, encodeJSON) data Call = Call diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index e639771f41..02e2e121b3 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -41,6 +41,7 @@ import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe) +import Data.Set (Set) import Data.String import Data.Text (Text) import Data.Text.Encoding (decodeLatin1) @@ -49,11 +50,11 @@ import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Data.Version (showVersion) import Data.Word (Word16) import Language.Haskell.TH (Exp, Q, runIO) +import Network.Socket (HostName) import Numeric.Natural import qualified Paths_simplex_chat as SC import Simplex.Chat.AppSettings import Simplex.Chat.Call -import Simplex.Chat.Markdown (MarkdownList) import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Operators @@ -76,7 +77,7 @@ import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, UpMigration) import qualified Simplex.Messaging.Agent.Store.DB as DB -import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..)) +import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -84,7 +85,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) @@ -137,6 +138,8 @@ data ChatConfig = ChatConfig chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, presetServers :: PresetServers, + shortLinkPresetServers :: NonEmpty SMPServer, + presetDomains :: [HostName], tbqSize :: Natural, fileChunkSize :: Integer, xftpDescrPartSize :: Int, @@ -169,10 +172,10 @@ data ChatHooks = ChatHooks { -- preCmdHook can be used to process or modify the commands before they are processed. -- This hook should be used to process CustomChatCommand. -- if this hook returns ChatResponse, the command processing will be skipped. - preCmdHook :: Maybe (ChatController -> ChatCommand -> IO (Either ChatResponse ChatCommand)), + preCmdHook :: Maybe (ChatController -> ChatCommand -> IO (Either (Either ChatError ChatResponse) ChatCommand)), -- eventHook can be used to additionally process or modify events, -- it is called before the event is sent to the user (or to the UI). - eventHook :: Maybe (ChatController -> ChatResponse -> IO ChatResponse), + eventHook :: Maybe (ChatController -> Either ChatError ChatEvent -> IO (Either ChatError ChatEvent)), -- acceptMember hook can be used to accept or reject member connecting via group link without API calls acceptMember :: Maybe (GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole))) } @@ -220,7 +223,7 @@ data ChatController = ChatController random :: TVar ChaChaDRG, eventSeq :: TVar Int, inputQ :: TBQueue String, - outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse), + outputQ :: TBQueue (Maybe RemoteHostId, Either ChatError ChatEvent), connNetworkStatuses :: TMap AgentConnId NetworkStatus, subscriptionMode :: TVar SubscriptionMode, chatLock :: Lock, @@ -353,17 +356,17 @@ data ChatCommand | APICheckToken DeviceToken | APIDeleteToken DeviceToken | APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString} - | ApiGetConnNtfMessages {connIds :: NonEmpty AgentConnId} + | APIGetConnNtfMessages (NonEmpty ConnMsgReq) | APIAddMember GroupId ContactId GroupMemberRole | APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter} | APIAcceptMember GroupId GroupMemberId GroupMemberRole | APIMembersRole GroupId (NonEmpty GroupMemberId) GroupMemberRole | APIBlockMembersForAll GroupId (NonEmpty GroupMemberId) Bool - | APIRemoveMembers GroupId (NonEmpty GroupMemberId) + | APIRemoveMembers {groupId :: GroupId, groupMemberIds :: Set GroupMemberId, withMessages :: Bool} | APILeaveGroup GroupId | APIListMembers GroupId | APIUpdateGroupProfile GroupId GroupProfile - | APICreateGroupLink GroupId GroupMemberRole + | APICreateGroupLink GroupId GroupMemberRole CreateShortLink | APIGroupLinkMemberRole GroupId GroupMemberRole | APIDeleteGroupLink GroupId | APIGetGroupLink GroupId @@ -436,21 +439,21 @@ data ChatCommand | EnableGroupMember GroupName ContactName | ChatHelp HelpSection | Welcome - | APIAddContact UserId IncognitoEnabled - | AddContact IncognitoEnabled + | APIAddContact UserId CreateShortLink IncognitoEnabled + | AddContact CreateShortLink IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to - | APIConnectPlan UserId AConnectionRequestUri - | APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) - | Connect IncognitoEnabled (Maybe AConnectionRequestUri) + | APIConnectPlan UserId AConnectionLink + | APIConnect UserId IncognitoEnabled (Maybe ACreatedConnLink) + | Connect IncognitoEnabled (Maybe AConnectionLink) | APIConnectContactViaAddress UserId IncognitoEnabled ContactId | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | DeleteContact ContactName ChatDeleteMode | ClearContact ContactName | APIListContacts UserId | ListContacts - | APICreateMyAddress UserId - | CreateMyAddress + | APICreateMyAddress UserId CreateShortLink + | CreateMyAddress CreateShortLink | APIDeleteMyAddress UserId | DeleteMyAddress | APIShowMyAddress UserId @@ -468,7 +471,7 @@ data ChatCommand | SendMemberContactMessage GroupName ContactName Text | SendLiveMessage ChatName Text | SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: Text, message :: Text} - | SendMessageBroadcast Text -- UserId (not used in UI) + | SendMessageBroadcast MsgContent -- UserId (not used in UI) | DeleteMessage ChatName Text | DeleteMemberMessage GroupName ContactName Text | EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text} @@ -480,7 +483,7 @@ data ChatCommand | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} | MemberRole GroupName ContactName GroupMemberRole | BlockForAll GroupName ContactName Bool - | RemoveMembers GroupName (NonEmpty ContactName) + | RemoveMembers {groupName :: GroupName, members :: Set ContactName, withMessages :: Bool} | LeaveGroup GroupName | DeleteGroup GroupName | ClearGroup GroupName @@ -491,7 +494,7 @@ data ChatCommand | ShowGroupProfile GroupName | UpdateGroupDescription GroupName (Maybe Text) | ShowGroupDescription GroupName - | CreateGroupLink GroupName GroupMemberRole + | CreateGroupLink GroupName GroupMemberRole CreateShortLink | GroupLinkMemberRole GroupName GroupMemberRole | DeleteGroupLink GroupName | ShowGroupLink GroupName @@ -544,7 +547,7 @@ data ChatCommand | QuitChat | ShowVersion | DebugLocks - | DebugEvent ChatResponse + | DebugEvent ChatEvent | GetAgentSubsTotal UserId | GetAgentServersSummary UserId | ResetAgentServersStats @@ -604,7 +607,6 @@ data ChatResponse | CRChatStarted | CRChatRunning | CRChatStopped - | CRChatSuspended | CRApiChats {user :: User, chats :: [AChat]} | CRChats {chats :: [AChat]} | CRApiChat {user :: User, chat :: AChat, navInfo :: Maybe NavigationInfo} @@ -612,7 +614,6 @@ data ChatResponse | CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]} | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} | CRChatItemId User (Maybe ChatItemId) - | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} | CRServerOperatorConditions {conditions :: ServerOperatorConditions} | CRUserServers {user :: User, userServers :: [UserOperatorServers]} @@ -628,30 +629,20 @@ data ChatResponse | CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} | CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} - | CRContactSwitch {user :: User, contact :: Contact, switchProgress :: SwitchProgress} - | CRGroupMemberSwitch {user :: User, groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress} | CRContactRatchetSyncStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberRatchetSyncStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} - | CRContactRatchetSync {user :: User, contact :: Contact, ratchetSyncProgress :: RatchetSyncProgress} - | CRGroupMemberRatchetSync {user :: User, groupInfo :: GroupInfo, member :: GroupMember, ratchetSyncProgress :: RatchetSyncProgress} - | CRContactVerificationReset {user :: User, contact :: Contact} - | CRGroupMemberVerificationReset {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRContactCode {user :: User, contact :: Contact, connectionCode :: Text} | CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text} | CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text} | CRTagsUpdated {user :: User, userTags :: [ChatTag], chatTags :: [ChatTagId]} | CRNewChatItems {user :: User, chatItems :: [AChatItem]} - | CRChatItemsStatusesUpdated {user :: User, chatItems :: [AChatItem]} | CRChatItemUpdated {user :: User, chatItem :: AChatItem} | CRChatItemNotChanged {user :: User, chatItem :: AChatItem} | CRChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction} | CRReactionMembers {user :: User, memberReactions :: [MemberReaction]} | CRChatItemsDeleted {user :: User, chatItemDeletions :: [ChatItemDeletion], byUser :: Bool, timed :: Bool} | CRGroupChatItemsDeleted {user :: User, groupInfo :: GroupInfo, chatItemIDs :: [ChatItemId], byUser :: Bool, member_ :: Maybe GroupMember} - | CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} | CRBroadcastSent {user :: User, msgContent :: MsgContent, successes :: Int, failures :: Int, timestamp :: UTCTime} - | CRMsgIntegrityError {user :: User, msgError :: MsgErrorType} - | CRCmdAccepted {corr :: CorrId} | CRCmdOk {user_ :: Maybe User} | CRChatHelp {helpSection :: HelpSection} | CRWelcome {user :: User} @@ -662,9 +653,7 @@ data ChatResponse | CRUserContactLinkUpdated {user :: User, contactLink :: UserContactLink} | CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest} | CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} - | CRGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} - | CRBusinessLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, fromContact :: Contact} - | CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]} + | CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool} | CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]} | CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} | CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus @@ -673,141 +662,61 @@ data ChatResponse | CRUserProfileNoChange {user :: User} | CRUserPrivacy {user :: User, updatedUser :: User} | CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]} - | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection} + | CRInvitation {user :: User, connLinkInvitation :: CreatedLinkInvitation, connection :: PendingContactConnection} | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection} | CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User} - | CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan} + | CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, connectionPlan :: ConnectionPlan} | CRSentConfirmation {user :: User, connection :: PendingContactConnection} | CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile} | CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile} - | CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} - | CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember} - | CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact} | CRContactDeleted {user :: User, contact :: Contact} - | CRContactDeletedByContact {user :: User, contact :: Contact} | CRChatCleared {user :: User, chatInfo :: AChatInfo} - | CRUserContactLinkCreated {user :: User, connReqContact :: ConnReqContact} + | CRUserContactLinkCreated {user :: User, connLinkContact :: CreatedLinkContact} | CRUserContactLinkDeleted {user :: User} - | CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest} | CRAcceptingContactRequest {user :: User, contact :: Contact} - | CRAcceptingBusinessRequest {user :: User, groupInfo :: GroupInfo} | CRContactAlreadyExists {user :: User, contact :: Contact} - | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} - | CRBusinessRequestAlreadyAccepted {user :: User, groupInfo :: GroupInfo} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo} | CRForwardPlan {user :: User, itemsCount :: Int, chatItemIds :: [ChatItemId], forwardConfirmation :: Maybe ForwardConfirmation} - | CRRcvFileDescrReady {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer, rcvFileDescr :: RcvFileDescr} | CRRcvFileAccepted {user :: User, chatItem :: AChatItem} + -- TODO add chatItem :: AChatItem | CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} | CRStandaloneFileInfo {fileMeta :: Maybe J.Value} | CRRcvStandaloneFileCreated {user :: User, rcvFileTransfer :: RcvFileTransfer} -- returned by _download - | CRRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats - | CRRcvFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, receivedSize :: Int64, totalSize :: Int64, rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileComplete {user :: User, chatItem :: AChatItem} - | CRRcvStandaloneFileComplete {user :: User, targetPath :: FilePath, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileError {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileWarning {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} - | CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} - | CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} - | CRSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]} | CRSndStandaloneFileCreated {user :: User, fileTransferMeta :: FileTransferMeta} -- returned by _upload - | CRSndFileStartXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} -- not used - | CRSndFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64} - | CRSndFileRedirectStartXFTP {user :: User, fileTransferMeta :: FileTransferMeta, redirectMeta :: FileTransferMeta} - | CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} - | CRSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]} - | CRSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta} - | CRSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} - | CRSndFileWarning {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} | CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary} | CRUserProfileImage {user :: User, profile :: Profile} | CRContactAliasUpdated {user :: User, toContact :: Contact} | CRGroupAliasUpdated {user :: User, toGroup :: GroupInfo} | CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection} | CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact} - | CRContactConnecting {user :: User, contact :: Contact} - | CRContactConnected {user :: User, contact :: Contact, userCustomProfile :: Maybe Profile} - | CRContactSndReady {user :: User, contact :: Contact} - | CRContactAnotherClient {user :: User, contact :: Contact} - | CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} - | CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} - | CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]} - | CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError} - | CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]} - | CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]} - | CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]} | CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]} - | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} - | CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} - | CRGroupInvitation {user :: User, shortGroupInfo :: ShortGroupInfo} - | CRReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} - | CRUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} | CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} - | CRMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} | CRMembersRoleUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], toRole :: GroupMemberRole} - | CRMemberBlockedForAll {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, blocked :: Bool} | CRMembersBlockedForAllUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], blocked :: Bool} - | CRConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, memberContact :: Maybe Contact} - | CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} - | CRDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRUnknownMemberCreated {user :: User, groupInfo :: GroupInfo, forwardedByMember :: GroupMember, member :: GroupMember} - | CRUnknownMemberBlocked {user :: User, groupInfo :: GroupInfo, blockedByMember :: GroupMember, member :: GroupMember} - | CRUnknownMemberAnnounced {user :: User, groupInfo :: GroupInfo, announcingMember :: GroupMember, unknownMember :: GroupMember, announcedMember :: GroupMember} - | CRGroupEmpty {user :: User, shortGroupInfo :: ShortGroupInfo} - | CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} | CRGroupProfile {user :: User, groupInfo :: GroupInfo} | CRGroupDescription {user :: User, groupInfo :: GroupInfo} -- only used in CLI - | CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole} - | CRGroupLink {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole} + | CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connLinkContact :: CreatedLinkContact, memberRole :: GroupMemberRole} + | CRGroupLink {user :: User, groupInfo :: GroupInfo, connLinkContact :: CreatedLinkContact, memberRole :: GroupMemberRole} | CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo} - | CRAcceptingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRNoMemberContactCreating {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- only used in CLI | CRNewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} | CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} - | CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} - | CRContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact} - | CRMemberSubError {user :: User, shortGroupInfo :: ShortGroupInfo, memberToSubscribe :: ShortGroupMember, chatError :: ChatError} - | CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]} - | CRGroupSubscribed {user :: User, shortGroupInfo :: ShortGroupInfo} - | CRPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]} - | CRSndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError} - | CRRcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} - | CRCallInvitation {callInvitation :: RcvCallInvitation} - | CRCallOffer {user :: User, contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool} - | CRCallAnswer {user :: User, contact :: Contact, answer :: WebRTCSession} - | CRCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo} - | CRCallEnded {user :: User, contact :: Contact} | CRCallInvitations {callInvitations :: [RcvCallInvitation]} - | CRUserContactLinkSubscribed -- TODO delete - | CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete | CRNtfTokenStatus {status :: NtfTknStatus} | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer} | CRNtfConns {ntfConns :: [NtfConn]} - | CRConnNtfMessages {receivedMsgs :: NonEmpty (Maybe NtfMsgInfo)} - | CRNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgAckInfo} + | CRConnNtfMessages {receivedMsgs :: NonEmpty RcvNtfMsgInfo} | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} | CRCurrentRemoteHost {remoteHost_ :: Maybe RemoteHostInfo} | CRRemoteHostStarted {remoteHost_ :: Maybe RemoteHostInfo, invitation :: Text, ctrlPort :: String, localAddrs :: NonEmpty RCCtrlAddress} - | CRRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text} - | CRNewRemoteHost {remoteHost :: RemoteHostInfo} - | CRRemoteHostConnected {remoteHost :: RemoteHostInfo} - | CRRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId, rhsState :: RemoteHostSessionState, rhStopReason :: RemoteHostStopReason} | CRRemoteFileStored {remoteHostId :: RemoteHostId, remoteFileSource :: CryptoFile} | CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]} - | CRRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo, ctrlAppInfo_ :: Maybe CtrlAppInfo, appVersion :: AppVersion, compatible :: Bool} | CRRemoteCtrlConnecting {remoteCtrl_ :: Maybe RemoteCtrlInfo, ctrlAppInfo :: CtrlAppInfo, appVersion :: AppVersion} - | CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} | CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo} - | CRRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason} - | CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption} | CRSQLResult {rows :: [Text]} #if !defined(dbPostgres) | CRArchiveExported {archiveErrors :: [ArchiveError]} @@ -822,25 +731,132 @@ data ChatResponse | CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]} | CRAgentSubsDetails {agentSubs :: SubscriptionsInfo} | CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo} - | CRContactDisabled {user :: User, contact :: Contact} - | CRConnectionDisabled {connectionEntity :: ConnectionEntity} - | CRConnectionInactive {connectionEntity :: ConnectionEntity, inactive :: Bool} - | CRAgentRcvQueuesDeleted {deletedRcvQueues :: NonEmpty DeletedRcvQueue} - | CRAgentConnsDeleted {agentConnIds :: NonEmpty AgentConnId} - | CRAgentUserDeleted {agentUserId :: Int64} - | CRMessageError {user :: User, severity :: Text, errorMessage :: Text} - | CRChatCmdError {user_ :: Maybe User, chatError :: ChatError} - | CRChatError {user_ :: Maybe User, chatError :: ChatError} - | CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]} | CRAppSettings {appSettings :: AppSettings} - | CRTimedAction {action :: String, durationMilliseconds :: Int64} | CRCustomChatResponse {user_ :: Maybe User, response :: Text} - | CRTerminalEvent TerminalEvent + deriving (Show) + +data ChatEvent + = CEvtChatSuspended + | CEvtContactSwitch {user :: User, contact :: Contact, switchProgress :: SwitchProgress} + | CEvtGroupMemberSwitch {user :: User, groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress} + | CEvtContactRatchetSync {user :: User, contact :: Contact, ratchetSyncProgress :: RatchetSyncProgress} + | CEvtGroupMemberRatchetSync {user :: User, groupInfo :: GroupInfo, member :: GroupMember, ratchetSyncProgress :: RatchetSyncProgress} + | CEvtNewChatItems {user :: User, chatItems :: [AChatItem]} -- there is the same command response + | CEvtChatItemsStatusesUpdated {user :: User, chatItems :: [AChatItem]} + | CEvtChatItemUpdated {user :: User, chatItem :: AChatItem} -- there is the same command response + | CEvtChatItemNotChanged {user :: User, chatItem :: AChatItem} -- there is the same command response + | CEvtChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction} -- there is the same command response + | CEvtGroupChatItemsDeleted {user :: User, groupInfo :: GroupInfo, chatItemIDs :: [ChatItemId], byUser :: Bool, member_ :: Maybe GroupMember} -- there is the same command response + | CEvtChatItemsDeleted {user :: User, chatItemDeletions :: [ChatItemDeletion], byUser :: Bool, timed :: Bool} -- there is the same command response + | CEvtChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} + | CEvtUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} -- there is the same command response + | CEvtGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} + | CEvtBusinessLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, fromContact :: Contact} + | CEvtSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} -- there is the same command response + | CEvtContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} + | CEvtGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember} + | CEvtContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact} + | CEvtContactDeletedByContact {user :: User, contact :: Contact} + | CEvtReceivedContactRequest {user :: User, contactRequest :: UserContactRequest} + | CEvtAcceptingContactRequest {user :: User, contact :: Contact} -- there is the same command response + | CEvtAcceptingBusinessRequest {user :: User, groupInfo :: GroupInfo} + | CEvtContactRequestAlreadyAccepted {user :: User, contact :: Contact} + | CEvtBusinessRequestAlreadyAccepted {user :: User, groupInfo :: GroupInfo} + | CEvtRcvFileDescrReady {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer, rcvFileDescr :: RcvFileDescr} + | CEvtRcvFileAccepted {user :: User, chatItem :: AChatItem} -- there is the same command response + -- TODO add chatItem :: AChatItem + | CEvtRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} -- there is the same command response + | CEvtRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats + | CEvtRcvFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, receivedSize :: Int64, totalSize :: Int64, rcvFileTransfer :: RcvFileTransfer} + | CEvtRcvFileComplete {user :: User, chatItem :: AChatItem} + | CEvtRcvStandaloneFileComplete {user :: User, targetPath :: FilePath, rcvFileTransfer :: RcvFileTransfer} + | CEvtRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} + | CEvtRcvFileError {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} + | CEvtRcvFileWarning {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} + | CEvtSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} + | CEvtSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} + | CEvtSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer} + | CEvtSndFileStartXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} -- not used + | CEvtSndFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64} + | CEvtSndFileRedirectStartXFTP {user :: User, fileTransferMeta :: FileTransferMeta, redirectMeta :: FileTransferMeta} + | CEvtSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} + | CEvtSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]} + | CEvtSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta} + | CEvtSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} + | CEvtSndFileWarning {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} + | CEvtContactConnecting {user :: User, contact :: Contact} + | CEvtContactConnected {user :: User, contact :: Contact, userCustomProfile :: Maybe Profile} + | CEvtContactSndReady {user :: User, contact :: Contact} + | CEvtContactAnotherClient {user :: User, contact :: Contact} + | CEvtSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} + | CEvtContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} + | CEvtContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]} + | CEvtContactSubError {user :: User, contact :: Contact, chatError :: ChatError} + | CEvtContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]} + | CEvtUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]} + | CEvtNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]} + | CEvtNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]} -- there is the same command response + | CEvtHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} + | CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} + | CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} + | CEvtUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} + | CEvtJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- there is the same command response + | CEvtJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} + | CEvtMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} + | CEvtMemberBlockedForAll {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, blocked :: Bool} + | CEvtConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, memberContact :: Maybe Contact} + | CEvtDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember, withMessages :: Bool} + | CEvtDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember, withMessages :: Bool} + | CEvtLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CEvtUnknownMemberCreated {user :: User, groupInfo :: GroupInfo, forwardedByMember :: GroupMember, member :: GroupMember} + | CEvtUnknownMemberBlocked {user :: User, groupInfo :: GroupInfo, blockedByMember :: GroupMember, member :: GroupMember} + | CEvtUnknownMemberAnnounced {user :: User, groupInfo :: GroupInfo, announcingMember :: GroupMember, unknownMember :: GroupMember, announcedMember :: GroupMember} + | CEvtGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CEvtGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} -- there is the same command response + | CEvtAcceptingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CEvtNoMemberContactCreating {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- only used in CLI + | CEvtNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} + | CEvtContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact} + | CEvtCallInvitation {callInvitation :: RcvCallInvitation} + | CEvtCallOffer {user :: User, contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool} + | CEvtCallAnswer {user :: User, contact :: Contact, answer :: WebRTCSession} + | CEvtCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo} + | CEvtCallEnded {user :: User, contact :: Contact} + | CEvtNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgAckInfo} + | CEvtRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text} + | CEvtNewRemoteHost {remoteHost :: RemoteHostInfo} + | CEvtRemoteHostConnected {remoteHost :: RemoteHostInfo} + | CEvtRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId, rhsState :: RemoteHostSessionState, rhStopReason :: RemoteHostStopReason} + | CEvtRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo, ctrlAppInfo_ :: Maybe CtrlAppInfo, appVersion :: AppVersion, compatible :: Bool} + | CEvtRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} + | CEvtRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason} + | CEvtContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption} + | CEvtContactDisabled {user :: User, contact :: Contact} + | CEvtConnectionDisabled {connectionEntity :: ConnectionEntity} + | CEvtConnectionInactive {connectionEntity :: ConnectionEntity, inactive :: Bool} + | CEvtAgentRcvQueuesDeleted {deletedRcvQueues :: NonEmpty DeletedRcvQueue} + | CEvtAgentConnsDeleted {agentConnIds :: NonEmpty AgentConnId} + | CEvtAgentUserDeleted {agentUserId :: Int64} + | CEvtMessageError {user :: User, severity :: Text, errorMessage :: Text} + | CEvtChatErrors {chatErrors :: [ChatError]} + | CEvtTimedAction {action :: String, durationMilliseconds :: Int64} + | CEvtTerminalEvent TerminalEvent deriving (Show) data TerminalEvent = TEGroupLinkRejected {user :: User, groupInfo :: GroupInfo, groupRejectionReason :: GroupRejectionReason} | TERejectingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, groupRejectionReason :: GroupRejectionReason} + | TENewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} + | TEContactVerificationReset {user :: User, contact :: Contact} + | TEGroupMemberVerificationReset {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | TEGroupEmpty {user :: User, shortGroupInfo :: ShortGroupInfo} + | TEGroupSubscribed {user :: User, shortGroupInfo :: ShortGroupInfo} + | TEGroupInvitation {user :: User, shortGroupInfo :: ShortGroupInfo} + | TEMemberSubError {user :: User, shortGroupInfo :: ShortGroupInfo, memberToSubscribe :: ShortGroupMember, chatError :: ChatError} + | TEMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]} + | TEPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]} + | TESndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError} + | TERcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} deriving (Show) data DeletedRcvQueue = DeletedRcvQueue @@ -851,50 +867,36 @@ data DeletedRcvQueue = DeletedRcvQueue } deriving (Show) --- some of these can only be used as command responses -allowRemoteEvent :: ChatResponse -> Bool +allowRemoteEvent :: ChatEvent -> Bool allowRemoteEvent = \case - CRChatStarted -> False - CRChatRunning -> False - CRChatStopped -> False - CRChatSuspended -> False - CRRemoteHostList _ -> False - CRCurrentRemoteHost _ -> False - CRRemoteHostStarted {} -> False - CRRemoteHostSessionCode {} -> False - CRNewRemoteHost _ -> False - CRRemoteHostConnected _ -> False - CRRemoteHostStopped {} -> False - CRRemoteFileStored {} -> False - CRRemoteCtrlList _ -> False - CRRemoteCtrlFound {} -> False - CRRemoteCtrlConnecting {} -> False - CRRemoteCtrlSessionCode {} -> False - CRRemoteCtrlConnected _ -> False - CRRemoteCtrlStopped {} -> False - CRSQLResult _ -> False -#if !defined(dbPostgres) - CRSlowSQLQueries {} -> False -#endif + CEvtChatSuspended -> False + CEvtRemoteHostSessionCode {} -> False + CEvtNewRemoteHost _ -> False + CEvtRemoteHostConnected _ -> False + CEvtRemoteHostStopped {} -> False + CEvtRemoteCtrlFound {} -> False + CEvtRemoteCtrlSessionCode {} -> False + CEvtRemoteCtrlStopped {} -> False _ -> True -logResponseToFile :: ChatResponse -> Bool -logResponseToFile = \case - CRContactsDisconnected {} -> True - CRContactsSubscribed {} -> True - CRContactSubError {} -> True - CRMemberSubError {} -> True - CRSndFileSubError {} -> True - CRRcvFileSubError {} -> True - CRHostConnected {} -> True - CRHostDisconnected {} -> True - CRConnectionDisabled {} -> True - CRAgentRcvQueuesDeleted {} -> True - CRAgentConnsDeleted {} -> True - CRAgentUserDeleted {} -> True - CRChatCmdError {} -> True - CRChatError {} -> True - CRMessageError {} -> True +logEventToFile :: ChatEvent -> Bool +logEventToFile = \case + CEvtContactsDisconnected {} -> True + CEvtContactsSubscribed {} -> True + CEvtContactSubError {} -> True + CEvtHostConnected {} -> True + CEvtHostDisconnected {} -> True + CEvtConnectionDisabled {} -> True + CEvtAgentRcvQueuesDeleted {} -> True + CEvtAgentConnsDeleted {} -> True + CEvtAgentUserDeleted {} -> True + -- CRChatCmdError {} -> True -- TODO this should be separately logged to file as command error + CEvtMessageError {} -> True + CEvtTerminalEvent te -> case te of + TEMemberSubError {} -> True + TESndFileSubError {} -> True + TERcvFileSubError {} -> True + _ -> False _ -> False -- (Maybe GroupMemberId) can later be changed to GroupSndScope = GSSAll | GSSAdmins | GSSMember GroupMemberId @@ -940,6 +942,7 @@ data ConnectionPlan = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan} | CPContactAddress {contactAddressPlan :: ContactAddressPlan} | CPGroupLink {groupLinkPlan :: GroupLinkPlan} + | CPError {chatError :: ChatError} deriving (Show) data InvitationLinkPlan @@ -983,6 +986,7 @@ connectionPlanProceed = \case GLPOwnLink _ -> True GLPConnectingConfirmReconnect -> True _ -> False + CPError _ -> True data ForwardConfirmation = FCFilesNotAccepted {fileIds :: [FileTransferId]} @@ -1049,7 +1053,7 @@ data SimpleNetCfg = SimpleNetCfg requiredHostMode :: Bool, smpProxyMode_ :: Maybe SMPProxyMode, smpProxyFallback_ :: Maybe SMPProxyFallback, - smpWebPort :: Bool, + smpWebPortServers :: SMPWebPortServers, tcpTimeout_ :: Maybe Int, logTLSErrors :: Bool } @@ -1064,7 +1068,7 @@ defaultSimpleNetCfg = requiredHostMode = False, smpProxyMode_ = Nothing, smpProxyFallback_ = Nothing, - smpWebPort = False, + smpWebPortServers = SWPPreset, tcpTimeout_ = Nothing, logTLSErrors = False } @@ -1125,17 +1129,30 @@ instance FromJSON ChatTagData where parseJSON invalid = JT.prependFailure "bad ChatTagData, " (JT.typeMismatch "Object" invalid) data NtfConn = NtfConn - { user_ :: Maybe User, - connEntity_ :: Maybe ConnectionEntity, + { user :: User, + agentConnId :: AgentConnId, + agentDbQueueId :: Int64, + connEntity :: ConnectionEntity, + -- Decrypted ntf meta of the expected message (the one notification was sent for). + -- Nothing means it failed to decrypt or to decode, we can still show event for entity expectedMsg_ :: Maybe NtfMsgInfo } deriving (Show) +-- msgTs is broker message timestamp, it is used in ConnMsgReq / APIGetConnNtfMessages +-- to set it as last connection message in case queue is empty data NtfMsgInfo = NtfMsgInfo {msgId :: Text, msgTs :: UTCTime} deriving (Show) -receivedMsgInfo :: SMPMsgMeta -> NtfMsgInfo -receivedMsgInfo SMPMsgMeta {msgId, msgTs} = ntfMsgInfo_ msgId msgTs +data RcvNtfMsgInfo + = RNMInfo {ntfMsgInfo :: Maybe NtfMsgInfo} + | RNMError {ntfMsgError :: AgentErrorType} + deriving (Show) + +receivedMsgInfo :: Either AgentErrorType (Maybe SMPMsgMeta) -> RcvNtfMsgInfo +receivedMsgInfo = \case + Right msgMeta_ -> RNMInfo $ (\SMPMsgMeta {msgId, msgTs} -> ntfMsgInfo_ msgId msgTs) <$> msgMeta_ + Left e -> RNMError e expectedMsgInfo :: NMsgMeta -> NtfMsgInfo expectedMsgInfo NMsgMeta {msgId, msgTs} = ntfMsgInfo_ msgId msgTs @@ -1246,8 +1263,8 @@ data ChatErrorType | CEChatNotStarted | CEChatNotStopped | CEChatStoreChanged - | CEConnectionPlan {connectionPlan :: ConnectionPlan} | CEInvalidConnReq + | CEUnsupportedConnReq | CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String} | CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)} | CEContactNotReady {contact :: Contact} @@ -1394,7 +1411,7 @@ data RemoteCtrlSession tls :: TLS, rcsSession :: RCCtrlSession, http2Server :: Async (), - remoteOutputQ :: TBQueue ChatResponse + remoteOutputQ :: TBQueue (Either ChatError ChatEvent) } data RemoteCtrlSessionState @@ -1493,23 +1510,41 @@ mkStoreError :: SomeException -> StoreError mkStoreError = SEInternalError . show {-# INLINE mkStoreError #-} -chatCmdError :: Maybe User -> String -> ChatResponse -chatCmdError user = CRChatCmdError user . ChatError . CECommandError +throwCmdError :: String -> CM a +throwCmdError = throwError . ChatError . CECommandError +{-# INLINE throwCmdError #-} + +chatCmdError :: String -> Either ChatError ChatResponse +chatCmdError = Left . ChatError . CECommandError +{-# INLINE chatCmdError #-} throwChatError :: ChatErrorType -> CM a throwChatError = throwError . ChatError +{-# INLINE throwChatError #-} toViewTE :: TerminalEvent -> CM () -toViewTE = toView . CRTerminalEvent +toViewTE = toView . CEvtTerminalEvent {-# INLINE toViewTE #-} -- | Emit local events. -toView :: ChatResponse -> CM () +toView :: ChatEvent -> CM () toView = lift . toView' {-# INLINE toView #-} -toView' :: ChatResponse -> CM' () -toView' ev = do +toView' :: ChatEvent -> CM' () +toView' = toView_ . Right +{-# INLINE toView' #-} + +eToView :: ChatError -> CM () +eToView = lift . eToView' +{-# INLINE eToView #-} + +eToView' :: ChatError -> CM' () +eToView' = toView_ . Left +{-# INLINE eToView' #-} + +toView_ :: Either ChatError ChatEvent -> CM' () +toView_ ev = do cc@ChatController {outputQ = localQ, remoteCtrlSession = session, config = ChatConfig {chatHooks}} <- ask event <- case eventHook chatHooks of Just hook -> liftIO $ hook cc ev @@ -1517,9 +1552,9 @@ toView' ev = do atomically $ readTVar session >>= \case Just (_, RCSessionConnected {remoteOutputQ}) - | allowRemoteEvent event -> writeTBQueue remoteOutputQ event + | either (const True) allowRemoteEvent event -> writeTBQueue remoteOutputQ event -- TODO potentially, it should hold some events while connecting - _ -> writeTBQueue localQ (Nothing, Nothing, event) + _ -> writeTBQueue localQ (Nothing, event) withStore' :: (DB.Connection -> IO a) -> CM a withStore' action = withStore $ liftIO . action @@ -1582,8 +1617,6 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan) -$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan) - $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType) @@ -1598,6 +1631,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan) + $(JQ.deriveJSON defaultJSON ''AppFilePathsConfig) $(JQ.deriveJSON defaultJSON ''ContactSubStatus) @@ -1614,6 +1649,8 @@ $(JQ.deriveJSON defaultJSON ''UserProfileUpdateSummary) $(JQ.deriveJSON defaultJSON ''NtfMsgInfo) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RNM") ''RcvNtfMsgInfo) + $(JQ.deriveJSON defaultJSON ''NtfConn) $(JQ.deriveJSON defaultJSON ''NtfMsgAckInfo) @@ -1648,6 +1685,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "TE") ''TerminalEvent) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CEvt") ''ChatEvent) + $(JQ.deriveFromJSON defaultJSON ''ArchiveConfig) $(JQ.deriveFromJSON defaultJSON ''DBEncryptionConfig) diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 0dbee1542e..865eb6a760 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -8,6 +8,7 @@ module Simplex.Chat.Core runSimplexChat, sendChatCmdStr, sendChatCmd, + printResponseEvent, ) where @@ -23,9 +24,10 @@ import Simplex.Chat import Simplex.Chat.Controller import Simplex.Chat.Library.Commands import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) +import Simplex.Chat.Remote.Types (RemoteHostId) import Simplex.Chat.Store.Profiles import Simplex.Chat.Types -import Simplex.Chat.View (serializeChatResponse) +import Simplex.Chat.View (ChatResponseEvent, serializeChatError, serializeChatResponse) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..)) import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction) import System.Exit (exitFailure) @@ -34,7 +36,7 @@ import Text.Read (readMaybe) import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations}} chat = +simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations}, maintenance} chat = case logAgent of Just level -> do setLogLevel level @@ -48,7 +50,8 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {core exitFailure run db@ChatDatabase {chatStore} = do u_ <- getSelectActiveUser chatStore - cc <- newChatController db u_ cfg opts False + let backgroundMode = not maintenance + cc <- newChatController db u_ cfg opts backgroundMode u <- maybe (createActiveUser cc) pure u_ unless testView $ putStrLn $ "Current user: " <> userStr u runSimplexChat opts u cc chat @@ -61,10 +64,10 @@ runSimplexChat ChatOpts {maintenance} u cc chat a2 <- async $ chat u cc waitEither_ a1 a2 -sendChatCmdStr :: ChatController -> String -> IO ChatResponse +sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse) sendChatCmdStr cc s = runReaderT (execChatCommand Nothing . encodeUtf8 $ T.pack s) cc -sendChatCmd :: ChatController -> ChatCommand -> IO ChatResponse +sendChatCmd :: ChatController -> ChatCommand -> IO (Either ChatError ChatResponse) sendChatCmd cc cmd = runReaderT (execChatCommand' cmd) cc getSelectActiveUser :: DBStore -> IO (Maybe User) @@ -106,12 +109,17 @@ createActiveUser cc = do displayName <- T.pack <$> getWithPrompt "display name" let profile = Just Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) `runReaderT` cc >>= \case - CRActiveUser user -> pure user - r -> do - ts <- getCurrentTime - tz <- getCurrentTimeZone - putStrLn $ serializeChatResponse (Nothing, Nothing) ts tz Nothing r - loop + Right (CRActiveUser user) -> pure user + r -> printResponseEvent (Nothing, Nothing) (config cc) r >> loop + +printResponseEvent :: ChatResponseEvent r => (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Either ChatError r -> IO () +printResponseEvent hu cfg = \case + Right r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + putStrLn $ serializeChatResponse hu cfg ts tz (fst hu) r + Left e -> do + putStrLn $ serializeChatError True cfg e getWithPrompt :: String -> IO String getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 821bc3d6ce..8c475b111b 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -37,12 +37,13 @@ import Data.Either (fromRight, partitionEithers, rights) import Data.Foldable (foldr') import Data.Functor (($>)) import Data.Int (Int64) -import Data.List (find, foldl', isSuffixOf, partition, sortOn, zipWith4) +import Data.List (dropWhileEnd, find, foldl', isSuffixOf, partition, sortOn, zipWith4) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) +import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) @@ -90,7 +91,7 @@ import Simplex.Messaging.Agent.Store.Interface (execSQL) import Simplex.Messaging.Agent.Store.Shared (upMigration) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations) -import Simplex.Messaging.Client (NetworkConfig (..), SocksMode (SMAlways), textToHostMode) +import Simplex.Messaging.Client (NetworkConfig (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -224,8 +225,8 @@ startReceiveUserFiles :: User -> CM () startReceiveUserFiles user = do filesToReceive <- withStore' (`getRcvFilesToReceive` user) forM_ filesToReceive $ \ft -> - flip catchChatError (toView . CRChatError (Just user)) $ - toView =<< receiveFile' user ft False Nothing Nothing + flip catchChatError eToView $ + toView =<< receiveFileEvt' user ft False Nothing Nothing restoreCalls :: CM' () restoreCalls = do @@ -254,11 +255,11 @@ stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, atomically $ writeTVar files M.empty updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig -updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = +updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPortServers, tcpTimeout_, logTLSErrors} = let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_ cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_ cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_ - in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, smpWebPort, logTLSErrors} + in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, smpWebPortServers, logTLSErrors} useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)) useServers as opDomains uss = @@ -266,32 +267,28 @@ useServers as opDomains uss = xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss in (smp', xftp') -execChatCommand :: Maybe RemoteHostId -> ByteString -> CM' ChatResponse -execChatCommand rh s = do - u <- readTVarIO =<< asks currentUser +execChatCommand :: Maybe RemoteHostId -> ByteString -> CM' (Either ChatError ChatResponse) +execChatCommand rh s = case parseChatCommand s of - Left e -> pure $ chatCmdError u e + Left e -> pure $ chatCmdError e Right cmd -> case rh of Just rhId - | allowRemoteCommand cmd -> execRemoteCommand u rhId cmd s - | otherwise -> pure $ CRChatCmdError u $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand + | allowRemoteCommand cmd -> execRemoteCommand rhId cmd s + | otherwise -> pure $ Left $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand _ -> do cc@ChatController {config = ChatConfig {chatHooks}} <- ask case preCmdHook chatHooks of - Just hook -> liftIO (hook cc cmd) >>= either pure (execChatCommand_ u) - Nothing -> execChatCommand_ u cmd + Just hook -> liftIO (hook cc cmd) >>= either pure execChatCommand' + Nothing -> execChatCommand' cmd -execChatCommand' :: ChatCommand -> CM' ChatResponse -execChatCommand' cmd = asks currentUser >>= readTVarIO >>= (`execChatCommand_` cmd) +execChatCommand' :: ChatCommand -> CM' (Either ChatError ChatResponse) +execChatCommand' cmd = handleCommandError $ processChatCommand cmd -execChatCommand_ :: Maybe User -> ChatCommand -> CM' ChatResponse -execChatCommand_ u cmd = handleCommandError u $ processChatCommand cmd +execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> CM' (Either ChatError ChatResponse) +execRemoteCommand rhId cmd s = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s -execRemoteCommand :: Maybe User -> RemoteHostId -> ChatCommand -> ByteString -> CM' ChatResponse -execRemoteCommand u rhId cmd s = handleCommandError u $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s - -handleCommandError :: Maybe User -> CM ChatResponse -> CM' ChatResponse -handleCommandError u a = either (CRChatCmdError u) id <$> (runExceptT a `E.catches` ioErrors) +handleCommandError :: CM ChatResponse -> CM' (Either ChatError ChatResponse) +handleCommandError a = runExceptT a `E.catches` ioErrors where ioErrors = [ E.Handler $ \(e :: ExitCode) -> E.throwIO e, @@ -501,7 +498,7 @@ processChatCommand' vr = \case pure $ CRChatTags user tags APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) - unless (null errs) $ toView $ CRChatErrors (Just user) (map ChatErrorStore errs) + unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs) pure $ CRApiChats user previews APIGetChat (ChatRef cType cId) contentFilter pagination search -> withUser $ \user -> case cType of -- TODO optimize queries calculating ChatStats, currently they're disabled @@ -516,8 +513,8 @@ processChatCommand' vr = \case when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported" (localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search) pure $ CRApiChat user (AChat SCTLocal localChat) navInfo - CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + CTContactRequest -> throwCmdError "not implemented" + CTContactConnection -> throwCmdError "not supported" APIGetChatItems pagination search -> withUser $ \user -> do chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search pure $ CRChatItems user Nothing chatItems @@ -552,14 +549,14 @@ processChatCommand' vr = \case APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do _ <- createChatTag db user emoji text CRChatTags user <$> getUserChatTags db user - APISetChatTags (ChatRef cType chatId) tagIds -> withUser $ \user -> withFastStore' $ \db -> case cType of - CTDirect -> do + APISetChatTags (ChatRef cType chatId) tagIds -> withUser $ \user -> case cType of + CTDirect -> withFastStore' $ \db -> do updateDirectChatTags db chatId (maybe [] L.toList tagIds) CRTagsUpdated user <$> getUserChatTags db user <*> getDirectChatTags db chatId - CTGroup -> do + CTGroup -> withFastStore' $ \db -> do updateGroupChatTags db chatId (maybe [] L.toList tagIds) CRTagsUpdated user <$> getUserChatTags db user <*> getGroupChatTags db chatId - _ -> pure $ chatCmdError (Just user) "not supported" + _ -> throwCmdError "not supported" APIDeleteChatTag tagId -> withUser $ \user -> do withFastStore' $ \db -> deleteChatTag db user tagId ok user @@ -621,7 +618,7 @@ processChatCommand' vr = \case assertUserGroupRole gInfo GRAuthor let (_, ft_) = msgContentTexts mc if prohibitedSimplexLinks gInfo membership ft_ - then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) + then throwCmdError ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) else do cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId case cci of @@ -659,14 +656,14 @@ processChatCommand' vr = \case ci' <- updateLocalChatItem' db user noteFolderId ci (CISndMsgContent mc) True pure $ CRChatItemUpdated user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci') _ -> throwChatError CEInvalidChatItemUpdate - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of CTDirect -> withContactLock "deleteChatItem" chatId $ do (ct, items) <- getCommandDirectChatItems user chatId itemIds - case mode of - CIDMInternal -> deleteDirectCIs user ct items True False - CIDMInternalMark -> markDirectCIsDeleted user ct items True =<< liftIO getCurrentTime + deletions <- case mode of + CIDMInternal -> deleteDirectCIs user ct items + CIDMInternalMark -> markDirectCIsDeleted user ct items =<< liftIO getCurrentTime CIDMBroadcast -> do assertDeletable items assertDirectAllowed user MDSnd ct XMsgDel_ @@ -675,13 +672,14 @@ processChatCommand' vr = \case forM_ (L.nonEmpty events) $ \events' -> sendDirectContactMessages user ct events' if featureAllowed SCFFullDelete forUser ct - then deleteDirectCIs user ct items True False - else markDirectCIsDeleted user ct items True =<< liftIO getCurrentTime + then deleteDirectCIs user ct items + else markDirectCIsDeleted user ct items =<< liftIO getCurrentTime + pure $ CRChatItemsDeleted user deletions True False CTGroup -> withGroupLock "deleteChatItem" chatId $ do (gInfo, items) <- getCommandGroupChatItems user chatId itemIds - case mode of - CIDMInternal -> deleteGroupCIs user gInfo items True False Nothing =<< liftIO getCurrentTime - CIDMInternalMark -> markGroupCIsDeleted user gInfo items True Nothing =<< liftIO getCurrentTime + deletions <- case mode of + CIDMInternal -> deleteGroupCIs user gInfo items Nothing =<< liftIO getCurrentTime + CIDMInternalMark -> markGroupCIsDeleted user gInfo items Nothing =<< liftIO getCurrentTime CIDMBroadcast -> do ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo assertDeletable items @@ -691,11 +689,12 @@ processChatCommand' vr = \case -- TODO [knocking] validate: only current members or only single pending approval member mapM_ (sendGroupMessages user gInfo ms) events delGroupChatItems user gInfo items False + pure $ CRChatItemsDeleted user deletions True False CTLocal -> do (nf, items) <- getCommandLocalChatItems user chatId itemIds deleteLocalCIs user nf items True False - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" where assertDeletable :: forall c. ChatTypeI c => [CChatItem c] -> CM () assertDeletable items = do @@ -713,7 +712,8 @@ processChatCommand' vr = \case APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do (gInfo, items) <- getCommandGroupChatItems user gId itemIds ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo - delGroupChatItemsForMembers user gInfo ms items + deletions <- delGroupChatItemsForMembers user gInfo ms items + pure $ CRChatItemsDeleted user deletions True False APIArchiveReceivedReports gId -> withUser $ \user -> withFastStore $ \db -> do g <- getGroupInfo db vr user gId deleteTs <- liftIO getCurrentTime @@ -722,12 +722,13 @@ processChatCommand' vr = \case APIDeleteReceivedReports gId itemIds mode -> withUser $ \user -> withGroupLock "deleteReports" gId $ do (gInfo, items) <- getCommandGroupChatItems user gId itemIds unless (all isRcvReport items) $ throwChatError $ CECommandError "some items are not received reports" - case mode of - CIDMInternal -> deleteGroupCIs user gInfo items True False Nothing =<< liftIO getCurrentTime - CIDMInternalMark -> markGroupCIsDeleted user gInfo items True Nothing =<< liftIO getCurrentTime + deletions <- case mode of + CIDMInternal -> deleteGroupCIs user gInfo items Nothing =<< liftIO getCurrentTime + CIDMInternalMark -> markGroupCIsDeleted user gInfo items Nothing =<< liftIO getCurrentTime CIDMBroadcast -> do ms <- withFastStore' $ \db -> getGroupModerators db vr user gInfo delGroupChatItemsForMembers user gInfo ms items + pure $ CRChatItemsDeleted user deletions True False where isRcvReport = \case CChatItem _ ChatItem {content = CIRcvMsgContent (MCReport {})} -> True @@ -776,9 +777,9 @@ processChatCommand' vr = \case r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction pure $ CRChatItemReaction user add r _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" - CTLocal -> pure $ chatCmdError (Just user) "not supported" - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + CTLocal -> throwCmdError "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" where checkReactionAllowed rs = do when ((reaction `elem` rs) == add) $ @@ -794,8 +795,8 @@ processChatCommand' vr = \case CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds CTLocal -> planForward user . snd =<< getCommandLocalChatItems user fromChatId itemIds - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" where planForward :: User -> [CChatItem c] -> CM ChatResponse planForward user items = do @@ -858,8 +859,8 @@ processChatCommand' vr = \case Just cmrs' -> createNoteFolderContentItems user toChatId cmrs' Nothing -> pure $ CRNewChatItems user [] - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" where prepareForward :: User -> CM [ComposedMessageReq] prepareForward user = case fromCType of @@ -999,8 +1000,8 @@ processChatCommand' vr = \case user <- withFastStore $ \db -> getUserByNoteFolderId db chatId withFastStore' $ \db -> updateLocalChatItemsRead db user chatId ok user - CTContactRequest -> pure $ chatCmdError Nothing "not supported" - CTContactConnection -> pure $ chatCmdError Nothing "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" APIChatItemsRead chatRef@(ChatRef cType chatId) itemIds -> withUser $ \_ -> case cType of CTDirect -> do user <- withFastStore $ \db -> getUserByContactId db chatId @@ -1016,9 +1017,9 @@ processChatCommand' vr = \case setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt ok user - CTLocal -> pure $ chatCmdError Nothing "not supported" - CTContactRequest -> pure $ chatCmdError Nothing "not supported" - CTContactConnection -> pure $ chatCmdError Nothing "not supported" + CTLocal -> throwCmdError "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of CTDirect -> do withFastStore $ \db -> do @@ -1035,7 +1036,7 @@ processChatCommand' vr = \case nf <- getNoteFolder db user chatId liftIO $ updateNoteFolderUnreadChat db user nf unreadChat ok user - _ -> pure $ chatCmdError (Just user) "not supported" + _ -> throwCmdError "not supported" APIDeleteChat cRef@(ChatRef cType chatId) cdm -> withUser $ \user@User {userId} -> case cType of CTDirect -> do ct <- withFastStore $ \db -> getContact db vr user chatId @@ -1043,8 +1044,7 @@ processChatCommand' vr = \case withContactLock "deleteChat direct" chatId . procCmd $ case cdm of CDMFull notify -> do - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo + deleteCIFiles user filesInfo sendDelDeleteConns ct notify -- functions below are called in separate transactions to prevent crashes on android -- (possibly, race condition on integrity check?) @@ -1070,10 +1070,10 @@ processChatCommand' vr = \case let doSendDel = contactReady ct && contactActive ct && notify when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct) - deleteAgentConnectionsAsync' user contactConnIds doSendDel + deleteAgentConnectionsAsync' contactConnIds doSendDel CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId - deleteAgentConnectionAsync user acId + deleteAgentConnectionAsync acId withFastStore' $ \db -> deletePendingContactConnection db userId chatId pure $ CRContactConnectionDeleted user conn CTGroup -> do @@ -1084,8 +1084,7 @@ processChatCommand' vr = \case unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "deleteChat group" chatId . procCmd $ do - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo + deleteCIFiles user filesInfo let doSendDel = memberActive membership && isOwner -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) when doSendDel . void $ sendGroupMessage' user gInfo members XGrpDel @@ -1097,21 +1096,19 @@ processChatCommand' vr = \case withFastStore' $ \db -> deleteGroupMembers db user gInfo withFastStore' $ \db -> deleteGroup db user gInfo pure $ CRGroupDeletedUser user gInfo - CTLocal -> pure $ chatCmdError (Just user) "not supported" - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" + CTLocal -> throwCmdError "not supported" + CTContactRequest -> throwCmdError "not supported" APIClearChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of CTDirect -> do ct <- withFastStore $ \db -> getContact db vr user chatId filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo + deleteCIFiles user filesInfo withFastStore' $ \db -> deleteContactCIs db user ct pure $ CRChatCleared user (AChatInfo SCTDirect $ DirectChat ct) CTGroup -> do gInfo <- withFastStore $ \db -> getGroupInfo db vr user chatId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo + deleteCIFiles user filesInfo withFastStore' $ \db -> deleteGroupChatItemsMessages db user gInfo membersToDelete <- withFastStore' $ \db -> getGroupMembersForExpiration db vr user gInfo forM_ membersToDelete $ \m -> withFastStore' $ \db -> deleteGroupMember db user m @@ -1123,8 +1120,8 @@ processChatCommand' vr = \case withFastStore' $ \db -> deleteNoteFolderFiles db userId nf withFastStore' $ \db -> deleteNoteFolderCIs db user nf pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" + CTContactConnection -> throwCmdError "not supported" + CTContactRequest -> throwCmdError "not supported" APIAcceptContact incognito connReqId -> withUser $ \_ -> do userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId withUserContactLock "acceptContact" userContactLinkId $ do @@ -1169,9 +1166,9 @@ processChatCommand' vr = \case let call' = Call {contactId, callId, callUUID, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} call_ <- atomically $ TM.lookupInsert contactId call' calls forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing - toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] ok user - else pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)) + else throwCmdError ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)) SendCallInvitation cName callType -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName processChatCommand $ APISendCallInvitation contactId callType @@ -1285,7 +1282,7 @@ processChatCommand' vr = \case g <- getGroupInfo db vr user chatId liftIO $ setGroupUIThemes db user g uiThemes ok user - _ -> pure $ chatCmdError (Just user) "not supported" + _ -> throwCmdError "not supported" APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken APIRegisterToken token mode -> withUser $ \_ -> CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode) @@ -1293,30 +1290,21 @@ processChatCommand' vr = \case APICheckToken token -> withUser $ \_ -> CRNtfTokenStatus <$> withAgent (`checkNtfToken` token) APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_ - APIGetNtfConns nonce encNtfInfo -> withUser $ \user -> do + APIGetNtfConns nonce encNtfInfo -> withUser $ \_ -> do ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo (errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos)) - unless (null errs) $ toView $ CRChatErrors (Just user) errs - pure $ CRNtfConns ntfMsgs + unless (null errs) $ toView $ CEvtChatErrors errs + pure $ CRNtfConns $ catMaybes ntfMsgs where - getMsgConn :: DB.Connection -> NotificationInfo -> IO NtfConn - getMsgConn db NotificationInfo {ntfConnId, ntfMsgMeta = nMsgMeta} = do + getMsgConn :: DB.Connection -> NotificationInfo -> IO (Maybe NtfConn) + getMsgConn db NotificationInfo {ntfConnId, ntfDbQueueId, ntfMsgMeta = nMsgMeta} = do let agentConnId = AgentConnId ntfConnId - user_ <- getUserByAConnId db agentConnId - connEntity_ <- - pure user_ $>>= \user -> - eitherToMaybe <$> runExceptT (getConnectionEntity db vr user agentConnId) - pure $ - NtfConn - { user_, - connEntity_, - -- Decrypted ntf meta of the expected message (the one notification was sent for) - expectedMsg_ = expectedMsgInfo <$> nMsgMeta - } - ApiGetConnNtfMessages connIds -> withUser $ \_ -> do - let acIds = L.map (\(AgentConnId acId) -> acId) connIds - msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds - let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs + mkNtfConn user connEntity = NtfConn {user, agentConnId, agentDbQueueId = ntfDbQueueId, connEntity, expectedMsg_ = expectedMsgInfo <$> nMsgMeta} + getUserByAConnId db agentConnId + $>>= \user -> fmap (mkNtfConn user) . eitherToMaybe <$> runExceptT (getConnectionEntity db vr user agentConnId) + APIGetConnNtfMessages connMsgs -> withUser $ \_ -> do + msgs <- lift $ withAgent' (`getConnectionMessages` connMsgs) + let ntfMsgs = L.map receivedMsgInfo msgs pure $ CRConnNtfMessages ntfMsgs GetUserProtoServers (AProtocolType p) -> withUser $ \user -> withServerProtocol p $ do srvs <- withFastStore (`getUserServers` user) @@ -1416,7 +1404,7 @@ processChatCommand' vr = \case oldTTL = fromMaybe globalTTL oldTTL_ when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do lift $ setExpireCIFlag user False - expireChat user globalTTL `catchChatError` (toView . CRChatError (Just user)) + expireChat user globalTTL `catchChatError` eToView lift $ setChatItemsExpiration user globalTTL ttlCount ok user where @@ -1486,15 +1474,15 @@ processChatCommand' vr = \case liftIO $ updateGroupSettings db user chatId chatSettings pure ms forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId -> - withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchChatError` (toView . CRChatError (Just user)) + withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchChatError` eToView ok user - _ -> pure $ chatCmdError (Just user) "not supported" + _ -> throwCmdError "not supported" APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do m <- withFastStore $ \db -> do liftIO $ updateGroupMemberSettings db user gId gMemberId settings getGroupMember db vr user gId gMemberId let ntfOn = showMessages $ memberSettings m - toggleNtf user m ntfOn + toggleNtf m ntfOn ok user APIContactInfo contactId -> withUser $ \user@User {userId} -> do -- [incognito] print user's incognito profile for this contact @@ -1650,16 +1638,18 @@ processChatCommand' vr = \case EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome - APIAddContact userId incognito -> withUserId userId $ \user -> procCmd $ do + APIAddContact userId short incognito -> withUserId userId $ \user -> procCmd $ do -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOn subMode + let userData = shortLinkUserData short + (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation userData Nothing IKPQOn subMode + ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range - conn <- withFastStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode initialChatVersion PQSupportOn - pure $ CRInvitation user cReq conn - AddContact incognito -> withUser $ \User {userId} -> - processChatCommand $ APIAddContact userId incognito + conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' ConnNew incognitoProfile subMode initialChatVersion PQSupportOn + pure $ CRInvitation user ccLink' conn + AddContact short incognito -> withUser $ \User {userId} -> + processChatCommand $ APIAddContact userId short incognito APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do conn'_ <- withFastStore $ \db -> do conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId @@ -1677,42 +1667,29 @@ processChatCommand' vr = \case Nothing -> throwChatError CEConnectionIncognitoChangeProhibited APIChangeConnectionUser connId newUserId -> withUser $ \user@User {userId} -> do conn <- withFastStore $ \db -> getPendingContactConnection db userId connId - let PendingContactConnection {pccConnStatus, connReqInv} = conn - case (pccConnStatus, connReqInv) of - (ConnNew, Just cReqInv) -> do + let PendingContactConnection {pccConnStatus, connLinkInv} = conn + case (pccConnStatus, connLinkInv) of + (ConnNew, Just (CCLink cReqInv _)) -> do newUser <- privateGetUser newUserId - conn' <- ifM (canKeepLink cReqInv newUser) (updateConnRecord user conn newUser) (recreateConn user conn newUser) + conn' <- recreateConn user conn newUser pure $ CRConnectionUserChanged user conn conn' newUser _ -> throwChatError CEConnectionUserChangeProhibited where - canKeepLink :: ConnReqInvitation -> User -> CM Bool - canKeepLink (CRInvitationUri crData _) newUser = do - let ConnReqUriData {crSmpQueues = q :| _} = crData - SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - newUserServers <- - map protoServer' . L.filter (\ServerCfg {enabled} -> enabled) - <$> getKnownAgentServers SPSMP newUser - pure $ smpServer `elem` newUserServers - updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do - withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) - withFastStore' $ \db -> do - conn' <- updatePCCUser db userId conn newUserId - forM_ customUserProfileId $ \profileId -> - deletePCCIncognitoProfile db user profileId - pure conn' - recreateConn user conn@PendingContactConnection {customUserProfileId} newUser = do + recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do subMode <- chatReadVar subscriptionMode - (agConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation Nothing IKPQOn subMode + let userData = shortLinkUserData $ isJust $ connShortLink =<< connLinkInv + (agConnId, ccLink) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData Nothing IKPQOn subMode + ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId forM_ customUserProfileId $ \profileId -> deletePCCIncognitoProfile db user profileId - createDirectConnection db newUser agConnId cReq ConnNew Nothing subMode initialChatVersion PQSupportOn - deleteAgentConnectionAsync user (aConnId' conn) + createDirectConnection db newUser agConnId ccLink' ConnNew Nothing subMode initialChatVersion PQSupportOn + deleteAgentConnectionAsync (aConnId' conn) pure conn' - APIConnectPlan userId cReqUri -> withUserId userId $ \user -> - CRConnectionPlan user <$> connectPlan user cReqUri - APIConnect userId incognito (Just (ACR SCMInvitation cReq@(CRInvitationUri crData e2e))) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do + APIConnectPlan userId cLink -> withUserId userId $ \user -> + uncurry (CRConnectionPlan user) <$> connectPlan user cLink + APIConnect userId incognito (Just (ACCL SCMInvitation (CCLink cReq@(CRInvitationUri crData e2e) sLnk_))) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing @@ -1735,7 +1712,8 @@ processChatCommand' vr = \case where joinNewConn chatV dm = do connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' - pcc <- withFastStore' $ \db -> createDirectConnection db user connId cReq ConnPrepared (incognitoProfile $> profileToSend) subMode chatV pqSup' + let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ + pcc <- withFastStore' $ \db -> createDirectConnection db user connId ccLink ConnPrepared (incognitoProfile $> profileToSend) subMode chatV pqSup' joinPreparedConn connId pcc dm joinPreparedConn connId pcc@PendingContactConnection {pccConnId} dm = do void $ withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode @@ -1745,47 +1723,44 @@ processChatCommand' vr = \case ( CRInvitationUri crData {crScheme = SSSimplex} e2e, CRInvitationUri crData {crScheme = simplexChat} e2e ) - APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq + APIConnect userId incognito (Just (ACCL SCMContact ccLink)) -> withUserId userId $ \user -> connectViaContact user incognito ccLink APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq - Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do - plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) - unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) - case plan of - CPContactAddress (CAPContactViaAddress Contact {contactId}) -> - processChatCommand $ APIConnectContactViaAddress userId incognito contactId - _ -> processChatCommand $ APIConnect userId incognito aCReqUri + Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do + (ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink ILPOk); _ -> throwError e + connectWithPlan user incognito ccLink plan Connect _ Nothing -> throwChatError CEInvalidConnReq APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId when (isJust activeConn) $ throwChatError (CECommandError "contact already has connection") - case contactLink of - Just cReq -> connectContactViaAddress user incognito ct cReq + ccLink <- case contactLink of + Just (CLFull cReq) -> pure $ CCLink cReq Nothing + Just (CLShort sLnk) -> do + cReq <- getShortLinkConnReq user sLnk + pure $ CCLink cReq $ Just sLnk Nothing -> throwChatError (CECommandError "no address in contact profile") - ConnectSimplex incognito -> withUser $ \user@User {userId} -> do - let cReqUri = ACR SCMContact adminContactReq - plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) - unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) - case plan of - CPContactAddress (CAPContactViaAddress Contact {contactId}) -> - processChatCommand $ APIConnectContactViaAddress userId incognito contactId - _ -> processChatCommand $ APIConnect userId incognito (Just cReqUri) + connectContactViaAddress user incognito ct ccLink + ConnectSimplex incognito -> withUser $ \user -> do + plan <- contactRequestPlan user adminContactReq `catchChatError` const (pure $ CPContactAddress CAPOk) + connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) cdm ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect APIListContacts userId -> withUserId userId $ \user -> CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user) ListContacts -> withUser $ \User {userId} -> processChatCommand $ APIListContacts userId - APICreateMyAddress userId -> withUserId userId $ \user -> procCmd $ do + APICreateMyAddress userId short -> withUserId userId $ \user -> procCmd $ do subMode <- chatReadVar subscriptionMode - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing IKPQOn subMode - withFastStore $ \db -> createUserContactLink db user connId cReq subMode - pure $ CRUserContactLinkCreated user cReq - CreateMyAddress -> withUser $ \User {userId} -> - processChatCommand $ APICreateMyAddress userId + let userData = shortLinkUserData short + (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode + ccLink' <- shortenCreatedLink ccLink + withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode + pure $ CRUserContactLinkCreated user ccLink' + CreateMyAddress short -> withUser $ \User {userId} -> + processChatCommand $ APICreateMyAddress userId short APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do conns <- withFastStore $ \db -> getUserAddressConnections db vr user withChatLock "deleteMyAddress" $ do - deleteAgentConnectionsAsync user $ map aConnId conns + deleteAgentConnectionsAsync $ map aConnId conns withFastStore' (`deleteUserAddress` user) let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} r <- updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing @@ -1803,8 +1778,9 @@ processChatCommand' vr = \case let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing APISetProfileAddress userId True -> withUserId userId $ \user@User {profile = p} -> do - ucl@UserContactLink {connReqContact} <- withFastStore (`getUserAddress` user) - let p' = (fromLocalProfile p :: Profile) {contactLink = Just connReqContact} + ucl@UserContactLink {connLinkContact = CCLink cReq _} <- withFastStore (`getUserAddress` user) + -- TODO [short links] replace with short links + let p' = (fromLocalProfile p :: Profile) {contactLink = Just $ CLFull cReq} updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl SetProfileAddress onOff -> withUser $ \User {userId} -> processChatCommand $ APISetProfileAddress userId onOff @@ -1874,10 +1850,10 @@ processChatCommand' vr = \case Nothing -> do g <- withFastStore $ \db -> getGroupInfo db vr user gId unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" - toView $ CRNoMemberContactCreating user g m + toView $ CEvtNoMemberContactCreating user g m processChatCommand (APICreateMemberContact gId mId) >>= \case - cr@(CRNewMemberContact _ Contact {contactId} _ _) -> do - toView cr + CRNewMemberContact _ ct@Contact {contactId} _ _ -> do + toViewTE $ TENewMemberContact user ct g m processChatCommand $ APISendMemberContactInvitation contactId (Just mc) cr -> pure cr Just ctId -> do @@ -1888,7 +1864,7 @@ processChatCommand' vr = \case withSendRef chatRef $ \sendRef -> do let mc = MCText msg processChatCommand $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions] - SendMessageBroadcast msg -> withUser $ \user -> do + SendMessageBroadcast mc -> withUser $ \user -> do contacts <- withFastStore' $ \db -> getUserContacts db vr user withChatLock "sendMessageBroadcast" . procCmd $ do let ctConns_ = L.nonEmpty $ foldr addContactConn [] contacts @@ -1912,7 +1888,6 @@ processChatCommand' vr = \case lift . void $ withStoreBatch' $ \db -> map (createCI db user timestamp) ctSndMsgs pure CRBroadcastSent {user, msgContent = mc, successes = length ctSndMsgs, failures = length errs, timestamp} where - mc = MCText msg addContactConn :: Contact -> [(Contact, Connection)] -> [(Contact, Connection)] addContactConn ct ctConns = case contactSendConn_ ct of Right conn | directOrUsed ct -> (ct, conn) : ctConns @@ -1982,7 +1957,7 @@ processChatCommand' vr = \case Nothing -> do gVar <- asks random subMode <- chatReadVar subscriptionMode - (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOff subMode + (agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member @@ -2025,7 +2000,7 @@ processChatCommand' vr = \case updateGroupMemberStatus db userId fromMember GSMemInvited updateGroupMemberStatus db userId membership GSMemInvited throwError e - updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` (toView . CRChatError (Just user)) + updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` eToView pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct APIAcceptMember groupId gmId role -> withUser $ \user -> do @@ -2052,9 +2027,9 @@ processChatCommand' vr = \case assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole]) (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems (errs2, changed2, acis) <- changeRoleCurrentMems user g currentMems - unless (null acis) $ toView $ CRNewChatItems user acis + unless (null acis) $ toView $ CEvtNewChatItems user acis let errs = errs1 <> errs2 - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole} -- same order is not guaranteed where selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds @@ -2140,11 +2115,11 @@ processChatCommand' vr = \case cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch" let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ - unless (null acis) $ toView $ CRNewChatItems user acis + unless (null acis) $ toView $ CEvtNewChatItems user acis (errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs) blockMems) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs -- TODO not batched - requires agent batch api - forM_ blocked $ \m -> toggleNtf user m (not blockFlag) + forM_ blocked $ \m -> toggleNtf m (not blockFlag) pure CRMembersBlockedForAllUser {user, groupInfo = gInfo, members = blocked, blocked = blockFlag} where sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c @@ -2152,36 +2127,38 @@ processChatCommand' vr = \case let content = CISndGroupEvent $ SGEMemberBlocked groupMemberId (fromLocalProfile memberProfile) blockFlag ts = ciContentTexts content in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing - APIRemoveMembers groupId memberIds -> withUser $ \user -> + APIRemoveMembers {groupId, groupMemberIds, withMessages} -> withUser $ \user -> withGroupLock "removeMembers" groupId . procCmd $ do Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId - let (invitedMems, pendingMems, currentMems, maxRole, anyAdmin) = selectMembers members - when (length invitedMems + length pendingMems + length currentMems /= length memberIds) $ throwChatError CEGroupMemberNotFound - when (length memberIds > 1 && anyAdmin) $ throwChatError $ CECommandError "can't remove multiple members when admins selected" + let (count, invitedMems, pendingMems, currentMems, maxRole, anyAdmin) = selectMembers members + memCount = S.size groupMemberIds + when (count /= memCount) $ throwChatError CEGroupMemberNotFound + when (memCount > 1 && anyAdmin) $ throwChatError $ CECommandError "can't remove multiple members when admins selected" assertUserGroupRole gInfo $ max GRAdmin maxRole (errs1, deleted1) <- deleteInvitedMems user invitedMems (errs2, deleted2, acis2) <- deleteMemsSend user gInfo members currentMems - rs <- forM pendingMems $ \m -> deleteMemsSend user gInfo [m] [m] + rs <- forM pendingMems $ \m -> deleteMemsSend user gInfo [m] [m] -- TODO [knocking] let (errs3, deleted3, acis3) = concatTuples rs acis = acis2 <> acis3 errs = errs1 <> errs2 <> errs3 - unless (null acis) $ toView $ CRNewChatItems user acis - unless (null errs) $ toView $ CRChatErrors (Just user) errs - pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2 <> deleted3) -- same order is not guaranteed + unless (null acis) $ toView $ CEvtNewChatItems user acis + unless (null errs) $ toView $ CEvtChatErrors errs + when withMessages $ deleteMessages user gInfo $ currentMems <> pendingMems + pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2 <> deleted3) withMessages -- same order is not guaranteed where - selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) - selectMembers = foldr' addMember ([], [], [], GRObserver, False) + selectMembers :: [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) + selectMembers = foldl' addMember (0, [], [], [], GRObserver, False) where - addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, pending, current, maxRole, anyAdmin) - | groupMemberId `elem` memberIds = + addMember acc@(n, invited, pending, current, maxRole, anyAdmin) m@GroupMember {groupMemberId, memberStatus, memberRole} + | groupMemberId `S.member` groupMemberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin - in - case memberStatus of - GSMemInvited -> (m : invited, pending, current, maxRole', anyAdmin') - GSMemPendingApproval -> (invited, m : pending, current, maxRole', anyAdmin') - _ -> (invited, pending, m : current, maxRole', anyAdmin') - | otherwise = (invited, pending, current, maxRole, anyAdmin) + n' = n + 1 + in case memberStatus of + GSMemInvited -> (n', m : invited, pending, current, maxRole', anyAdmin') + GSMemPendingApproval -> (n', invited, m : pending, current, maxRole', anyAdmin') + _ -> (n', invited, pending, m : current, maxRole', anyAdmin') + | otherwise = acc deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember]) deleteInvitedMems user memsToDelete = do deleteMembersConnections user memsToDelete @@ -2194,7 +2171,7 @@ processChatCommand' vr = \case deleteMemsSend user gInfo sendToMems memsToDelete = case L.nonEmpty memsToDelete of Nothing -> pure ([], [], []) Just memsToDelete' -> do - let events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId) memsToDelete' + let events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages) memsToDelete' (msgs_, _gsr) <- sendGroupMessages user gInfo sendToMems events let itemsData = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_) cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False @@ -2212,6 +2189,9 @@ processChatCommand' vr = \case delMember db m = do deleteOrUpdateMemberRecordIO db user m pure m {memberStatus = GSMemRemoved} + deleteMessages user gInfo@GroupInfo {membership} ms + | groupFeatureMemberAllowed SGFFullDelete membership gInfo = deleteGroupMembersCIs user gInfo ms membership + | otherwise = markGroupMembersCIsDeleted user gInfo ms membership concatTuples :: [([a], [b], [c])] -> ([a], [b], [c]) concatTuples xs = (concat as, concat bs, concat cs) where (as, bs, cs) = unzip3 xs @@ -2223,7 +2203,7 @@ processChatCommand' vr = \case -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) msg <- sendGroupMessage' user gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) - toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] -- TODO delete direct connections that were unused deleteGroupLinkIfExists user gInfo -- member records are not deleted to keep history @@ -2240,12 +2220,12 @@ processChatCommand' vr = \case processChatCommand $ APIJoinGroup groupId enableNtfs MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMembersRole gId [gMemberId] memRole BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked - RemoveMembers gName gMemberNames -> withUser $ \user -> do + RemoveMembers gName gMemberNames withMessages -> withUser $ \user -> do (gId, gMemberIds) <- withStore $ \db -> do gId <- getGroupIdByName db user gName - gMemberIds <- forM gMemberNames $ getGroupMemberIdByName db user gId + gMemberIds <- S.fromList <$> mapM (getGroupMemberIdByName db user gId) (S.toList gMemberNames) pure (gId, gMemberIds) - processChatCommand $ APIRemoveMembers gId gMemberIds + processChatCommand $ APIRemoveMembers gId gMemberIds withMessages LeaveGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APILeaveGroup groupId @@ -2274,16 +2254,18 @@ processChatCommand' vr = \case updateGroupProfileByName gName $ \p -> p {description} ShowGroupDescription gName -> withUser $ \user -> CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) - APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do + APICreateGroupLink groupId mRole short -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId assertUserGroupRole gInfo GRAdmin when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole groupLinkId <- GroupLinkId <$> drgRandomBytes 16 subMode <- chatReadVar subscriptionMode let crClientData = encodeJSON $ CRDataGroup groupLinkId - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just crClientData) IKPQOff subMode - withFastStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode - pure $ CRGroupLinkCreated user gInfo cReq mRole + userData = shortLinkUserData short + (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode + ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink + withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole subMode + pure $ CRGroupLinkCreated user gInfo ccLink' mRole APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId (groupLinkId, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo @@ -2309,7 +2291,7 @@ processChatCommand' vr = \case when (isJust $ memberContactId m) $ throwChatError $ CECommandError "member contact already exists" subMode <- chatReadVar subscriptionMode -- TODO PQ should negotitate contact connection with PQSupportOn? - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOff subMode + (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode -- TODO not sure it is correct to set connections status here? @@ -2327,12 +2309,12 @@ processChatCommand' vr = \case let ct' = ct {contactGrpInvSent = True} forM_ msgContent_ $ \mc -> do ci <- saveSndChatItem user (CDDirectSnd ct') sndMsg (CISndMsgContent mc) - toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] pure $ CRNewMemberContactSentInv user ct' g m _ -> throwChatError CEGroupMemberNotActive - CreateGroupLink gName mRole -> withUser $ \user -> do + CreateGroupLink gName mRole short -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APICreateGroupLink groupId mRole + processChatCommand $ APICreateGroupLink groupId mRole short GroupLinkMemberRole gName mRole -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIGroupLinkMemberRole groupId mRole @@ -2356,7 +2338,7 @@ processChatCommand' vr = \case LastChats count_ -> withUser' $ \user -> do let count = fromMaybe 5000 count_ (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) - unless (null errs) $ toView $ CRChatErrors (Just user) (map ChatErrorStore errs) + unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs) pure $ CRChats previews LastMessages (Just chatName) count search -> withUser $ \user -> do chatRef <- getChatRef user chatName @@ -2402,7 +2384,7 @@ processChatCommand' vr = \case processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage - SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" + SendFileDescription _chatName _f -> throwCmdError "TODO" -- TODO to use priority transactions we need a parameter that differentiates manual and automatic acceptance ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> withFileLock "receiveFile" fileId . procCmd $ do @@ -2425,7 +2407,7 @@ processChatCommand' vr = \case throwChatError $ CEFileCancel fileId "file transfer is complete" | otherwise -> do fileAgentConnIds <- cancelSndFile user ftm fts True - deleteAgentConnectionsAsync user fileAgentConnIds + deleteAgentConnectionsAsync fileAgentConnIds withFastStore (\db -> liftIO $ lookupChatRefByFileId db user fileId) >>= \case Nothing -> pure () Just (ChatRef CTDirect contactId) -> do @@ -2446,7 +2428,7 @@ processChatCommand' vr = \case | rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete" | otherwise -> case xftpRcvFile of Nothing -> do - cancelRcvFileTransfer user ftr >>= mapM_ (deleteAgentConnectionAsync user) + cancelRcvFileTransfer user ftr >>= mapM_ deleteAgentConnectionAsync ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId pure $ CRRcvFileCancelled user ci ftr Just XFTPRcvFile {agentRcvFileId} -> do @@ -2594,22 +2576,13 @@ processChatCommand' vr = \case GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions GetAgentQueuesInfo -> lift $ CRAgentQueuesInfo <$> withAgent' getAgentQueuesInfo -- CustomChatCommand is unsupported, it can be processed in preCmdHook - -- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand - CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported" + -- in a modified CLI app or core - the hook should return Either (Either ChatError ChatResponse) ChatCommand, + -- where Left means command result, and Right – some other command to be processed by this function. + CustomChatCommand _cmd -> withUser $ \_ -> throwCmdError "not supported" where - -- below code would make command responses asynchronous where they can be slow - -- in View.hs `r'` should be defined as `id` in this case - -- procCmd :: m ChatResponse -> m ChatResponse - -- procCmd action = do - -- ChatController {chatLock = l, smpAgent = a, outputQ = q, random = gVar} <- ask - -- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 - -- void . forkIO $ - -- withAgentLock a . withLock l name $ - -- (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchChatError` (pure . CRChatError)) - -- pure $ CRCmdAccepted corrId - -- use function below to make commands "synchronous" procCmd :: CM ChatResponse -> CM ChatResponse procCmd = id + {-# INLINE procCmd #-} ok_ = pure $ CRCmdOk Nothing ok = pure . CRCmdOk . Just getChatRef :: User -> ChatName -> CM ChatRef @@ -2669,8 +2642,8 @@ processChatCommand' vr = \case CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg _ -> throwChatError $ CECommandError "not supported" - connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> CM ChatResponse - connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withInvitationLock "connectViaContact" (strEncode cReq) $ do + connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> CM ChatResponse + connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq case groupLinkId of @@ -2700,11 +2673,12 @@ processChatCommand' vr = \case -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup + let sLnk' = serverShortLink <$> sLnk + conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV pure $ CRSentInvitation user conn incognitoProfile - connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> CM ChatResponse - connectContactViaAddress user incognito ct cReq = + connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse + connectContactViaAddress user incognito ct (CCLink cReq shortLink) = withInvitationLock "connectContactViaAddress" (strEncode cReq) $ do newXContactId <- XContactId <$> drgRandomBytes 16 let pqSup = PQSupportOn @@ -2713,10 +2687,10 @@ processChatCommand' vr = \case -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - (pccConnId, ct') <- withFastStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup + (pccConnId, ct') <- withFastStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash shortLink newXContactId incognitoProfile subMode chatV pqSup joinContact user pccConnId connId cReq incognitoProfile newXContactId False pqSup chatV pure $ CRSentInvitationToContact user ct' incognitoProfile - prepareContact :: User -> ConnectionRequestUri 'CMContact -> PQSupport -> CM (ConnId, VersionChat) + prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat) prepareContact user cReq pqSup = do -- 0) toggle disabled - PQSupportOff -- 1) toggle enabled, address supports PQ (connRequestPQSupport returns Just True) - PQSupportOn, enable support with compression @@ -2727,7 +2701,7 @@ processChatCommand' vr = \case let chatV = agentToChatVersion agentV connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup pure (connId, chatV) - joinContact :: User -> Int64 -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM () + joinContact :: User -> Int64 -> ConnId -> ConnReqContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM () joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV = do let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId) @@ -2770,7 +2744,7 @@ processChatCommand' vr = \case let idsEvts = L.map ctSndEvent changedCts msgReqs_ <- lift $ L.zipWith ctMsgReq changedCts <$> createSndMessages idsEvts (errs, cts) <- partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_ - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts lift $ createContactsSndFeatureItems user' changedCts' pure @@ -2810,7 +2784,7 @@ processChatCommand' vr = \case mergedProfile' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False when (mergedProfile' /= mergedProfile) $ withContactLock "updateProfile" (contactId' ct) $ do - void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchChatError` (toView . CRChatError (Just user)) + void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchChatError` eToView lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' pure $ CRContactPrefsUpdated user ct ct' runUpdateGroupProfile :: User -> Group -> GroupProfile -> CM ChatResponse @@ -2834,7 +2808,7 @@ processChatCommand' vr = \case let cd = CDGroupSnd g' unless (sameGroupProfileInfo p p') $ do ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') - toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat g') ci] + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat g') ci] createGroupFeatureChangedItems user cd CISndGroupFeature g g' pure $ CRGroupUpdated user g g' Nothing checkValidName :: GroupName -> CM () @@ -2849,7 +2823,7 @@ processChatCommand' vr = \case when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g) when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved unless (memberActive membership) $ throwChatError CEGroupMemberNotActive - delGroupChatItemsForMembers :: User -> GroupInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM ChatResponse + delGroupChatItemsForMembers :: User -> GroupInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM [ChatItemDeletion] delGroupChatItemsForMembers user gInfo ms items = do assertDeletable gInfo items assertUserGroupRole gInfo GRAdmin -- TODO GRModerator when most users migrate @@ -2878,16 +2852,16 @@ processChatCommand' vr = \case CIGroupRcv GroupMember {memberId} -> (msgId, memberId) CIGroupSnd -> (msgId, membershipMemId) - delGroupChatItems :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> CM ChatResponse + delGroupChatItems :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> CM [ChatItemDeletion] delGroupChatItems user gInfo@GroupInfo {membership} items moderation = do deletedTs <- liftIO getCurrentTime when moderation $ do ciIds <- concat <$> withStore' (\db -> forM items $ \(CChatItem _ ci) -> markMessageReportsDeleted db user gInfo ci membership deletedTs) - unless (null ciIds) $ toView $ CRGroupChatItemsDeleted user gInfo ciIds True (Just membership) + unless (null ciIds) $ toView $ CEvtGroupChatItemsDeleted user gInfo ciIds True (Just membership) let m = if moderation then Just membership else Nothing if groupFeatureMemberAllowed SGFFullDelete membership gInfo - then deleteGroupCIs user gInfo items True False m deletedTs - else markGroupCIsDeleted user gInfo items True m deletedTs + then deleteGroupCIs user gInfo items m deletedTs + else markGroupCIsDeleted user gInfo items m deletedTs updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse updateGroupProfileByName gName update = withUser $ \user -> do g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> @@ -2964,7 +2938,7 @@ processChatCommand' vr = \case let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole timed_ <- contactCITimed ct ci <- saveSndChatItem' user (CDDirectSnd ct) msg content Nothing Nothing Nothing timed_ False - toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) drgRandomBytes :: Int -> CM ByteString @@ -3010,11 +2984,10 @@ processChatCommand' vr = \case deleteChatUser :: User -> Bool -> CM ChatResponse deleteChatUser user delSMPQueues = do filesInfo <- withFastStore' (`getUserFileInfo` user) - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo + deleteCIFiles user filesInfo withAgent (\a -> deleteUser a (aUserId user) delSMPQueues) `catchChatError` \case - e@(ChatErrorAgent NO_USER _) -> toView $ CRChatError (Just user) e + e@(ChatErrorAgent NO_USER _) -> eToView e e -> throwError e withFastStore' (`deleteUserRecord` user) when (activeUser user) $ chatWriteVar currentUser Nothing @@ -3033,32 +3006,77 @@ processChatCommand' vr = \case pure (gId, chatSettings) _ -> throwChatError $ CECommandError "not supported" processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings - connectPlan :: User -> AConnectionRequestUri -> CM ConnectionPlan - connectPlan user (ACR SCMInvitation (CRInvitationUri crData e2e)) = do - withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqSchemas) >>= \case - Nothing -> pure $ CPInvitationLink ILPOk - Just (RcvDirectMsgConnection Connection {connStatus = ConnPrepared} Nothing) -> - pure $ CPInvitationLink ILPOk - Just (RcvDirectMsgConnection conn ct_) -> do - let Connection {connStatus, contactConnInitiated} = conn - if - | connStatus == ConnNew && contactConnInitiated -> - pure $ CPInvitationLink ILPOwnLink - | not (connReady conn) -> - pure $ CPInvitationLink (ILPConnecting ct_) - | otherwise -> case ct_ of - Just ct -> pure $ CPInvitationLink (ILPKnown ct) - Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact" - Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" + connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan) + connectPlan user (ACL SCMInvitation cLink) = case cLink of + CLFull cReq -> invitationReqAndPlan cReq Nothing + CLShort l -> do + let l' = serverShortLink l + withFastStore' (\db -> getConnectionEntityViaShortLink db vr user l') >>= \case + Just (cReq, ent) -> + (ACCL SCMInvitation (CCLink cReq (Just l')),) <$> (invitationEntityPlan ent `catchChatError` (pure . CPError)) + Nothing -> getShortLinkConnReq user l' >>= (`invitationReqAndPlan` Just l') where - cReqSchemas :: (ConnReqInvitation, ConnReqInvitation) - cReqSchemas = + invitationReqAndPlan cReq sLnk_ = do + plan <- inviationRequestPlan user cReq `catchChatError` (pure . CPError) + pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) + connectPlan user (ACL SCMContact cLink) = case cLink of + CLFull cReq -> contactReqAndPlan cReq Nothing + CLShort l@(CSLContact _ ct _ _) -> do + let l' = serverShortLink l + case ct of + CCTContact -> + withFastStore' (\db -> getUserContactLinkViaShortLink db user l') >>= \case + Just (UserContactLink (CCLink cReq _) _) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPContactAddress CAPOwnLink) + Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l') + CCTGroup -> + withFastStore' (\db -> getGroupInfoViaUserShortLink db vr user l') >>= \case + Just (cReq, g) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPGroupLink (GLPOwnLink g)) + Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l') + CCTChannel -> throwChatError $ CECommandError "channel links are not supported in this version" + where + contactReqAndPlan cReq sLnk_ = do + plan <- contactRequestPlan user cReq `catchChatError` (pure . CPError) + pure (ACCL SCMContact $ CCLink cReq sLnk_, plan) + connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse + connectWithPlan user@User {userId} incognito ccLink plan + | connectionPlanProceed plan = do + case plan of CPError e -> eToView e; _ -> pure () + case plan of + CPContactAddress (CAPContactViaAddress Contact {contactId}) -> + processChatCommand $ APIConnectContactViaAddress userId incognito contactId + _ -> processChatCommand $ APIConnect userId incognito (Just ccLink) + | otherwise = pure $ CRConnectionPlan user ccLink plan + inviationRequestPlan :: User -> ConnReqInvitation -> CM ConnectionPlan + inviationRequestPlan user cReq = do + withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ cReqSchemas cReq) >>= \case + Nothing -> pure $ CPInvitationLink ILPOk + Just ent -> invitationEntityPlan ent + where + cReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation) + cReqSchemas (CRInvitationUri crData e2e) = ( CRInvitationUri crData {crScheme = SSSimplex} e2e, CRInvitationUri crData {crScheme = simplexChat} e2e ) - connectPlan user (ACR SCMContact (CRContactUri crData)) = do + invitationEntityPlan :: ConnectionEntity -> CM ConnectionPlan + invitationEntityPlan = \case + RcvDirectMsgConnection Connection {connStatus = ConnPrepared} Nothing -> + pure $ CPInvitationLink ILPOk + RcvDirectMsgConnection conn ct_ -> do + let Connection {connStatus, contactConnInitiated} = conn + if + | connStatus == ConnNew && contactConnInitiated -> + pure $ CPInvitationLink ILPOwnLink + | not (connReady conn) -> + pure $ CPInvitationLink (ILPConnecting ct_) + | otherwise -> case ct_ of + Just ct -> pure $ CPInvitationLink (ILPKnown ct) + Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact" + _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" + contactRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan + contactRequestPlan user (CRContactUri crData) = do let ConnReqUriData {crClientData} = crData groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli + cReqHashes = bimap hash hash cReqSchemas case groupLinkId of -- contact address Nothing -> @@ -3104,9 +3122,31 @@ processChatCommand' vr = \case ( CRContactUri crData {crScheme = SSSimplex}, CRContactUri crData {crScheme = simplexChat} ) - cReqHashes :: (ConnReqUriHash, ConnReqUriHash) - cReqHashes = bimap hash hash cReqSchemas + hash :: ConnReqContact -> ConnReqUriHash hash = ConnReqUriHash . C.sha256Hash . strEncode + getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m) + getShortLinkConnReq user l = do + l' <- restoreShortLink' l + (cReq, cData) <- withAgent (\a -> getConnShortLink a (aUserId user) l') + case cData of + ContactLinkData {direct} | not direct -> throwChatError CEUnsupportedConnReq + _ -> pure () + pure cReq + -- This function is needed, as UI uses simplex:/ schema in message view, so that the links can be handled without browser, + -- and short links are stored with server hostname schema, so they wouldn't match without it. + serverShortLink :: ConnShortLink m -> ConnShortLink m + serverShortLink = \case + CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey + CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey + restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) + shortLinkUserData short = if short then Just "" else Nothing + shortenCreatedLink :: CreatedConnLink m -> CM (CreatedConnLink m) + shortenCreatedLink (CCLink cReq sLnk) = CCLink cReq <$> mapM (\l -> (`shortenShortLink` l) <$> asks (shortLinkPresetServers . config)) sLnk + createdGroupLink :: CreatedLinkContact -> CreatedLinkContact + createdGroupLink (CCLink cReq shortLink) = CCLink cReq (toGroupLink <$> shortLink) + where + toGroupLink :: ShortLinkContact -> ShortLinkContact + toGroupLink (CSLContact sch _ srv k) = CSLContact sch CCTGroup srv k updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM () updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db vr user groupId @@ -3150,7 +3190,7 @@ processChatCommand' vr = \case let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) msgs_ when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch" r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) Nothing itemsData timed_ live - processSendErrs user r + processSendErrs r forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> forM_ cis $ \ci -> startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) deleteAt @@ -3230,7 +3270,7 @@ processChatCommand' vr = \case when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" createMemberSndStatuses cis_ msgs_ gsr let r@(_, cis) = partitionEithers cis_ - processSendErrs user r + processSendErrs r forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> forM_ cis $ \ci -> startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) deleteAt @@ -3300,7 +3340,7 @@ processChatCommand' vr = \case case contactOrGroup of CGContact Contact {activeConn} -> forM_ activeConn $ \conn -> withFastStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr - CGGroup _ ms -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user)) + CGGroup _ ms -> forM_ ms $ \m -> saveMemberFD m `catchChatError` eToView where -- we are not sending files to pending members, same as with inline files saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} = @@ -3319,23 +3359,23 @@ processChatCommand' vr = \case zipWith4 $ \(ComposedMessage {msgContent}, itemForwarded, ts, mm) f q -> \case Right msg -> Right $ NewSndChatItemData msg (CISndMsgContent msgContent) ts mm f q itemForwarded Left e -> Left e -- step over original error - processSendErrs :: User -> ([ChatError], [ChatItem c d]) -> CM () - processSendErrs user = \case + processSendErrs :: ([ChatError], [ChatItem c d]) -> CM () + processSendErrs = \case -- no errors ([], _) -> pure () -- at least one item is successfully created - (errs, _ci : _) -> toView $ CRChatErrors (Just user) errs + (errs, _ci : _) -> toView $ CEvtChatErrors errs -- single error ([err], []) -> throwError err -- multiple errors (errs@(err : _), []) -> do - toView $ CRChatErrors (Just user) errs + toView $ CEvtChatErrors errs throwError err getCommandDirectChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (Contact, [CChatItem 'CTDirect]) getCommandDirectChatItems user ctId itemIds = do ct <- withFastStore $ \db -> getContact db vr user ctId (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds)) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs pure (ct, items) where getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect)) @@ -3344,7 +3384,7 @@ processChatCommand' vr = \case getCommandGroupChatItems user gId itemIds = do gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db gInfo) (L.toList itemIds)) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs pure (gInfo, items) where getGroupCI :: DB.Connection -> GroupInfo -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) @@ -3353,7 +3393,7 @@ processChatCommand' vr = \case getCommandLocalChatItems user nfId itemIds = do nf <- withStore $ \db -> getNoteFolder db user nfId (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds)) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs pure (nf, items) where getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal)) @@ -3478,7 +3518,7 @@ startExpireCIThread user@User {userId} = do liftIO $ threadDelay' delay interval <- asks $ ciExpirationInterval . config forever $ do - flip catchChatError' (toView' . CRChatError (Just user)) $ do + flip catchChatError' (eToView') $ do expireFlags <- asks expireCIFlags atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry lift waitChatStartedAndActivated @@ -3510,7 +3550,7 @@ agentSubscriber = do q <- asks $ subQ . smpAgent forever (atomically (readTBQueue q) >>= process) `E.catchAny` \e -> do - toView' $ CRChatError Nothing $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing + eToView' $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing E.throwIO e where process :: (ACorrId, AEntityId, AEvt) -> CM' () @@ -3520,7 +3560,7 @@ agentSubscriber = do SAERcvFile -> processAgentMsgRcvFile corrId entId msg SAESndFile -> processAgentMsgSndFile corrId entId msg where - run action = action `catchChatError'` (toView' . CRChatError Nothing) + run action = action `catchChatError'` (eToView') type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType ())) @@ -3573,7 +3613,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do viaUserContactLink, groupLinkId, customUserProfileId, - connReqInv = Nothing, + connLinkInv = Nothing, localAlias, createdAt, updatedAt = createdAt @@ -3625,9 +3665,9 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do notifyCLI = do let cRs = resultsFor rs cts cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs - toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs - when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors - notifyAPI = toView . CRNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus) + toView . CEvtContactSubSummary user $ map (uncurry ContactSubStatus) cRs + when ce $ mapM_ (toView . uncurry (CEvtContactSubError user)) cErrors + notifyAPI = toView . CEvtNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus) statuses = M.foldrWithKey' addStatus [] cts where addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)] @@ -3644,44 +3684,44 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do e -> show e -- TODO possibly below could be replaced with less noisy events for API contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM () - contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs + contactLinkSubsToView rs = toView . CEvtUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [ShortGroup] -> Map ConnId ShortGroupMember -> Bool -> CM () groupSubsToView rs gs ms ce = do mapM_ groupSub $ sortOn (\(ShortGroup ShortGroupInfo {groupName = g} _) -> g) gs - toView . CRMemberSubSummary user $ map (uncurry MemberSubStatus) mRs + toViewTE . TEMemberSubSummary user $ map (uncurry MemberSubStatus) mRs where mRs = resultsFor rs ms groupSub :: ShortGroup -> CM () groupSub (ShortGroup g@ShortGroupInfo {groupId = gId, membershipStatus} members) = do - when ce $ mapM_ (toView . uncurry (CRMemberSubError user g)) mErrors - toView groupEvent + when ce $ mapM_ (toViewTE . uncurry (TEMemberSubError user g)) mErrors + toViewTE groupEvent where mErrors :: [(ShortGroupMember, ChatError)] mErrors = sortOn (\(ShortGroupMember {memberName = n}, _) -> n) . filterErrors $ filter (\(ShortGroupMember {groupId}, _) -> groupId == gId) mRs - groupEvent :: ChatResponse + groupEvent :: TerminalEvent groupEvent - | membershipStatus == GSMemInvited = CRGroupInvitation user g - | null members = CRGroupEmpty user g - | otherwise = CRGroupSubscribed user g + | membershipStatus == GSMemInvited = TEGroupInvitation user g + | null members = TEGroupEmpty user g + | otherwise = TEGroupSubscribed user g sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> CM () sndFileSubsToView rs sfts = do let sftRs = resultsFor rs sfts forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do - forM_ err_ $ toView . CRSndFileSubError user ft + forM_ err_ $ toViewTE . TESndFileSubError user ft void . forkIO $ do threadDelay 1000000 when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $ sendFileChunk user ft rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM () - rcvFileSubsToView rs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . filterErrors . resultsFor rs + rcvFileSubsToView rs = mapM_ (toViewTE . uncurry (TERcvFileSubError user)) . filterErrors . resultsFor rs pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM () - pendingConnSubsToView rs = toView . CRPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs + pendingConnSubsToView rs = toViewTE . TEPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a] - withStore_ a = withStore' (`a` user) `catchChatError` \e -> toView (CRChatError (Just user) e) $> [] + withStore_ a = withStore' (`a` user) `catchChatError` \e -> eToView e $> [] filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)] filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_) resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)] @@ -3703,28 +3743,28 @@ cleanupManager = do liftIO $ threadDelay' initialDelay stepDelay <- asks (cleanupManagerStepDelay . config) forever $ do - flip catchChatError (toView . CRChatError Nothing) $ do + flip catchChatError eToView $ do lift waitChatStartedAndActivated users <- withStore' getUsers let (us, us') = partition activeUser users forM_ us $ cleanupUser interval stepDelay forM_ us' $ cleanupUser interval stepDelay - cleanupMessages `catchChatError` (toView . CRChatError Nothing) + cleanupMessages `catchChatError` eToView -- TODO possibly, also cleanup async commands - cleanupProbes `catchChatError` (toView . CRChatError Nothing) + cleanupProbes `catchChatError` eToView liftIO $ threadDelay' $ diffToMicroseconds interval where - runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do + runWithoutInitialDelay cleanupInterval = flip catchChatError eToView $ do lift waitChatStartedAndActivated users <- withStore' getUsers let (us, us') = partition activeUser users - forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u)) - forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u)) + forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` eToView + forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` eToView cleanupUser cleanupInterval stepDelay user = do - cleanupTimedItems cleanupInterval user `catchChatError` (toView . CRChatError (Just user)) + cleanupTimedItems cleanupInterval user `catchChatError` eToView liftIO $ threadDelay' stepDelay -- TODO remove in future versions: legacy step - contacts are no longer marked as deleted - cleanupDeletedContacts user `catchChatError` (toView . CRChatError (Just user)) + cleanupDeletedContacts user `catchChatError` eToView liftIO $ threadDelay' stepDelay cleanupTimedItems cleanupInterval user = do ts <- liftIO getCurrentTime @@ -3736,7 +3776,7 @@ cleanupManager = do contacts <- withStore' $ \db -> getDeletedContacts db vr user forM_ contacts $ \ct -> withStore (\db -> deleteContactWithoutGroups db user ct) - `catchChatError` (toView . CRChatError (Just user)) + `catchChatError` eToView cleanupMessages = do ts <- liftIO getCurrentTime let cutoffTs = addUTCTime (-(30 * nominalDay)) ts @@ -3762,7 +3802,7 @@ expireChatItems user@User {userId} globalTTL sync = do loop :: [Int64] -> (Int64 -> CM ()) -> CM () loop [] _ = pure () loop (a : as) process = continue $ do - process a `catchChatError` (toView . CRChatError (Just user)) + process a `catchChatError` eToView loop as process continue :: CM () -> CM () continue a = @@ -3783,8 +3823,7 @@ expireContactChatItems user vr globalTTL ctId = withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do lift waitChatStartedAndActivated filesInfo <- withStore' $ \db -> getContactExpiredFileInfo db user ct expirationDate - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo + deleteCIFiles user filesInfo withStore' $ \db -> deleteContactExpiredCIs db user ct expirationDate expireGroupChatItems :: User -> VersionRangeChat -> Int64 -> UTCTime -> GroupId -> CM () @@ -3795,8 +3834,7 @@ expireGroupChatItems user vr globalTTL createdAtCutoff groupId = withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do lift waitChatStartedAndActivated filesInfo <- withStore' $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo + deleteCIFiles user filesInfo withStore' $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m @@ -3934,13 +3972,13 @@ chatCommandP = "/_ntf check " *> (APICheckToken <$> strP), "/_ntf delete " *> (APIDeleteToken <$> strP), "/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP), - "/_ntf conn messages " *> (ApiGetConnNtfMessages <$> strP), + "/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP), "/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), "/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI "/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), "/_member role #" *> (APIMembersRole <$> A.decimal <*> _strP <*> memberRole), - "/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* A.space <* "blocked=" <*> onOffP), - "/_remove #" *> (APIRemoveMembers <$> A.decimal <*> _strP), + "/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* " blocked=" <*> onOffP), + "/_remove #" *> (APIRemoveMembers <$> A.decimal <*> _strP <*> (" messages=" *> onOffP <|> pure False)), "/_leave #" *> (APILeaveGroup <$> A.decimal), "/_members #" *> (APIListMembers <$> A.decimal), "/_server test " *> (APITestProtoServer <$> A.decimal <* A.space <*> strP), @@ -4026,7 +4064,7 @@ chatCommandP = ("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> memberRole), "/block for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True), "/unblock for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False), - ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMembers <$> displayNameP <* A.space <*> (L.fromList <$> (char_ '@' *> displayNameP) `A.sepBy1'` A.char ',')), + ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMembers <$> displayNameP <* A.space <*> (S.fromList <$> (char_ '@' *> displayNameP) `A.sepBy1'` A.char ',') <*> (" messages=" *> onOffP <|> pure False)), ("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayNameP), ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayNameP), ("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayNameP <*> chatDeleteMode), @@ -4043,11 +4081,11 @@ chatCommandP = "/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <* A.space <*> (Just <$> msgTextP)), "/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing), "/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP), - "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)), + "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember) <*> shortOnOffP), "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), "/_get link #" *> (APIGetGroupLink <$> A.decimal), - "/create link #" *> (CreateGroupLink <$> displayNameP <*> (memberRole <|> pure GRMember)), + "/create link #" *> (CreateGroupLink <$> displayNameP <*> (memberRole <|> pure GRMember) <*> shortP), "/set link role #" *> (GroupLinkMemberRole <$> displayNameP <*> memberRole), "/delete link #" *> (DeleteGroupLink <$> displayNameP), "/show link #" *> (ShowGroupLink <$> displayNameP), @@ -4058,12 +4096,12 @@ chatCommandP = "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP), - "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), - "/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP), + "/_connect " *> (APIAddContact <$> A.decimal <*> shortOnOffP <*> incognitoOnOffP), + "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP), "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), "/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal), + ("/connect" <|> "/c") *> (AddContact <$> shortP <*> incognitoP), ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), - ("/connect" <|> "/c") *> (AddContact <$> incognitoP), ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, @@ -4078,7 +4116,7 @@ chatCommandP = ("\\\\ #" <|> "\\\\#") *> (DeleteMemberMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> textP), ("! " <|> "!") *> (EditMessage <$> chatNameP <* A.space <*> (quotedMsg <|> pure "") <*> msgTextP), ReactToMessage <$> (("+" $> True) <|> ("-" $> False)) <*> reactionP <* A.space <*> chatNameP' <* A.space <*> textP, - "/feed " *> (SendMessageBroadcast <$> msgTextP), + "/feed " *> (SendMessageBroadcast . MCText <$> msgTextP), ("/chats" <|> "/cs") *> (LastChats <$> (" all" $> Nothing <|> Just <$> (A.space *> A.decimal <|> pure 20))), ("/tail" <|> "/t") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> pure Nothing), ("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> stringP))), @@ -4097,8 +4135,8 @@ chatCommandP = ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), "/simplex" *> (ConnectSimplex <$> incognitoP), - "/_address " *> (APICreateMyAddress <$> A.decimal), - ("/address" <|> "/ad") $> CreateMyAddress, + "/_address " *> (APICreateMyAddress <$> A.decimal <*> shortOnOffP), + ("/address" <|> "/ad") *> (CreateMyAddress <$> shortP), "/_delete_address " *> (APIDeleteMyAddress <$> A.decimal), ("/delete_address" <|> "/da") $> DeleteMyAddress, "/_show_address " *> (APIShowMyAddress <$> A.decimal), @@ -4168,7 +4206,12 @@ chatCommandP = ] where choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput) + connLinkP = do + ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) + >>= mapM (\(ACR m cReq) -> ACCL m . CCLink cReq <$> optional (A.space *> strP)) + shortP = (A.space *> ("short" <|> "s")) $> True <|> pure False incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False + shortOnOffP = (A.space *> "short=" *> onOffP) <|> pure False incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,") imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P)) @@ -4232,7 +4275,7 @@ chatCommandP = { directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing}, history = Just HistoryGroupPreference {enable = FEOn} } - pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences} + pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences, memberAdmission = Nothing} fullNameP = A.space *> textP <|> pure "" textP = safeDecodeUtf8 <$> A.takeByteString pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' ')) @@ -4244,6 +4287,12 @@ chatCommandP = cfArgs <- optional $ CFArgs <$> (" key=" *> strP <* A.space) <*> (" nonce=" *> strP) path <- filePath pure $ CryptoFile path cfArgs + connMsgsP = L.fromList <$> connMsgP `A.sepBy1'` A.char ',' + connMsgP = do + AgentConnId msgConnId <- strP <* A.char ':' + msgDbQueueId <- strP <* A.char ':' + ts <- strP + pure ConnMsgReq {msgConnId, msgDbQueueId, msgTs = Just ts} memberRole = A.choice [ " owner" $> GROwner, @@ -4306,11 +4355,11 @@ chatCommandP = requiredHostMode <- (" required-host-mode" $> True) <|> pure False smpProxyMode_ <- optional $ " smp-proxy=" *> strP smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP - smpWebPort <- (" smp-web-port" $> True) <|> pure False + smpWebPortServers <- (" smp-web-port-servers=" *> strP) <|> (" smp-web-port" $> SWPAll) <|> pure SWPPreset t_ <- optional $ " timeout=" *> A.decimal logTLSErrors <- " log=" *> onOffP <|> pure False let tcpTimeout_ = (1000000 *) <$> t_ - pure $ SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} + pure $ SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPortServers, tcpTimeout_, logTLSErrors} #if !defined(dbPostgres) dbKeyP = nonEmptyKey <$?> strP nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k @@ -4335,7 +4384,7 @@ displayNameP = safeDecodeUtf8 <$> (quoted '\'' <|> takeNameTill (\c -> isSpace c refChar c = c > ' ' && c /= '#' && c /= '@' && c /= '\'' mkValidName :: String -> String -mkValidName = reverse . dropWhile isSpace . fst3 . foldl' addChar ("", '\NUL', 0 :: Int) +mkValidName = dropWhileEnd isSpace . take 50 . reverse . fst3 . foldl' addChar ("", '\NUL', 0 :: Int) where fst3 (x, _, _) = x addChar (r, prev, punct) c = if validChar then (c' : r, c', punct') else (r, prev, punct) diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 1b18abda37..bbefbcfde0 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -184,11 +184,11 @@ callTimed ct aciContent = aciContentCallStatus (ACIContent _ (CIRcvCall st _)) = Just st aciContentCallStatus _ = Nothing -toggleNtf :: User -> GroupMember -> Bool -> CM () -toggleNtf user m ntfOn = +toggleNtf :: GroupMember -> Bool -> CM () +toggleNtf m ntfOn = when (memberActive m) $ forM_ (memberConnId m) $ \connId -> - withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` (toView . CRChatError (Just user)) + withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` eToView prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup)) prepareGroupMsg db user g@GroupInfo {membership} mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of @@ -388,8 +388,8 @@ cancelFilesInProgress user filesInfo = do lift $ agentXFTPDeleteRcvFiles xrfIds let smpSFConnIds = concatMap (\(ft, sfts) -> mapMaybe (smpSndFileConnId ft) sfts) sfs smpRFConnIds = mapMaybe smpRcvFileConnId rfs - deleteAgentConnectionsAsync user smpSFConnIds - deleteAgentConnectionsAsync user smpRFConnIds + deleteAgentConnectionsAsync smpSFConnIds + deleteAgentConnectionsAsync smpRFConnIds where fileEnded CIFileInfo {fileStatus} = case fileStatus of Just (AFS _ status) -> ciFileEnded status @@ -441,25 +441,25 @@ deleteFilesLocally files = withFilesFolder :: (FilePath -> CM ()) -> CM () withFilesFolder action = asks filesFolder >>= readTVarIO >>= mapM_ action -deleteDirectCIs :: User -> Contact -> [CChatItem 'CTDirect] -> Bool -> Bool -> CM ChatResponse -deleteDirectCIs user ct items byUser timed = do +deleteDirectCIs :: User -> Contact -> [CChatItem 'CTDirect] -> CM [ChatItemDeletion] +deleteDirectCIs user ct items = do let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items deleteCIFiles user ciFilesInfo (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) - unless (null errs) $ toView $ CRChatErrors (Just user) errs - pure $ CRChatItemsDeleted user deletions byUser timed + unless (null errs) $ toView $ CEvtChatErrors errs + pure deletions where deleteItem db (CChatItem md ci) = do deleteDirectChatItem db user ct ci pure $ contactDeletion md ct ci Nothing -deleteGroupCIs :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse -deleteGroupCIs user gInfo items byUser timed byGroupMember_ deletedTs = do +deleteGroupCIs :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion] +deleteGroupCIs user gInfo items byGroupMember_ deletedTs = do let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items deleteCIFiles user ciFilesInfo (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) - unless (null errs) $ toView $ CRChatErrors (Just user) errs - pure $ CRChatItemsDeleted user deletions byUser timed + unless (null errs) $ toView $ CEvtChatErrors errs + pure deletions where deleteItem :: DB.Connection -> CChatItem 'CTGroup -> IO ChatItemDeletion deleteItem db (CChatItem md ci) = do @@ -468,12 +468,30 @@ deleteGroupCIs user gInfo items byUser timed byGroupMember_ deletedTs = do Nothing -> Nothing <$ deleteGroupChatItem db user gInfo ci pure $ groupDeletion md gInfo ci ci' +deleteGroupMemberCIs :: MsgDirectionI d => User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> CM () +deleteGroupMemberCIs user gInfo member byGroupMember msgDir = do + deletedTs <- liftIO getCurrentTime + filesInfo <- withStore' $ \db -> deleteGroupMemberCIs_ db user gInfo member byGroupMember msgDir deletedTs + deleteCIFiles user filesInfo + +deleteGroupMembersCIs :: User -> GroupInfo -> [GroupMember] -> GroupMember -> CM () +deleteGroupMembersCIs user gInfo members byGroupMember = do + deletedTs <- liftIO getCurrentTime + filesInfo <- withStore' $ \db -> fmap concat $ forM members $ \m -> deleteGroupMemberCIs_ db user gInfo m byGroupMember SMDRcv deletedTs + deleteCIFiles user filesInfo + +deleteGroupMemberCIs_ :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO [CIFileInfo] +deleteGroupMemberCIs_ db user gInfo member byGroupMember msgDir deletedTs = do + fs <- getGroupMemberFileInfo db user gInfo member + updateMemberCIsModerated db user gInfo member byGroupMember msgDir deletedTs + pure fs + deleteLocalCIs :: User -> NoteFolder -> [CChatItem 'CTLocal] -> Bool -> Bool -> CM ChatResponse deleteLocalCIs user nf items byUser timed = do let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items deleteFilesLocally ciFilesInfo (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs pure $ CRChatItemsDeleted user deletions byUser timed where deleteItem db (CChatItem md ci) = do @@ -487,30 +505,49 @@ deleteCIFiles user filesInfo = do cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo -markDirectCIsDeleted :: User -> Contact -> [CChatItem 'CTDirect] -> Bool -> UTCTime -> CM ChatResponse -markDirectCIsDeleted user ct items byUser deletedTs = do +markDirectCIsDeleted :: User -> Contact -> [CChatItem 'CTDirect] -> UTCTime -> CM [ChatItemDeletion] +markDirectCIsDeleted user ct items deletedTs = do let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items cancelFilesInProgress user ciFilesInfo (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) - unless (null errs) $ toView $ CRChatErrors (Just user) errs - pure $ CRChatItemsDeleted user deletions byUser False + unless (null errs) $ toView $ CEvtChatErrors errs + pure deletions where markDeleted db (CChatItem md ci) = do ci' <- markDirectChatItemDeleted db user ct ci deletedTs pure $ contactDeletion md ct ci (Just ci') -markGroupCIsDeleted :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse -markGroupCIsDeleted user gInfo items byUser byGroupMember_ deletedTs = do +markGroupCIsDeleted :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion] +markGroupCIsDeleted user gInfo items byGroupMember_ deletedTs = do let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items cancelFilesInProgress user ciFilesInfo (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) - unless (null errs) $ toView $ CRChatErrors (Just user) errs - pure $ CRChatItemsDeleted user deletions byUser False + unless (null errs) $ toView $ CEvtChatErrors errs + pure deletions + -- pure $ CRChatItemsDeleted user deletions byUser False where markDeleted db (CChatItem md ci) = do ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs pure $ groupDeletion md gInfo ci (Just ci') +markGroupMemberCIsDeleted :: User -> GroupInfo -> GroupMember -> GroupMember -> CM () +markGroupMemberCIsDeleted user gInfo member byGroupMember = do + deletedTs <- liftIO getCurrentTime + filesInfo <- withStore' $ \db -> markGroupMemberCIsDeleted_ db user gInfo member byGroupMember deletedTs + cancelFilesInProgress user filesInfo + +markGroupMembersCIsDeleted :: User -> GroupInfo -> [GroupMember] -> GroupMember -> CM () +markGroupMembersCIsDeleted user gInfo members byGroupMember = do + deletedTs <- liftIO getCurrentTime + filesInfo <- withStore' $ \db -> fmap concat $ forM members $ \m -> markGroupMemberCIsDeleted_ db user gInfo m byGroupMember deletedTs + cancelFilesInProgress user filesInfo + +markGroupMemberCIsDeleted_ :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> UTCTime -> IO [CIFileInfo] +markGroupMemberCIsDeleted_ db user gInfo member byGroupMember deletedTs = do + fs <- getGroupMemberFileInfo db user gInfo member + markMemberCIsDeleted db user gInfo member byGroupMember deletedTs + pure fs + groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion groupDeletion md g ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci') where @@ -533,7 +570,7 @@ updateCallItemStatus user ct@Contact {contactId} Call {chatItemId} receivedStatu updateDirectChatItemView :: User -> Contact -> ChatItemId -> ACIContent -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> CM () updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) edited live timed_ msgId_ = do ci' <- withStore $ \db -> updateDirectChatItem db user ct chatItemId ciContent edited live timed_ msgId_ - toView $ CRChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci') + toView $ CEvtChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci') callStatusItemContent :: User -> Contact -> ChatItemId -> WebRTCCallStatus -> CM (Maybe ACIContent) callStatusItemContent user Contact {contactId} chatItemId receivedStatus = do @@ -581,11 +618,25 @@ receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do (CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError where - processError = \case - -- TODO AChatItem in Cancelled events - ChatErrorAgent (SMP _ SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft - ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft - e -> throwError e + -- TODO AChatItem in Cancelled events + processError e + | rctFileCancelled e = pure $ CRRcvFileAcceptedSndCancelled user ft + | otherwise = throwError e + +receiveFileEvt' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatEvent +receiveFileEvt' user ft userApprovedRelays rcvInline_ filePath_ = do + (CEvtRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError + where + -- TODO AChatItem in Cancelled events + processError e + | rctFileCancelled e = pure $ CEvtRcvFileAcceptedSndCancelled user ft + | otherwise = throwError e + +rctFileCancelled :: ChatError -> Bool +rctFileCancelled = \case + ChatErrorAgent (SMP _ SMP.AUTH) _ -> True + ChatErrorAgent (CONN DUPLICATE) _ -> True + _ -> False acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do @@ -692,7 +743,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation forM_ aci_ $ \aci -> do cleanupACIFile aci - toView $ CRChatItemUpdated user aci + toView $ CEvtChatItemUpdated user aci throwChatError $ CEFileNotApproved fileId unknownSrvs cleanupACIFile :: AChatItem -> CM () @@ -746,7 +797,7 @@ startReceivingFile user fileId = do liftIO $ updateRcvFileStatus db fileId FSConnected liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1 getChatItemByFileId db vr user fileId - toView $ CRRcvFileStart user ci + toView $ CEvtRcvFileStart user ci getRcvFilePath :: FileTransferId -> Maybe FilePath -> String -> Bool -> CM FilePath getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of @@ -921,7 +972,7 @@ acceptBusinessJoinRequestAsync where businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, image} groupPreferences = - GroupProfile {displayName, fullName, description = Nothing, image, groupPreferences = Just groupPreferences} + GroupProfile {displayName, fullName, description = Nothing, image, groupPreferences = Just groupPreferences, memberAdmission = Nothing} profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing @@ -947,7 +998,7 @@ introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {ac forM_ (L.nonEmpty events) $ \events' -> sendGroupMemberMessages user conn events' groupId else forM_ shuffledIntros $ \intro -> - processIntro intro `catchChatError` (toView . CRChatError (Just user)) + processIntro intro `catchChatError` eToView memberIntro :: GroupMember -> ChatMsgEvent 'Json memberIntro reMember = let mInfo = memberInfo reMember @@ -970,7 +1021,7 @@ introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {ac (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items let errors = map ChatErrorStore errs <> errs' - unless (null errors) $ toView $ CRChatErrors (Just user) errors + unless (null errors) $ toView $ CEvtChatErrors errors let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_ forM_ (L.nonEmpty events') $ \events'' -> sendGroupMemberMessages user conn events'' groupId @@ -1070,7 +1121,7 @@ deleteGroupLinkIfExists user gInfo = do deleteGroupLink_ :: User -> GroupInfo -> Connection -> CM () deleteGroupLink_ user gInfo conn = do - deleteAgentConnectionAsync user $ aConnId conn + deleteAgentConnectionAsync $ aConnId conn withStore' $ \db -> deleteGroupLink db user gInfo startProximateTimedItemThread :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM () @@ -1104,12 +1155,14 @@ deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do case cType of CTDirect -> do (ct, ci) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId - deleteDirectCIs user ct [ci] True True >>= toView + deletions <- deleteDirectCIs user ct [ci] + toView $ CEvtChatItemsDeleted user deletions True True CTGroup -> do (gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId deletedTs <- liftIO getCurrentTime - deleteGroupCIs user gInfo [ci] True True Nothing deletedTs >>= toView - _ -> toView . CRChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType" + deletions <- deleteGroupCIs user gInfo [ci] Nothing deletedTs + toView $ CEvtChatItemsDeleted user deletions True True + _ -> eToView $ ChatError $ CEInternalError "bad deleteTimedItem cType" startUpdatedTimedItemThread :: User -> ChatRef -> ChatItem c d -> ChatItem c d -> CM () startUpdatedTimedItemThread user chatRef ci ci' = @@ -1133,7 +1186,7 @@ createContactPQSndItem user ct conn@Connection {pqSndEnabled} pqSndEnabled' = ct' = ct {activeConn = Just conn'} :: Contact when (contactPQEnabled ct /= contactPQEnabled ct') $ do createInternalChatItem user (CDDirectSnd ct') ciContent Nothing - toView $ CRContactPQEnabled user ct' pqSndEnabled' + toView $ CEvtContactPQEnabled user ct' pqSndEnabled' pure (ct', conn') updateContactPQRcv :: User -> Contact -> Connection -> PQEncryption -> CM (Contact, Connection) @@ -1149,7 +1202,7 @@ updateContactPQRcv user ct conn@Connection {connId, pqRcvEnabled} pqRcvEnabled' ct' = ct {activeConn = Just conn'} :: Contact when (contactPQEnabled ct /= contactPQEnabled ct') $ do createInternalChatItem user (CDDirectRcv ct') ciContent Nothing - toView $ CRContactPQEnabled user ct' pqRcvEnabled' + toView $ CEvtContactPQEnabled user ct' pqRcvEnabled' pure (ct', conn') updatePeerChatVRange :: Connection -> VersionRangeChat -> CM Connection @@ -1234,9 +1287,9 @@ sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentCo liftIO $ updateSndFileStatus db ft FSComplete liftIO $ deleteSndFileChunks db ft updateDirectCIFileStatus db vr user fileId CIFSSndComplete - toView $ CRSndFileComplete user ci ft + toView $ CEvtSndFileComplete user ci ft lift $ closeFileHandle fileId sndFiles - deleteAgentConnectionAsync user acId + deleteAgentConnectionAsync acId sendFileChunkNo :: SndFileTransfer -> Integer -> CM () sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do @@ -1284,7 +1337,7 @@ appendFileChunk ft@RcvFileTransfer {fileId, fileStatus, cryptoArgs, fileInvitati removeFile fsFilePath `catchChatError` \_ -> pure () renameFile tmpFile fsFilePath Left e -> do - toView $ CRChatError Nothing e + eToView e removeFile tmpFile `catchChatError` \_ -> pure () withStore' (`removeFileCryptoArgs` fileId) where @@ -1309,7 +1362,7 @@ isFileActive fileId files = do cancelRcvFileTransfer :: User -> RcvFileTransfer -> CM (Maybe ConnId) cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInline} = - cancel' `catchChatError` (\e -> toView (CRChatError (Just user) e) $> fileConnId) + cancel' `catchChatError` (\e -> eToView e $> fileConnId) where cancel' = do lift $ closeFileHandle fileId rcvFiles @@ -1327,13 +1380,13 @@ cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInlin cancelSndFile :: User -> FileTransferMeta -> [SndFileTransfer] -> Bool -> CM [ConnId] cancelSndFile user FileTransferMeta {fileId, xftpSndFile} fts sendCancel = do withStore' (\db -> updateFileCancelled db user fileId CIFSSndCancelled) - `catchChatError` (toView . CRChatError (Just user)) + `catchChatError` eToView case xftpSndFile of Nothing -> catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel) Just xsf -> do forM_ fts (\ft -> cancelSndFileTransfer user ft False) - lift (agentXFTPDeleteSndFileRemote user xsf fileId) `catchChatError` (toView . CRChatError (Just user)) + lift (agentXFTPDeleteSndFileRemote user xsf fileId) `catchChatError` eToView pure [] -- TODO v6.0 remove @@ -1341,7 +1394,7 @@ cancelSndFileTransfer :: User -> SndFileTransfer -> Bool -> CM (Maybe ConnId) cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, agentConnId = AgentConnId acId, fileStatus, fileInline} sendCancel = if fileStatus == FSCancelled || fileStatus == FSComplete then pure Nothing - else cancel' `catchChatError` (\e -> toView (CRChatError (Just user) e) $> fileConnId) + else cancel' `catchChatError` (\e -> eToView e $> fileConnId) where cancel' = do withStore' $ \db -> do @@ -1368,16 +1421,16 @@ deleteMembersConnections user members = deleteMembersConnections' user members F deleteMembersConnections' :: User -> [GroupMember] -> Bool -> CM () deleteMembersConnections' user members waitDelivery = do let memberConns = mapMaybe (\GroupMember {activeConn} -> activeConn) members - deleteAgentConnectionsAsync' user (map aConnId memberConns) waitDelivery + deleteAgentConnectionsAsync' (map aConnId memberConns) waitDelivery lift . void . withStoreBatch' $ \db -> map (\Connection {connId} -> deleteConnectionRecord db user connId) memberConns -deleteMemberConnection :: User -> GroupMember -> CM () -deleteMemberConnection user mem = deleteMemberConnection' user mem False +deleteMemberConnection :: GroupMember -> CM () +deleteMemberConnection mem = deleteMemberConnection' mem False -deleteMemberConnection' :: User -> GroupMember -> Bool -> CM () -deleteMemberConnection' user GroupMember {activeConn} waitDelivery = do +deleteMemberConnection' :: GroupMember -> Bool -> CM () +deleteMemberConnection' GroupMember {activeConn} waitDelivery = do forM_ activeConn $ \conn -> do - deleteAgentConnectionAsync' user (aConnId conn) waitDelivery + deleteAgentConnectionAsync' (aConnId conn) waitDelivery withStore' $ \db -> updateConnectionStatus db conn ConnDeleted deleteOrUpdateMemberRecord :: User -> GroupMember -> CM () @@ -1462,7 +1515,7 @@ sendGroupMemberMessages user conn events groupId = do when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) let idsEvts = L.map (GroupId groupId,) events (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs forM_ (L.nonEmpty msgs) $ \msgs' -> batchSendConnMessages user conn MsgFlags {notification = True} msgs' @@ -1591,7 +1644,7 @@ sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> Non sendGroupMessages user gInfo members events = do -- TODO [knocking] when sending to all, send profile update to pending approval members too, then filter for next step? when shouldSendProfileUpdate $ - sendProfileUpdate `catchChatError` (toView . CRChatError (Just user)) + sendProfileUpdate `catchChatError` eToView sendGroupMessages_ user gInfo members events where User {profile = p, userMemberProfileUpdatedAt} = user @@ -1747,10 +1800,10 @@ memberSendAction gInfo events members m@GroupMember {memberRole, memberStatus} = XGrpMsgForward {} -> True _ -> False -sendGroupMemberMessage :: MsgEncodingI e => User -> GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM () -sendGroupMemberMessage user gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do +sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM () +sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do msg <- createSndMessage chatMsgEvent (GroupId groupId) - messageMember msg `catchChatError` (toView . CRChatError (Just user)) + messageMember msg `catchChatError` eToView where messageMember :: SndMessage -> CM () messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case @@ -1814,7 +1867,7 @@ saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember@GroupMember {me if sameMemberId refMemberId am then forM_ (memberConn forwardingMember) $ \fmConn -> void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemberId) groupId - else toView $ CRMessageError user "error" "saveGroupFwdRcvMsg: referenced author member id doesn't match message member id" + else toView $ CEvtMessageError user "error" "saveGroupFwdRcvMsg: referenced author member id doesn't match message member id" throwError e _ -> throwError e @@ -1933,20 +1986,22 @@ agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode pure (cmdId, connId) -deleteAgentConnectionAsync :: User -> ConnId -> CM () -deleteAgentConnectionAsync user acId = deleteAgentConnectionAsync' user acId False +deleteAgentConnectionAsync :: ConnId -> CM () +deleteAgentConnectionAsync acId = deleteAgentConnectionAsync' acId False +{-# INLINE deleteAgentConnectionAsync #-} -deleteAgentConnectionAsync' :: User -> ConnId -> Bool -> CM () -deleteAgentConnectionAsync' user acId waitDelivery = do - withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` (toView . CRChatError (Just user)) +deleteAgentConnectionAsync' :: ConnId -> Bool -> CM () +deleteAgentConnectionAsync' acId waitDelivery = do + withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` eToView -deleteAgentConnectionsAsync :: User -> [ConnId] -> CM () -deleteAgentConnectionsAsync user acIds = deleteAgentConnectionsAsync' user acIds False +deleteAgentConnectionsAsync :: [ConnId] -> CM () +deleteAgentConnectionsAsync acIds = deleteAgentConnectionsAsync' acIds False +{-# INLINE deleteAgentConnectionsAsync #-} -deleteAgentConnectionsAsync' :: User -> [ConnId] -> Bool -> CM () -deleteAgentConnectionsAsync' _ [] _ = pure () -deleteAgentConnectionsAsync' user acIds waitDelivery = do - withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` (toView . CRChatError (Just user)) +deleteAgentConnectionsAsync' :: [ConnId] -> Bool -> CM () +deleteAgentConnectionsAsync' [] _ = pure () +deleteAgentConnectionsAsync' acIds waitDelivery = do + withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` eToView agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM () agentXFTPDeleteRcvFile aFileId fileId = do @@ -2047,8 +2102,8 @@ createContactsFeatureItems :: createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do let dirsCIContents = map contactChangedFeatures cts (errs, acis) <- partitionEithers <$> createInternalItemsForChats user Nothing dirsCIContents - unless (null errs) $ toView' $ CRChatErrors (Just user) errs - toView' $ CRNewChatItems user acis + unless (null errs) $ toView' $ CEvtChatErrors errs + toView' $ CEvtNewChatItems user acis where contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d]) contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do @@ -2093,7 +2148,7 @@ createGroupFeatureItems user cd ciContent GroupInfo {fullGroupPreferences} = createInternalChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM () createInternalChatItem user cd content itemTs_ = lift (createInternalItemsForChats user itemTs_ [(cd, [content])]) >>= \case - [Right aci] -> toView $ CRNewChatItems user [aci] + [Right aci] -> toView $ CEvtNewChatItems user [aci] [Left e] -> throwError e rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs) @@ -2129,7 +2184,7 @@ createLocalChatItems :: createLocalChatItems user cd itemsData createdAt = do withStore' $ \db -> updateChatTs db user cd createdAt (errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) $ L.toList itemsData) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + unless (null errs) $ toView $ CEvtChatErrors errs pure items where createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd) @@ -2142,9 +2197,7 @@ withUser' :: (User -> CM ChatResponse) -> CM ChatResponse withUser' action = asks currentUser >>= readTVarIO - >>= maybe (throwChatError CENoActiveUser) run - where - run u = action u `catchChatError` (pure . CRChatCmdError (Just u)) + >>= maybe (throwChatError CENoActiveUser) action withUser :: (User -> CM ChatResponse) -> CM ChatResponse withUser action = withUser' $ \user -> @@ -2198,7 +2251,7 @@ simplexTeamContactProfile = { displayName = "SimpleX Chat team", fullName = "", image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8KCwkMEQ8SEhEPERATFhwXExQaFRARGCEYGhwdHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAETARMDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD7LooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiivP/iF4yFvv0rSpAZek0yn7v+yPeunC4WpiqihBf8A8rOc5w2UYZ4jEPTourfZDvH3jL7MW03SpR53SWUfw+w96veA/F0erRLY3zKl6owD2k/8Ar15EWLEljknqadDK8MqyxMUdTlWB5Br66WS0Hh/ZLfv1ufiNLj7Mo5m8ZJ3g9OTpy+Xn5/pofRdFcd4B8XR6tEthfMEvVHyk9JB/jXY18fiMPUw9R06i1P3PK80w2aYaOIw8rxf3p9n5hRRRWB6AUUVDe3UFlavc3MixxIMsxppNuyJnOMIuUnZIL26gsrV7m5kWOJBlmNeU+I/Gd9e6sk1hI8FvA2Y1z973NVPGnimfXLoxRFo7JD8if3vc1zefevr8syiNKPtKyvJ9Ox+F8Ycb1cdU+rYCTjTi/iWjk1+nbue3eEPEdtrtoMER3SD95Hn9R7Vu18+6bf3On3kd1aSmOVDkEd/Y17J4P8SW2vWY6R3aD97F/Ue1eVmmVPDP2lP4fyPtODeMoZrBYXFO1Zf+Tf8AB7r5o3qKKK8Q/QgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAqavbTXmmz20Fw1vJIhVZB1FeDa3p15pWoSWl6hWQHr2YeoNfQlY3izw9Z6/YGGZQky8xSgcqf8K9jKcyWEnyzXuv8D4njLhZ51RVSi7VYLRdGu3k+z+88HzRuq1rWmXmkX8lnexFHU8Hsw9RVLNfcxlGcVKLumfgFahUozdOorSWjT6E0M0kMqyxOyOpyrKcEGvXPAPjCPVolsb9wl6owGPAkH+NeO5p8M0kMqyxOyOpyrA4INcWPy+njKfLLfoz2+HuIMTkmI9pT1i/ij0a/wA+zPpGiuM+H/jCPV4lsL91S+QfKTwJR/jXW3t1BZWslzcyLHFGMsxNfB4jC1aFX2U1r+fof0Rl2bYXMMKsVRl7vXy7p9rBfXVvZWr3NzKscSDLMTXjnjbxVPrtyYoiY7JD8if3vc0zxv4ruNeujFEWjsoz8if3vc1zOa+synKFh0qtVe9+X/BPxvjLjKWZSeEwjtSW7/m/4H5kmaM1HmlB54r3bH51YkzXo3wz8MXMc0es3ZeED/VR5wW9z7VB8O/BpnMerarEREDuhhb+L3Pt7V6cAAAAAAOgFfL5xmqs6FH5v9D9a4H4MlzQzHGq1tYR/KT/AEXzCiiivlj9hCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAxfFvh208QWBhmASdRmKUdVP+FeH63pl5pGoSWV5EUdTwezD1HtX0VWL4t8O2fiHTzBONk6g+TKByp/wr28pzZ4WXs6msH+B8NxdwhTzeDxGHVqy/8m8n59n954FmjNW9b0y80fUHsr2MpIp4PZh6iqWfevuYyjOKlF3TPwetQnRm6dRWktGmSwzSQyrLE7I6nKsDgg1teIPFOqa3a29vdy4jiUAheN7f3jWBmjNROhTnJTkrtbGtLF4ijSnRpzajPddHbuP3e9Lmo80ua0scth+a9E+HXgw3Hl6tqsZEX3oYmH3vc+1J8OPBZnKavq0eIhzDCw+9/tH29q9SAAAAGAOgr5bOM35b0KD16v8ARH6twXwXz8uPx0dN4xfXzf6IFAUAAAAdBRRRXyZ+wBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFB4GTXyj+1p+0ONJjufA3ga6DX7qU1DUY24gB4McZH8Xqe38tqFCdefLETaSufQ3h/4geEde8Uah4a0rWra51Ow/wBfCrD8ceuO+OldRX5I+GfEWseG/ENvr2j30ttqFvJ5iSqxyT3z6g96/RH9nD41aT8U9AWGcx2fiK1QC7tC33/+mieqn07V14zL3QXNHVEQnc9dooorzjQKKKKACiis7xHrel+HdGudY1m8is7K2QvLLI2AAP600m3ZAYfxUg8Pr4VutT1+7isYbSMuLp/4Pb3z6V8++HNd0zxDpq6hpVys8DHGRwVPoR2NeIftJ/G7VPifrbWVk8lp4btZD9mtwcGU/wDPR/c9h2rgfh34z1LwdrAurV2ktZCBcW5PyyD/AB9DX2WTyqYWny1Ho+nY+C4t4Wp5tF16CtVX/k3k/Ps/vPr/ADRmsjwx4g07xFpMWpaZOJInHI/iQ9wR61qbq+mVmro/D6tCdGbp1FZrdEma6/4XafpWoa7jUpV3oA0MLdJD/ntXG5p8E0kMqyxOyOhyrKcEGsMTRlWpShGVm+p1ZbiYYPFQr1IKai72fU+nFAUAKAAOABRXEfDnxpFrMK6fqDhL9BhSeko9frXb1+a4rDVMNUdOotT+k8szLD5lh44jDu8X968n5hRRRXOegFFFFABUGoXlvYWkl1dSrHFGMliaL+7t7C0kuruVYoYxlmNeI+OvFtx4huzHFuisYz+7jz97/aNenluW1MbU00it2fM8S8SUMkoXetR/DH9X5fmeteF/E+m+IFkFoxSWMnMb9cev0rbr5t0vULrTb6K8s5TFNGcgj+R9q9w8E+KbXxDYjlY7xB+9i/qPaurNsneE/eUtYfkeTwlxjHNV9XxVo1V90vTz8vmjoqKKK8I+8CiiigAooooAKKKKACiiigD5V/a8+P0mgvdeAvCUskepFdl9eDjyQR9xPfHeviiR3lkaSR2d2OWZjkk+tfoj+058CtP+Jektq2jxRWnie2T91KMKLlR/yzf+h7V+fOuaVqGiarcaXqtpLaXls5jlikXDKRX0mWSpOlaG/U56l76lKtPwtr+reGNetdb0S8ls761cPHJG2D9D6g9MVmUV6TSasyD9Jf2cfjXpPxR0MW9w0dp4gtkAubYnHmf7aeo/lXr1fkh4W1/V/DGuW2taHey2d9bOHjkjP6H1HtX6Jfs5fGvR/inoQgmeOz8RWqD7XaE439vMT1U+navnMfgHRfPD4fyN4Tvoz12iis7xJremeHdEutZ1i7jtLK1jLyyucAAf1rzUm3ZGgeJNb0vw7otzrOs3kVpZWyF5ZZDgAD+Z9q/PL9pP436r8UNZaxs2ks/Dlq5+z24ODMf77+p9B2o/aU+N2p/FDXDZ2LS2fhy1ci3t84Mx/wCej+/oO1eNV9DgMAqS55/F+RhOd9EFFFABJwBkmvUMzqPh34y1Lwjq63FszSWshAntyeHHt719Z2EstzpVlqD2txbR3kCzxLPGUbawyODXK/slfs8nUpbXx144tGFkhElhp8q4849pHB/h9B3r608X+GLDxBpX2WRFiljX9xIowUPYfT2rGnnkMPWVJ6x6vt/XU+P4o4SjmtN4igrVV/5N5Pz7P7z56zRmrmvaVe6LqMljexMkiHg9mHqKoZr6uEozipRd0z8Rq0J0ZunUVmtGmTwTSQTJNC7JIhyrKcEGvZvhz41j1mJdP1GRUv0GFY8CX/69eJZqSCaWCVZYXZHU5VlOCDXDmGXU8bT5ZaPo+x7WQZ9iMlxHtKesX8UejX+fZn1FRXDfDbxtHrUKadqDqmoIuAx4EoHf613NfnWKwtTC1HTqKzR/QGW5lh8yw8cRh3eL+9Ps/MKr6heW1hZyXd3KsUUYyzGjUby20+zku7yZYoY13MzGvDPHvi+48RXpjiZorCM/u4/73+0feuvLMsqY6pZaRW7/AK6nlcScR0MloXetR/DH9X5D/Hni648Q3nlxlo7GM/u48/e9zXL7qZmjNfodDDwoU1TpqyR+AY7G18dXlXryvJ/19w/dVvSdRutMvo7yzlaOVDkY7+xqkDmvTPhn4HMxj1jV4v3Y+aCFh97/AGjWGPxNHDUXKrt27+R15JlWLzHFxp4XSS1v/L53PQ/C+oXGqaJb3t1bNbyyLkoe/v8AQ1p0AAAAAADoBRX5nUkpSbirLsf0lh6c6dKMJy5mkrvv5hRRRUGwUUUUAFFFFABRRRQAV4d+038CdO+JWkyavo8cdp4mtkzHIBhbkD+B/f0Ne40VpSqypSUovUTV9GfkTruk6joer3Ok6taS2d7ayGOaGVdrKRVKv0T/AGnfgXp/xK0h9Y0iOO18TWqZikAwLkD+B/6Gvz51zStQ0TVbjS9UtZbW8tnKSxSLgqRX1GExccRG636o55RcSlWp4V1/VvDGvWut6JeSWl9bOGjkQ4/A+oPpWXRXU0mrMk/RP4LftDeFvF3ge41HxDfW+lappkG+/idsBwP40HfJ7V8o/tJ/G/VPifrbWVk8tn4btn/0e2zgykfxv6n0HavGwSM4JGeuO9JXFRwFKlUc18vIpzbVgoooAJIAGSa7SQr6x/ZM/Z4k1J7Xxz44tClkMSWFhIuDL3Ejg/w+g70fsmfs8NqMtt448c2eLJCJLCwlX/WnqHcH+H0HevtFFVECIoVVGAAMACvFx+PtenTfqzWEOrEjRI41jjUIigBVAwAPSnUUV4ZsYXjLwzZeJNOaCcBLhQfJmA5U/wCFeBa/pV7ompSWF9GUkToccMOxHtX01WF4z8M2XiXTTBOAk6AmGYDlD/hXvZPnEsHL2dTWD/A+K4r4UhmsHXoK1Zf+TeT8+z+8+c80Zq5r2k3ui6jJY30ZSRTwezD1FUM1+gQlGcVKLumfiFWjOjN06is1umTwTSQTJNE7JIh3KynBBr2PwL8QrO701odbnSC5t0yZCcCUD+teK5pd1cWPy2ljoctTdbPqetkme4rJ6rqUHdPdPZ/8Mdb4/wDGFz4ivDFGxisIz+7j/ve5rls1HuozXTQw1PD01TpqyR5+OxlfHV5V68ryf9fcSZozTAa9P+GHgQzmPWdZhIjHzQQMPvf7R9qxxuMpYOk6lR/8E6MpyfEZriFQoL1fRLux/wAMvApmMesazFiP70EDfxf7R9vavWFAUAAAAcACgAAAAAAdBRX5xjsdVxtXnn8l2P3/ACXJcNlGHVGivV9W/wCugUUUVxHrhRRRQAUUUUAFFFFABRRRQAUUUUAFeH/tOfArT/iXpUmsaSsVp4mto/3UuMLcgDhH/oe1e4Vn+I9a0zw7otzrGsXkVpZWyF5ZZGwAB/WtaNSdOalDcTSa1PyZ1zStQ0TVrnStVtZLS8tnMcsUgwVIqlXp/wC0l8S7T4nePn1aw0q3srO3XyYJBGBNOoPDSHv7DtXmFfXU5SlBOSszlYUUUVYAAScDk19Zfsmfs7vqLW3jjx1ZFLMESafYSjmXuJHHZfQd6+VtLvJtO1K2v7cRtLbyrKgkQOpKnIyp4I46Gv0b/Zv+NOjfFDw+lrIIrDX7RAtzZ8AMMffj9V9u1efmVSrCn7m3Vl00m9T16NEjjWONVRFGFUDAA9KWiivmToCiiigAooooAwfGnhiy8S6cYJwEuEH7mYDlT/hXz7r+k32h6lJYahFskQ8Hsw9QfSvpjUr2106ykvLyZYYYxlmY18+/EXxa/ijU1aOMRWkGRCCBuPuT/Svr+GK2KcnTSvT/ACfl/kfmPiBhMvUI1m7Vn0XVefp0fy9Oa3UbqZmjNfa2PynlJM+9AOajzTo5GjkV0YqynIPoaVg5T1P4XeA/P8vWdaiIj+9BAw+9/tH29q9dAAAAAAHQVwPwx8dQ63Ammai6R6hGuFJ4Ew9vf2rvq/Ms5qYmeJaxGjWy6W8j+gOFcPl9LAReBd0931b8+3oFFFFeSfSBRRRQAUUUUAFFFFABRRRQAUUUUAFFFZ3iTW9L8OaJdazrN5HaWNqheWWQ4AH+NNJt2QB4l1vTPDmiXWs6xdx2llaxl5ZHOAAO3ufavzx/aT+N2qfFDWzZWbSWfhy2ci3tg2DKf77+p9B2pf2lfjdqfxQ1trGxeW08N2z/AOj2+cGYj/lo/v6DtXjVfQ4DAKkuefxfkYTnfRBRRQAScAZNeoZhRXv3w2/Zh8V+Lfh7deJprgadcvHv02zlT5rgdcsf4Qe1eHa5pWoaJq1zpWq2ktpeW0hjlikXDKwrOFanUk4xd2htNFKtTwrr+reGNdtta0S8ltL22cPHIhx07H1HtWXRWjSasxH6S/s4/GrSfijoYtp3jtfENqg+1WpON4/vp6j27V69X5IeFfEGr+F9etdc0O9ks7+1cPHKh/QjuD3Ffoj+zl8bNI+KWhLbztFZ+IraMfa7TON+Osieqn07V85j8A6L54fD+RvCd9GevUUUV5hoFVtTvrXTbGW9vJligiXczNRqd9aabYy3t7MsMEQyzMa+ffiN42uvE96YoS0OmxH91F3b/ab3r1spympmFSy0it3+i8z57iDiCjlFG71qPZfq/Id8RPGl14lvTFEzRafGf3cf97/aNclmmZozX6Xh8NTw1NU6askfheNxdbG1pV68ryY/NGTTM16R4J+GVxrGkSX+pSSWfmJ/oq45J7MR6Vni8ZRwkOes7I1y7K8TmNX2WHjd7/0zzvJozV3xDpF7oepyWF/EUkQ8HHDD1FZ+feuiEozipRd0zjq0Z0puE1ZrdE0E8sEyTQu0ciHKspwQa9z+GHjuLXIU0zUpFTUEXCseBKB/WvBs1JBPLBMk0LmORCGVlOCDXn5lllLH0uWWjWz7HsZFnlfJ6/tKesXuu6/z7M+tKK4D4X+PItdhTTNSdY9SQYVicCYDuPf2rv6/M8XhKuEqulVVmj92y7MaGYUFXoO6f4Ps/MKKKK5juCiiigAooooAKKKKACiig9KAM7xLrmleG9EudZ1q8jtLG2QvLK5wAPQep9q/PH9pP43ap8T9beyspJbTw3bSH7NbZx5pH8b+p9u1bH7YPxL8XeJPG114V1G0udH0jT5SIrNuDOR0kbs2e3pXgdfRZfgVTSqT3/IwnO+iCiigAkgAZJr1DMK+s/2TP2d31Brbxz46tNtmMSafp8i8y9/MkB6L0wO9J+yb+zwdSe28b+ObLFmpEljYSr/rT1DuP7voO9faCKqIERQqqMAAYAFeLj8fa9Om/VmsIdWEaJGixooVFGFUDAA9K8Q/ac+BWnfErSZNY0mOO08T2yZilAwtyAPuP/Q9q9worx6VWVKSlF6mrSasfkTrmlahomrXOlaray2l7bSGOaKRcMrCqVfon+098C7D4l6U+s6Skdr4mtY/3UmMC5UdI29/Q1+fOt6XqGi6rcaVqlrJa3ls5SWKQYKkV9RhMXHERut+qOeUeUpVqeFfEGreGNdttb0W7ktb22cNG6HH4H1FZdFdTSasyT9Jf2cPjVpXxR0Fbe4eK18Q2qD7Va7sbx/z0T1H8q9V1O+tdNsZb29mWGCJdzMxr8ovAOoeIdK8W2GoeF5podVhlDQtEefcH2PevsbxP4417xTp1jDq3lQGKFPOigJ2NLj5m59849K4KHD0sTX9x2h18vJHj55xDSyqhd61Hsv1fkaXxG8bXXie9MURaLTo2/dR5+9/tH3rkM1HmjNffYfC08NTVOmrJH4ljMXWxtaVau7yZJmgHmmAmvWfhN8PTceVrmuQkRDDW9uw+9/tN7Vjj8dSwNJ1ar9F3OjK8pr5nXVGivV9Eu7H/Cf4emcx63rkJEfDW9u4+9/tMPT2r2RQFAVQABwAKAAAAAAB0Aor8uzDMKuOq+0qfJdj9zyjKMPlVBUaK9X1bOf8b+FbHxRppt7gCO4UfuZwOUP9R7V86+IdHv8AQtTk0/UIikqHg9mHqD6V9VVz3jnwrY+KNMNvcKEuEBME2OUP+FenkmdywUvZVdab/A8PijheGZw9vQVqq/8AJvJ+fZnzLuo3Ve8Q6Pf6FqclhqERjkQ8Hsw9Qazs1+jwlGpFSi7pn4xVozpTcJqzW6J7eeSCZJoZGjkQhlZTgg17t8LvHsWuQppmpOseooMKxPEw/wAa8DzV3Q7fULvVIIdLWQ3ZcGMx8EH1z2rzs1y2jjaLVTRrZ9v+AezkGcYnK8SpUVzKWjj3/wCD2PrCiqOgx38Oj20eqTJNeLGBK6jAJq9X5VOPLJq9z98pyc4KTVr9H0CiiipLCiiigAooooAKKKKAPK/2hfg3o/xT8PFdsVprlupNnebec/3W9VNfnR4y8Naz4R8RXWg69ZvaXts5V1YcEdmB7g9jX6115V+0P8GtF+Knh05SO0161UmzvQuD/uP6qf0r08DjnRfJP4fyM5wvqj80RycCvrP9kz9ndtRNr458dWTLaAiTT9PlXBl9JJB/d7gd+tXv2bv2Y7yz19vEHxFs1VbKYi1sCQwlZTw7f7PcDvX2CiLGioihVUYAAwAK6cfmGns6T9WTCHVhGiRoqRqFRRgKBgAUtFFeGbBRRRQAV4h+038CtP8AiZpTatpCQ2fia2jPlS4wtyo52P8A0Pavb6K0pVZUpKUXqJq+jPyJ1zStQ0TVrnStVtJbS9tnMcsUgwVIqPS7C61O+isrKFpZ5W2qor9AP2r/AIM6J448OzeJLV7fTtesoyRO3yrcqP4H9/Q14F8OvBlp4XsvMkCTajKP3suM7f8AZX0H86+1yiDzFcy0S3Pms+zqllNLXWb2X6vyH/DnwZaeF7EPIEm1CUDzZcfd/wBke1dfmo80ua+0pUY0oqMVofjWLxNXF1XWrO8mSZozUea9N+B/hTTdau5NUv5opvsrjbak8k9mYelc+OxcMHQlWqbI1y3LqmYYmOHpbvuafwj+HhnMWva5DiMENb27D73ozD09q9oAAAAAAHQCkUBVCqAAOABS1+U5jmNXH1XUqfJdj9yyjKKGV0FRor1fVsKKKK4D1AooooA57xz4UsPFOmG3uFEdwgJgnA5Q/wBR7V84eI9Gv9A1SXT9RhMcqHg/wuOxB7ivrCud8d+E7DxTpZt51CXKDMEwHKn/AAr6LI88lgpeyq603+Hmv1Pj+J+GIZnB16KtVX/k3k/Psz5p0uxu9Tv4rGxheaeVtqIoyTX0T8OPBNp4XsRJKFm1GQfvZf7v+yvtR8OfBFn4UtDIxW41CUfvJsdB/dX0FdfWue568W3RoP3Pz/4BhwvwtHL0sTiVeq9l/L/wQooor5g+3CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKrarf2ml2E19fTpBbwrud2OAKTVdQtNLsJb6+mWGCJcszGvm34nePLzxXfmGEtDpkTfuos/f/wBpvevZyfJ6uZVbLSC3f6LzPBz3PaOVUbvWb2X6vyH/ABM8d3fiq/MULPDpsR/dRdN3+03vXF5pm6jdX6phsLTw1JUqSskfjGLxVbGVnWrO8mSZ96M0wGnSq8UhjkRkdeCrDBFb2OXlFzWn4b1y/wBA1SPUNPmMciHkdmHoR6Vk7hS596ipTjUi4zV0y6c50pqcHZrZn1X4C8W2HizShc27BLmMATwZ5Q/4V0dfIfhvXL/w/qseo6dMY5U6js47gj0r6Y8BeLtP8WaUtzbER3KAefATyh/qPevzPPshlgJe1pa03+Hk/wBGfr/DfEkcygqNbSqv/JvNefdHSUUUV80fWhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFVtVv7TS7CW+vp1ht4l3O7HpSatqNnpWny319OsMES7mZjXzP8UfH154tv8AyYWeDS4WPlQ5xvP95vU/yr2smyarmVWy0gt3+i8zws8zylldK71m9l+r8h/xP8eXfiy/MUJaHTIm/cxZ5b/ab3ris0zNGa/V8NhaWFpKlSVkj8bxeKrYuq61Z3kx+aX2pmTXsnwc+GrXBh8Qa/CViB3W9sw5b0Zh6e1YZhj6OAourVfourfY3y3LK+Y11Ror1fRLux3wc+GxuPK1/X4SIgQ1tbuPvf7TD09BXT/Fv4dQ6/bPqukxpFqca5KgYE4Hb6+9ekKAqhVAAHAApa/L62fYupi1ilKzWy6W7f5n63R4bwVPBPBuN0931v3/AMj4wuIZred4J42jlQlWVhgg0zNfRHxc+HUXiCB9W0mNI9TRcso4EwH9a+eLiKW2neCeNo5UO1kYYIPpX6TlOa0cypc8NJLddv8AgH5XnOS1srrck9YvZ9/+CJmtPw1rl/4f1WLUdPmMcqHkZ4Yeh9qys0Zr0qlONSLhNXTPKpznSmpwdmtmfWHgDxfp/i3SVubZhHcoAJ4CfmQ/1HvXSV8feGdd1Dw9q0WpabMY5UPIz8rr3UjuK+nPAHjDT/FulLcW7CO6QYngJ5Q/1FfmGfZBLAS9rS1pv8PJ/oz9c4c4jjmMFRraVV/5N5rz7o6WiiivmT6wKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAOY+JXhRfFvh5rAXDwTod8LA/KW9GHcV8s65pV/oupzadqNu0FxC2GVu/uPUV9m1x/xM8DWHi/TD8qw6jEP3E4HP+6fUV9Tw7n7wEvY1v4b/AAf+Xc+S4k4eWYR9vR/iL8V29ex8q5o+gq9ruk32i6nLp2oQNFPG2CCOvuPUV6v8Gvhk1w0PiDxDBiH71tbOPvejMPT2r9Cx2Z4fB4f283o9rdfQ/OMBlWIxuI+rwjZre/T1F+DPw0NwYfEPiCDEQ+a2tnH3vRmHp6Cvc1AVQqgADgAUKoVQqgAAYAHalr8lzPMq2Y1nVqv0XRI/YsryuhltBUqS9X1bCiiivOPSCvNfi98OYvEVu+raTEseqRrllHAnHoff3r0qiuvBY2tgqyq0nZr8fJnHjsDRx1F0ayun+Hmj4ruIZbad4J42ilQlWRhgg1Hmvoz4vfDiLxDA+raRGseqRjLIOBOP8a8AsdI1K91hdIgtJDetJ5ZiK4Knvn0xX6zleb0Mwoe1Ts1uu3/A8z8dzbJK+XYj2TV0/hff/g+Q3SbC81XUIbCwgee4mYKiKOpr6a+F3ga28IaaWkYTajOo8+Tsv+yvtTPhd4DtPCWnCWULNqcq/vZcfd/2V9q7avh+IeIHjG6FB/u1u+//AAD73hrhuOBSxGIV6j2X8v8AwQooor5M+xCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAxdd8LaHrd/a32pWKTT2rbo2Pf2PqK2VAVQqgAAYAHalorSVWc4qMm2lt5GcKNOEnKMUm9/MKKKKzNAooooAKKKKACs+HRdLh1iXV4rKFb6VQrzBfmIrQoqozlG/K7XJlCMrOSvYKKKKkoKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//2Q=="), - contactLink = Just adminContactReq, + contactLink = Just $ CLFull adminContactReq, preferences = Nothing } @@ -2208,7 +2261,7 @@ simplexStatusContactProfile = { displayName = "SimpleX-Status", fullName = "", image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAr6ADAAQAAAABAAAArwAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgArwCvAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQAC//aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Q/v4ooooAKKKKACiiigAoorE8R+ItF8J6Jc+IvEVwlrZ2iGSWWQ4CgVUISlJRirtmdatTo05VaslGMU223ZJLVtvokbdFfl3of/BRbS734rtpup2Ig8LSsIYrjnzkOcea3bafTqBX6cafqFjq1jFqemSrPbzqHjkQ5VlPIINetm2Q43LXD65T5eZXX+XquqPiuC/Efh/itYh5HiVUdGTjJWaflJJ6uEvsy2fqXKKKK8c+5Ciq17e2mnWkl/fyLDDCpd3c4VVHJJJr8c/2kf8Ago34q8M3mpTfByG3fT7CGSJZrlC3nStwJF5GFU8gd69LA5VicXTrVaMfdpxcpPokk397toj4LjvxKyLhGjRqZxValVkowhFc05O9m0tPdjfV7dN2kfq346+J3w9+GWlPrXxA1m00i1QZL3Uqxj8Mnn8K/Mj4tf8ABYD4DeEJ5dM+Gmn3niq4TIE0YEFtn/ffBI+imv51vHfxA8b/ABR1+bxT8RNUuNXvp3LtJcOWCk84VeigdgBXI18LXzupLSkrL72fzrxH9IXNsTKVPKKMaMOkpe/P8fdXpaXqfqvrf/BYH9p6+1w3+iafo1jZA8WrRPKSPeTcpz9BX1l8J/8Ags34PvxDp/xn8M3OmSnAe709hcQfUoSHA/A1/PtSE4/GuKGZ4mLvz39T4TL/ABe4swlZ1ljpTvvGaUo/dbT/ALdsf2rfCX9pT4HfHGzF18M/EdnqTYBaFXCzJn+9G2GH5V7nX8IOm6hqGkX8eraLcy2d3EcpPbuY5FPsykGv6gf+CWf7QPxB+OPwX1Ky+JF22pX3h69+yJdyf62WJlDrvPdlzjPevdwGae3l7OcbP8D+i/DTxm/1ixkcqx2H5K7TalF3jLlV2rPWLtqtWvM/T2iiivYP3c//0f7+KKKKACiiigAooooAK/Fv/goX8Qvi2fFcXgfWrRtP8NDEls0bZS7YfxORxlT0Xt1r9pK8u+L/AMI/Cfxp8F3HgvxbFujlGYpgB5kMg6Op9R+tfR8K5vQy3MYYnE01KK0843+0vNf8NZn5f4wcFZhxTwziMpy3FOjVeqSdo1Lf8u5u11GXk97Xuro/mBFyDX3t+yL+2Be/CW+h8B+OHafw7cyALIxJa0Ldx6p6jt1FfMvx/wDgR4w/Z+8YN4d8RoZrSbLWd4owk6D+TDuK8KF0K/pLFYHA51geWVp0pq6a/Brs1/wH2P8ALvJsz4h4D4h9tR5qGLoS5ZRls11jJbSjJferSi9mf1uafqFlqtlFqWmyrPBOoeORDlWU8gg069vrPTbSS/v5FhghUu7ucKqjqSa/CH9j79sm++EuoQ/D/wAeSNceHbmRVjlZstZk9x6p6jt2q3+15+2fffFS8n8AfD2V7bw9CxWWZThrwj+Se3evxB+G2Zf2n9TX8Lf2nTl/+S/u/PbU/v2P0nuGv9Vf7cf+9/D9Xv73tLd/+ffXn7afF7pqftbfth3nxUu5vAXgGR7fw/A5WWUHDXZX19E9B361+Z/xKm3eCL9R3UfzFbQul6Cn+I/A3ivxR8LPEXivSbVn07RoVkurg8Iu5gAue7HPSv1HOsrwmVcN4uhRSjBUp6vq3Fq7fVt/5I/gTNeI884x4kjmeYOVWtKSdop2hCPvWjFbQjFNv5ybbuz4Toqa0ge9uoLOIhWnkSNSxwAXIUEnsBnmv0+/aK/4Jg+O/gj8Hoviz4b1n/hJFt40l1G2ig2NDG4yZEIJ3KvfgHHNfxVTw9SpGUoK6W5+xZVw1mWZYfEYrA0XOFBKU2raJ31te72b0T0R+XRIAyegr+gr/glx+yZoHhjwBc/tKfFywiafUY2OmpeIGS3sVGWmIbgF+TkjhR71+YP7DX7Lt9+1H8ZLfR75WTw5pBS61ScDKsoIKwg+snf0Ffqd/wAFSv2o4Phf4Ltv2WvhmVtrjUbRBfvA2Ps1kOFhAHQyAc9ML9a9HL6UacHi6q0W3mz9Q8M8owuV4KvxpnEL0aN40Yv/AJeVXpp5LZPo7v7J+M/7U/jX4e/EL4/+JfFXwrsI9P0Ke5K26RKESTZw0oUcAOeQBX7J/wDBFU5+HPjYf9RWH/0SK/nqACgKOgr+hT/giouPh143b11SH/0SKWVzc8YpPrf8jHwexk8XxzSxVRJSn7WTSVknKMnoui7H7a0UUV9cf3Mf/9L+/iiiigAoorzX4wfGD4afAP4bav8AF74v6xbaD4d0K3e6vb26cJHHGgyevUnoAOSeBTjFyajFXYHpVFf55Xxt/wCDu34nj9vzS/G3wX0Qz/ArQ2ksLnSp1CXurQyMA15uPMTqBmJD2+914/uU/Y//AGxfgH+3P8ENL+P37OutxazoWpoNwHyzW02PmhmjPKSKeCD9RxXqY/JcXg4QqV4WUvw8n2ZnCrGTaTPqGiiivKNDy/4u/CLwd8afBtx4N8ZW4kilBMUoH7yGTs6HsR+tfzjftA/AXxl+z54yfw34jQzWkuXs7xF/dzR/0YdxX9OPiDxBofhPQ7vxN4mu4rDT7CF57m4ncJHFFGMszMcAAAZJNf53n/Bav/g5W1H4ufGjTvg5+xB5F14E8JX4l1HVriIE6xNE2GjhLDKQdRuGC55HHX9L8Os+x2ExP1eKcsO/iX8vmvPy6/ifg3jZ4NYDjDBPFUEqeYU17k/50vsT8n0lvF+V0fq0LhTUgnA4r4y/ZG/bJ+FX7YXw9HjDwBP5N/ahV1LTZeJrSUjoR3U/wsOK+sRdL/n/APXX9G0nCrBTpu6Z/mVmuSYvLcXUwOPpOnWg7SjJWaf9ap7NarQ+pf2dP2evGH7Q3i4aLogNvp1uQ15esMpEnoPVj2Ffrd+1V8GvDnw5/YU8X+APh/Z7IrewEjYGXlZGUs7nqSQM18C/sO/ti6b8F7o/Dnx6qpoN9LvS6RRvglbjL45ZT69vpX7wX1poHjjwxNYzbL3TdUt2jbaQySRSrg4PoQa/nnxXxGaTxLwmIjy4e3uW2lpu33Xbp87v+7Po58I8L4nhfFVMuqKeY1oTp1nJe9S5k0oxWtoPfmXxve1uVfwqKA0YHYiv6Ev+CZ37bVv490eP9mb4zXAn1GKJo9Murg5F3bgYMLk9XUcD+8tflR+1/wDsn+Nv2XfiNdadqFs8vh28md9Mv1GY3iJyEY9nXoQa+UrC/v8ASr+DVdJnktbq2dZYZomKvG6nIZSOhFfztQrVMJW1Xqu5+Z8PZ5mvBWeSc4NSg+WrTeinHqv1jL56ptP+s7xHZ/A//gnR8EfE/jTwra+RHqF5JdxWpbLTXcwwkSnrsGPwXNfyrfEDx54l+J/jXU/iB4wna51LVZ3nmdj3Y8KPQKOAPQV2vxX/AGhvjT8corC3+K2vz6vFpq7beNgERT3YqvBY92NeNVeOxirNRpq0Fsju8RePKWfTo4TLqPscFRXuU9F7z+KTSuvJK7srvqwr+ir/AIIuaVd2/wAH/FesSIRDd6uFjb+8Y41Dfka/BX4YfCzx78ZfGVr4C+G+nyajqV22Aqj5I17u7dFUdya/r+/ZV+Aenfs2fBLSPhbZyC4ntVaW7nAx5tzKd0jfTJwPYV1ZLQk63tbaI+w8AOHcXiM8ebcjVClGS5ujlJWUV3sm27baX3R9FUUUV9Uf2gf/0/7+KKKKACv4If8Ag8QT9vN9W8IsVk/4Z+WJedOL7f7Xyd32/HGNu3yc/LnPev73q84+Lnwj+G/x3+HGr/CT4uaRba74d123e1vbK6QPHJG4weD0I6gjkHkV6WUY9YLFQxDgpJdP8vMipDmi0f4W1frt/wAEhP8Agrt8af8AglD8b38V+Fo21zwPr7xp4i0B3KpcRoeJoTyEnjBO04+boeK+m/8AguZ/wQz+I3/BMD4kyfEn4Ww3fiD4Oa5KzWWolC76XKx4tbphwOuI3PDAc81/PdX7LCeFzHC3VpU5f18mjympU5eZ/t9fsk/tb/Av9tv4G6N+0F+z3rUWs6BrEQYFCPNt5cfPDMnVJEPDKf5V794h8Q6F4T0O78TeJ7uGw06wiae4uZ3EcUUaDLMzHAAA6k1/j9f8EiP+Cunxv/4JTfHAeKPCZfWfAuuyRx+IvD8jkRTxg486Lsk8YJ2n+Loa/V7/AILy/wDBxZd/t2eHl/Zc/Y6mu9I+Gl1DDNrWoSBoLvUpGAY2+OqQoeH/AL5GOlfneI4OxCxio0taT+12Xn59u53xxMeW73ND/g4M/wCDgzVP2yNV1H9jz9j3UZrD4ZWE7waxrEDlH110ONiEYItgQe/7z6V/I6AAMDgCgAKNo6Cv0j/4Jkf8Ex/j/wD8FOvj/Y/Cj4UWE9voFvNGdf18xk2um2pPzEt0MhGdiZyTX6FhsNhctwvLH3YR1bfXzfn/AEjhlKVSR77/AMEMf2Rf2v8A9qr9tPRrb9mNpdL0fSp438UaxKjNYW+nk/PHKOA7uoIjTrnniv7Lfj98CvG37PPjiXwj4uiLxNl7S7UYjuIuzD39R1Ffvt+wn+wd+z5/wTy+A+n/AAF/Z70pbKyt1V728cA3V/c4w0079WYnoOijgV7V8cPgb4G+Pngqfwb41twwYEwXCgebBJ2ZT/MdDXi5N4mTwmYWqRvhXpb7S/vL9V28z8c8YfBXC8XYL61hbQx9Ne7LpNfyT8v5ZfZfkfyXi5r9Lf2Jv24bn4S3UHwz+JkzT+HZ5AsNy5LNZlu3vHn8q+KPj38CPHf7PPjabwn4yt2ELMxtLsD91cRg8Mp6Z9R2rxAXAPANfuePyzL89y/2c7TpTV1JdOzT6Nf8Bn8C5FnGfcEZ79Yw96OJpPlnCS0a6xkusX/k4u9mf2IeK/B/w++Mngt9C8U2ltrWi6lEGCuA6OrDhlPY+hHNfztftw/8E4tN+AGlTfE34ba3HJo0koVdMvGC3CFv4Ym/5aAenBArvf2PP2+9R+CGmv4B+JSy6joEUbtaOp3TQOBkRj1Rjx7V8uftEftH+Nf2i/G7+KPEzmG0hyllZqT5cEef1Y9zX4LT8GMTisynhsY7UI6qot5J7Jefe+i87o/prxI8YuEM/wCF6WM+rc2ZSXKo6qVJrdykvih/Ktebsmnb4DkilicxyqVYdQRzXUaN4R1HVMSzjyIf7zDk/QV6dIlpJIJ5Y1Z16MRk1+qf7DX7Ed58ULmH4p/Fe2kt/D8Dq9paSDabwjncf+mf/oX0rKXg3lOR+0zDPMW6lCL92EVyufZN3vfyjbvdI/AeFsJnHFOPp5TktD97L4pP4YLrJu2iXnq3ok20es/8Erv2f/G/gf8AtD4ozj7Bo2pwiFIpY/3t2VOQ4J5VFzx659q/aKq9paWthax2VlGsUMShERBtVVHAAA6AVYr4LNcdTxWIdSjRjSpqyjGKslFber7t6tn+k3APB1LhjJaOUUqsqjjdylJ/FKTvJpfZV9orbzd2yiiivNPsj//U/v4ooooAKKKKAPO/iz8Jvh18c/h1q/wm+LGk2+ueHtdt3tb2yukDxyxuMEEHoR1B6g81/lm/8Fy/+CFfxG/4Jh/ENvid8J4bzxF8Htdmke1vliaRtHctxbXTAEBecRyHAbGDzX+q54j8R6B4Q0C88U+KbyHT9N0+F7i5ubhxHFFFGMszMcAADqa/zM/+Dhb/AIL06p+3f4rvP2Tf2Xr6S0+Eui3DR397GcHXriM8N7W6EfIP4jz6V9fwfPGLFctD+H9q+3/D9jmxKjy+9ufyq0UAY4or9ZPMP0v/AOCX3/BLf9oT/gqP8d4Phf8ACa0lsvDtjLG3iDxDJGTa6bbse56NKwB8uPOSfav9ZX9hD9hT4Df8E8v2fdK/Z7+AenLbWNkoe8vHUfab+6I+eeZhyWY9B0UcCv8AKC/4JUf8FV/j1/wSu+PCfEf4aSHUvC+rPHH4i0CViIL63U43D+7MgJKN+B4r/Wd/Yy/bM+BH7eHwH0j9oL9n7Vo9S0fU4182LI8+0nx88MydVdTxz16ivzbjZ43nipfwOlu/n59uh6GE5Labn1ZRRRXwB2Hi3x3+BPgj9oHwJceCPGcIIYFre4UfvYJezKf5jvX8vH7QvwB8d/s4eOZfB/jKEtDIS9neKP3VxFngqfX1Hav6gvj58e/An7PHgK48ceN7gLtBW2twf3txL2RR/M9hX8rX7Qn7Rnjz9o3x5L418ZyhUXKWlqh/dW8WeFUevqe5r988G4Zu3Ut/ueu/839z/wBu6fM/jj6UdPhlwo8y/wCFTS3Lb+H/ANPf/bPtf9unlQuAec077SPWueFznrTxc1+/eyP4udE/XX9g79h24+K8tv8AF74qQvD4fgkDWdo64N4V53H/AKZg/wDfX0r+ge0tLWwtY7KyjWKGJQiIgwqqOAAOwFfzc/sIft2XnwO1KH4ZfEeVp/Ct5L8k7Es9k7YHH/TMnkjt1r+kDTNT07WtOg1fSJ0ubW5QSRSxncjowyCCOoNfyr4q0s3jmreYfwtfZW+Hl/8Akv5r6/Kx/or9HSXDX+rqhkqtidPb81vac/d/3P5Lab/auXqKKK/Lz+gwooooA//V/v4ooooAKxfEniTQPB2gXnirxVew6dpunQvcXV1cOI4oYoxlndjgAADJJrar/PV/4Ozf+CiX7Xlr8Yrf9hCx0u98GfDaS0iv5L1GZT4iZs5HmKceTERgx9d3LcYr08py2eOxMaEXbu/L9SKk1CN2fIX/AAcD/wDBfrXv27vFF1+yx+ylqFzpnwl0id476+icxSa/MhwGOMEWykHYv8fU9hX8qoAAwOAKUAAYFfqj/wAEnf8AglH8cv8Agqp8ek+Hvw/R9M8I6NJFJ4k19lzHZW7k/ImeGmcAhF/E8V+xUKGFyzC2Xuwju/1fds8tuVSXmM/4JQ/8Epfjr/wVU+Pcfw5+HiPpXhPSXjl8ReIZEJhsoGP3E7PO4B2J+J4r7o/4Li/8EC/H3/BL/UYPjH8Hp7vxV8JNQMcL3sy7rnTLkgDbcFRjZI3KPwATg9q/0rP2MP2MPgL+wZ8BdI/Z5/Z60hNM0bS4x5kpANxeTn7887gAvI55JPToOK9y+J/ww8AfGfwBqvwu+KOlW+t6Brdu9re2V0gkilicYIIP6HqDXwVbjSu8YqlNfulpy9139e3Y7VhY8tnuf4VdfqD/AMErP+Cpvx1/4Jb/ALQNn8S/h7cS6j4VvpUj8QeH2kIt723zgsB0WVRyjetffn/BeH/ghJ4x/wCCZvjlvjP8EYbvXPg5rk7GKcqZJdGmc5FvOwH+rOcRyH0wea/nCr9ApVcNmOGuvehL+vk0cLUqcvM/24v2Mf20PgH+3l8CdK/aA/Z61iPVNI1FF86LI+0Wc+PnhnTqjqeOevUcV3nx/wD2gfh/+zp4CuPHHjq5CBQVtrZT+9uJeyIP5noBX+Ud/wAEL/25f2t/2NP2u7A/s7xPrPhzW5Yk8T6LOzCyls1PzTE9I5UXJRupPHIr+p39o79pXx/+0v8AEGbxv42l2RrlLO0QnyreLPCqPX1PUmvM4b8KauYZg5VJWwkdW/tP+6vPu+i8z8r8VvF3D8L4P6vhbTx017sekF/PL/21fafkjV/aF/aN8e/tHePZ/GvjOc+XuK2lopPlW8WeFUevqe9eFfasDmsL7UB1r9kv+Cen/BPuX4mPa/Gv41Wrw6HE4k0/T5FwbsjkO4PPl56D+L6V/QWbZjlnDmW+1q2hSgrRit2+kYrq/wDh2fw9kXDmdcZ526NK9SvUfNOctorrKT6JdF6JIh/Yq/4JyXXxq8MSfEn4wtPpukXkLLp1vH8s0hYcTHPRR1Ud6+KP2nP2bvHX7MXj+Twl4pUz2U+Xsb5QRHcRZ/Rh/Etf2D2trbWNtHZ2caxRRKEREGFVRwAAOgFeSfHL4G+Af2gvAVz4A8f2wmt5huimUDzYJB0dD2I/Wv5/yrxgx0c3niMcr4abtyL7C6OPdrr/ADeWlv604g+jdlFTh6ngsrfLjaauqj/5eS6xn2i/s2+Hz1v/ABi+d3r9O/2DP28r/wCBGpRfDT4lSvdeFL2UBJmYs9izcZX1j7kduor48/ah/Zr8bfsu/EWTwZ4pHn2c4MtheqMJcQ5IB9mHRhXzd9oAFf0Djsuy3iHLeSdqlGorpr8Gn0a/4DW6P5DyrMc74Mzz2tG9LE0XaUXs11jJdYv/ACaezP7pdK1bTNd02DWdGnS6tLlBJFLEwZHRuQQR1FaFfix/wSG1n47X3hPVLHXUL+BoT/oEtxneLjPzLD6pjr2B6d6/aev424nyP+yMyrZf7RT5Huvv17NdV0Z/pTwPxP8A6w5Lh82dGVJ1FrGXdaNp9YveL6oKKKK8A+sP/9b+/iiiigAr4E/4KI/8E4f2b/8AgpZ8DLr4M/H7SklljV5NJ1aJQLzTblhxLC/Uc43L0YcGvvuitKNadKaqU3aS2Ymk1Zn+Vt8Nf+DZH9vDxJ/wUEn/AGQfGti+m+DdMkF5eeNlTNjLpRb5Xgz964cfL5XVWyTx1/0lv2L/ANif9nv9gn4H6b8Bv2dNDh0jSrFF8+YKDcXs4GGmuJOskjHPJ6dBxX1lgZz3pa9bNc+xWPjGFV2iui6vu/60M6dKMNgooorxTU4T4m/DHwB8ZfAeqfDH4paRba7oGtQPbXtjeRiWGaJxghlII/wr/M//AOCw/wDwbq/En9kb9o7Ttc/ZhQ6h8KvGl4VgknkUyaJIxy0UmTueMDmNgCexr/SN/aA/aA+Hf7N3w6u/iL8RbtYYIFIggBHm3Ev8Mca9yfyA5NfyB/tTftZfEX9qv4gSeL/GEv2exgLJYWEZPlW8WeOO7H+Ju9fsXhRwnmOZYl4hNwwi+Jv7T/lj5930Xnofj3iv4nYThrCPD0bTxs17kekV/PPy7L7T8rn58fs1fs1/Df8AZg8Dp4U8CwB7qYK19fuAZrmQDkseyjsvQV9GfaWrAWcjvUnnt6mv62w+Cp0KapUo2itkfwFmOLxWPxNTGYyo51Zu8pN6t/1stktEftx/wTa/YHsfi6sHx2+L8aT6BFJnT7DcGFy6dWlAzhQf4T171/SBaWltY20dlZRrFDEoREQYVVHAAA6AV/Hv+xJ+3N4y/ZO8Wi0ui+oeE9QkX7dYk5KdjLFzw49Ohr+tj4c/Efwb8WPB1l498A30eoaZqEYkiljOevVWHZh0IPIr+TPGXLs6p5p9Zxz5sO9KbXwxX8rXSXd/a3Wmi/t76P8AmHD08l+qZZDkxUdaydueT/mT0vDsl8Oz1d33FFFFfjR/QB4x8dPgN8O/2hvA1x4F+Idms8MgJhmAxLbydnjbqCP1r8RPg3/wSV8Z/wDC9r7T/izMreDNIlEkM8TYfUVPKpgcoAPv+/Ar+iKivrsh43zbKMLWweCq2hUXXXlf80eza0/HdJnwPFHhpkHEGOw+YZlQ5qlJ7rTnXSM/5op6/hs2jD8NeGdA8HaHbeGvC9nFYWFmgjhghUIiKOwArcoor5Oc5Tk5Sd292fd06cacVCCtFaJLZLsgoooqSz//1/7+KKKKACiiigAooooAK8J/aK/aG+H37M/wzvPiX8QrgRwwDbb26kebczH7saDuSep7DmvdW3bTt69s1/Hj/wAFS9c/acu/2hbiw+Psf2fTYWf+w47bd9ha2zw0ZPWQj7+eQfav0Dw44PpcRZssLXqqFOK5pK9pSS6RXfu+i1PzvxN4zrcN5PLF4ei51JPli7XjFv7U327Lq9Dwr9qv9rn4lftZ+Pv+Ev8AG8i29na7ksNPiJ8m2iJ7Ak5Y/wATHrXy/wDacDJNYfn45PFftR/wTX/4Ju6j8aryz+OXxttpLXwtbSrJY2Mi7W1Bl53MD0hB/wC+vpX9jZpmGU8LZT7WolTo01aMVu30jFdW/wDNvqz+HcryTOeLs4dODdSvUd5Tlsl1lJ9Eui9Elsix/wAE8/8Agmpc/Hq3HxZ+OcFxY+F8f6Daj93Jen++eMiMdum76V88ft4fsM+LP2RvGH9p6MJtS8G6gxNnfMMmFj/yxmIAAYfwnuPev7DbGxs9Ms4tP0+JYIIFCRxoNqqq8AADoBXL+P8AwB4R+KHhG+8C+OrGPUNM1CMxTQyjIIPcehHUEdDX8x4PxqzWOdvH11fDS0dJbKPRp/zrdvrtta39V47wCyWeQRy7D6YqOqrPeUuqkv5Hsl9ndXd7/wACwuGHevvT9iL9u7x1+yP4n+wMDqXhPUJVN/YMTlOxlh/uuB+BqH9vD9hXxl+yD4v/ALS03zNT8HajIfsV8VyYSf8AljNjgMOx/iHvX59C6bHav6fjDKeJsqurVcPVX9ecZRfzTP5LdLOeE850vRxNJ/15SjJfJo/v3+GnxJ8HfF3wRp/xC8BXiX2l6lEJYZEPr1Vh2YdCDyDXd1/PD/wRa8KftJW8moeKfPNp8N7kMBBdKT9ouR/Hbgn5QP4m6Gv6Hq/iHjXh6lkmb1svoVlUjF6Nbq/2ZdOZdbfhsf6AcC8SVs9yahmWIoOlOS1T2dvtR68r3V/x3ZRRRXyh9eFFFFABRRRQB//Q/v4ooooAKKKKACiiigAr5u/aj/Zg+HX7VvwyuPh14+i2N/rLO8jA861mHR0Pp2YdCOK+kaK6sDjq+DxEMVhZuFSDumt00cmOwOHxuHnhcVBTpzVpJ7NM/nF/ZW/4I2eINL+MV9rH7Rk0Vz4d0G5H2GCA8anjlXfuiDjcvJJ46V/RfY2FlpdlFpumxJBbwII444wFVEUYAAHAAFW6K9/injHM+Ia8a+Y1L8qsorSK7tLu3q3+iSPn+E+C8q4dw86GW07czvJvWT7Jvstkv1bYUUUV8sfVnEfEb4c+Dvix4Mv/AAB49sY9Q0vUYjFNDIMjB7j0YdQRyDX4HeH/APgiNJB+0LKNe1vzvhzARcxBeLyUEn/R27ADu46jtmv6KKK+r4d42zjI6Vajl1ZxjUVmt7P+aN9pW0uv0R8lxJwNk2e1aFfMqCnKk7p7XX8srbxvrZ/qzn/CnhXw/wCCPDll4R8K2sdlp2nQrBbwRDCoiDAAFdBRRXy05ynJzm7t6tvqfVwhGEVCCsloktkgoooqSgooooAKKKKAP//R/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z"), - contactLink = Just (either error id $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"), + contactLink = Just (either error CLFull $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"), preferences = Nothing } @@ -2218,5 +2271,5 @@ timeItToView s action = do a <- action t2 <- liftIO getCurrentTime let diff = diffToMilliseconds $ diffUTCTime t2 t1 - toView' $ CRTimedAction s diff + toView' $ CEvtTimedAction s diff pure a diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 739ab25ea9..d60faef639 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -90,20 +90,20 @@ smallGroupsRcptsMemLimit = 20 processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessage _ _ (DEL_RCVQS delQs) = - toView $ CRAgentRcvQueuesDeleted $ L.map rcvQ delQs + toView $ CEvtAgentRcvQueuesDeleted $ L.map rcvQ delQs where rcvQ (connId, server, rcvId, err_) = DeletedRcvQueue (AgentConnId connId) server (AgentQueueId rcvId) err_ processAgentMessage _ _ (DEL_CONNS connIds) = - toView $ CRAgentConnsDeleted $ L.map AgentConnId connIds + toView $ CEvtAgentConnsDeleted $ L.map AgentConnId connIds processAgentMessage _ "" (ERR e) = - toView $ CRChatError Nothing $ ChatErrorAgent e Nothing + eToView $ ChatErrorAgent e Nothing processAgentMessage corrId connId msg = do lockEntity <- critical (withStore (`getChatLockEntity` AgentConnId connId)) withEntityLock "processAgentMessage" lockEntity $ do vr <- chatVersionRange -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case - Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user)) + Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` eToView _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) -- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. @@ -121,22 +121,22 @@ critical a = processAgentMessageNoConn :: AEvent 'AENone -> CM () processAgentMessageNoConn = \case - CONNECT p h -> hostEvent $ CRHostConnected p h - DISCONNECT p h -> hostEvent $ CRHostDisconnected p h - DOWN srv conns -> serverEvent srv conns NSDisconnected CRContactsDisconnected - UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed - SUSPENDED -> toView CRChatSuspended - DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId + CONNECT p h -> hostEvent $ CEvtHostConnected p h + DISCONNECT p h -> hostEvent $ CEvtHostDisconnected p h + DOWN srv conns -> serverEvent srv conns NSDisconnected CEvtContactsDisconnected + UP srv conns -> serverEvent srv conns NSConnected CEvtContactsSubscribed + SUSPENDED -> toView CEvtChatSuspended + DEL_USER agentUserId -> toView $ CEvtAgentUserDeleted agentUserId ERRS cErrs -> errsEvent cErrs where - hostEvent :: ChatResponse -> CM () + hostEvent :: ChatEvent -> CM () hostEvent = whenM (asks $ hostEvents . config) . toView serverEvent srv conns nsStatus event = do chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds ifM (asks $ coreApi . config) (notifyAPI connIds) notifyCLI where connIds = map AgentConnId conns - notifyAPI = toView . CRNetworkStatus nsStatus + notifyAPI = toView . CEvtNetworkStatus nsStatus notifyCLI = do cs <- withStore' (`getConnectionsContacts` conns) toView $ event srv cs @@ -144,7 +144,7 @@ processAgentMessageNoConn = \case errsEvent cErrs = do vr <- chatVersionRange errs <- lift $ rights <$> withStoreBatch' (\db -> map (getChatErr vr db) cErrs) - toView $ CRChatErrors Nothing errs + toView $ CEvtChatErrors errs where getChatErr :: VersionRangeChat -> DB.Connection -> (ConnId, AgentErrorType) -> IO ChatError getChatErr vr db (connId, err) = @@ -156,7 +156,7 @@ processAgentMsgSndFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId) withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $ withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case - Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) + Just user -> process user fileId `catchChatError` eToView _ -> do lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId @@ -176,7 +176,7 @@ processAgentMsgSndFile _corrId aFileId msg = do ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId status lookupChatItemByFileId db vr user fileId - toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal + toView $ CEvtSndFileProgressXFTP user ci ft sndProgress sndTotal SFDONE sndDescr rfds -> do withStore' $ \db -> setSndFTPrivateSndDescr db user fileId (fileDescrText sndDescr) ci <- withStore $ \db -> lookupChatItemByFileId db vr user fileId @@ -188,18 +188,18 @@ processAgentMsgSndFile _corrId aFileId msg = do [] -> sendFileError (FileErrOther "no receiver descriptions") "no receiver descriptions" vr ft rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of [] -> case xftpRedirectFor of - Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft + Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CEvtSndFileRedirectStartXFTP user ft Just _ -> sendFileError (FileErrOther "chaining redirects") "Prohibit chaining redirects" vr ft rfds' -> do -- we have 1 chunk - use it as URI whether it is redirect or not ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor - toView $ CRSndStandaloneFileComplete user ft' $ map (decodeLatin1 . strEncode . FD.fileDescriptionURI) rfds' + toView $ CEvtSndStandaloneFileComplete user ft' $ map (decodeLatin1 . strEncode . FD.fileDescriptionURI) rfds' Just (AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) -> case (msgId_, itemDeleted) of (Just sharedMsgId, Nothing) -> do when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send" -- TODO either update database status or move to SFPROG - toView $ CRSndFileProgressXFTP user ci ft 1 1 + toView $ CEvtSndFileProgressXFTP user ci ft 1 1 case (rfds, sfts, d, cInfo) of (rfd : extraRFDs, sft : _, SMDSnd, DirectChat ct) -> do withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs) @@ -208,9 +208,9 @@ processAgentMsgSndFile _corrId aFileId msg = do Just rs -> case L.last rs of Right ([msgDeliveryId], _) -> withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId - Right (deliveryIds, _) -> toView $ CRChatError (Just user) $ ChatError $ CEInternalError $ "SFDONE, sendFileDescriptions: expected 1 delivery id, got " <> show (length deliveryIds) - Left e -> toView $ CRChatError (Just user) e - Nothing -> toView $ CRChatError (Just user) $ ChatError $ CEInternalError "SFDONE, sendFileDescriptions: expected at least 1 result" + Right (deliveryIds, _) -> eToView $ ChatError $ CEInternalError $ "SFDONE, sendFileDescriptions: expected 1 delivery id, got " <> show (length deliveryIds) + Left e -> eToView e + Nothing -> eToView $ ChatError $ CEInternalError "SFDONE, sendFileDescriptions: expected at least 1 result" lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) (_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do ms <- withStore' $ \db -> getGroupMembers db vr user g @@ -223,7 +223,7 @@ processAgentMsgSndFile _corrId aFileId msg = do liftIO $ updateCIFileStatus db user fileId CIFSSndComplete getChatItemByFileId db vr user fileId lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) - toView $ CRSndFileCompleteXFTP user ci' ft + toView $ CEvtSndFileCompleteXFTP user ci' ft where memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)] memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts') @@ -244,7 +244,7 @@ processAgentMsgSndFile _corrId aFileId msg = do ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId (CIFSSndWarning $ agentFileError e) lookupChatItemByFileId db vr user fileId - toView $ CRSndFileWarning user ci ft err + toView $ CEvtSndFileWarning user ci ft err SFERR e -> sendFileError (agentFileError e) (tshow e) vr ft where @@ -259,7 +259,7 @@ processAgentMsgSndFile _corrId aFileId msg = do let (errs, msgReqs) = partitionEithers . L.toList $ L.zipWith (fmap . toMsgReq) connsIdsEvts sndMsgs_ delivered <- mapM deliverMessages (L.nonEmpty msgReqs) let errs' = errs <> maybe [] (lefts . L.toList) delivered - unless (null errs') $ toView $ CRChatErrors (Just user) errs' + unless (null errs') $ toView $ CEvtChatErrors errs' pure delivered where connDescrEvents :: Int -> NonEmpty (Connection, (ConnOrGroupId, ChatMsgEvent 'Json)) @@ -278,7 +278,7 @@ processAgentMsgSndFile _corrId aFileId msg = do liftIO $ updateFileCancelled db user fileId (CIFSSndError ferr) lookupChatItemByFileId db vr user fileId lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) - toView $ CRSndFileError user ci ft err + toView $ CEvtSndFileError user ci ft err agentFileError :: AgentErrorType -> FileError agentFileError = \case @@ -298,7 +298,7 @@ processAgentMsgRcvFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $ withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case - Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) + Just user -> process user fileId `catchChatError` eToView _ -> do lift $ withAgent' (`xftpDeleteRcvFile` aFileId) throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId @@ -318,7 +318,7 @@ processAgentMsgRcvFile _corrId aFileId msg = do ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId status lookupChatItemByFileId db vr user fileId - toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal ft + toView $ CEvtRcvFileProgressXFTP user ci rcvProgress rcvTotal ft RFDONE xftpPath -> case liveRcvFileTransferPath ft of Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file" @@ -331,25 +331,25 @@ processAgentMsgRcvFile _corrId aFileId msg = do updateCIFileStatus db user fileId CIFSRcvComplete lookupChatItemByFileId db vr user fileId agentXFTPDeleteRcvFile aFileId fileId - toView $ maybe (CRRcvStandaloneFileComplete user fsTargetPath ft) (CRRcvFileComplete user) ci_ + toView $ maybe (CEvtRcvStandaloneFileComplete user fsTargetPath ft) (CEvtRcvFileComplete user) ci_ RFWARN e -> do ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId (CIFSRcvWarning $ agentFileError e) lookupChatItemByFileId db vr user fileId - toView $ CRRcvFileWarning user ci e ft + toView $ CEvtRcvFileWarning user ci e ft RFERR e | e == FILE NOT_APPROVED -> do aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted forM_ aci_ cleanupACIFile agentXFTPDeleteRcvFile aFileId fileId - forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci + forM_ aci_ $ \aci -> toView $ CEvtChatItemUpdated user aci | otherwise -> do aci_ <- withStore $ \db -> do liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ agentFileError e) lookupChatItemByFileId db vr user fileId forM_ aci_ cleanupACIFile agentXFTPDeleteRcvFile aFileId fileId - toView $ CRRcvFileError user aci_ e ft + toView $ CEvtRcvFileError user aci_ e ft processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do @@ -360,9 +360,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = entity <- critical $ withStore (\db -> getConnectionEntity db vr user $ AgentConnId agentConnId) >>= updateConnStatus case agentMessage of END -> case entity of - RcvDirectMsgConnection _ (Just ct) -> toView $ CRContactAnotherClient user ct - _ -> toView $ CRSubscriptionEnd user entity - MSGNTF msgId msgTs_ -> toView $ CRNtfMessage user entity $ ntfMsgAckInfo msgId msgTs_ + RcvDirectMsgConnection _ (Just ct) -> toView $ CEvtContactAnotherClient user ct + _ -> toView $ CEvtSubscriptionEnd user entity + MSGNTF msgId msgTs_ -> toView $ CEvtNtfMessage user entity $ ntfMsgAckInfo msgId msgTs_ _ -> case entity of RcvDirectMsgConnection conn contact_ -> processDirectMessage agentMessage entity conn contact_ @@ -438,13 +438,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = MWARN _ err -> processConnMWARN connEntity conn err MERR _ err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) processConnMERR connEntity conn err MERRS _ err -> do -- error cannot be AUTH error here - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -468,11 +468,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure () forM_ aChatMsgs $ \case Right (ACMsg _ chatMsg) -> - processEvent ct' conn' tags eInfo chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e + processEvent ct' conn' tags eInfo chatMsg `catchChatError` \e -> eToView e Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e - toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) + eToView (ChatError . CEException $ "error parsing chat message: " <> e) checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent where aChatMsgs = parseChatMessages msgBody @@ -543,7 +543,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing True allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend - toView $ CRBusinessLinkConnecting user gInfo host ct + toView $ CEvtBusinessLinkConnecting user gInfo host ct _ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info" INFO pqSupport connInfo -> do processINFOpqSupport conn pqSupport @@ -567,7 +567,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [incognito] print incognito profile used for this contact incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) lift $ setContactNetworkStatus ct' NSConnected - toView $ CRContactConnected user ct' (fmap fromLocalProfile incognitoProfile) + toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile) when (directOrUsed ct') $ do createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo pqEnc) Nothing createFeatureEnabledItems ct' @@ -601,11 +601,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = cis <- updateDirectItemsStatus' db ct conn msgId (CISSndSent SSPComplete) liftIO $ forM cis $ \ci -> setDirectSndChatItemViaProxy db user ct ci (isJust proxy) let acis = map ctItem cis - unless (null acis) $ toView $ CRChatItemsStatusesUpdated user acis + unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis where ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct) SWITCH qd phase cStats -> do - toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats) + toView $ CEvtContactSwitch user ct (SwitchProgress qd phase cStats) when (phase == SPStarted || phase == SPCompleted) $ case qd of QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing @@ -628,12 +628,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = . mapM (\(ci, content') -> updateDirectChatItem' db user contactId ci content' False False Nothing Nothing) . mdeUpdatedCI e case ci_ of - Just ci -> toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + Just ci -> toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) _ -> do - toView $ CRContactRatchetSync user ct (RatchetSyncProgress rss cStats) + toView $ CEvtContactRatchetSync user ct (RatchetSyncProgress rss cStats) createInternalChatItem user (CDDirectRcv ct) (CIRcvDecryptionError mde n) Nothing ratchetSyncEventItem ct' = do - toView $ CRContactRatchetSync user ct' (RatchetSyncProgress rss cStats) + toView $ CEvtContactRatchetSync user ct' (RatchetSyncProgress rss cStats) createInternalChatItem user (CDDirectRcv ct') (CIRcvConnEvent $ RCERatchetSync rss) Nothing OK -> -- [async agent commands] continuation on receiving OK @@ -643,7 +643,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (directOrUsed ct && sqSecured) $ do lift $ setContactNetworkStatus ct NSConnected - toView $ CRContactSndReady user ct + toView $ CEvtContactSndReady user ct forM_ viaUserContactLink $ \userContactLinkId -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId let (UserContactLink {autoAccept}, _) = ucl @@ -655,14 +655,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processConnMWARN connEntity conn err MERR msgId err -> do updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err) - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) processConnMERR connEntity conn err MERRS msgIds err -> do -- error cannot be AUTH error here updateDirectItemsStatusMsgs ct conn (L.toList msgIds) (CISSndError $ agentSndError err) - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -671,7 +671,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just AutoAccept {autoReply = Just mc} -> do (msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) - toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] _ -> pure () processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () @@ -698,7 +698,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> setNewContactMemberConnRequest db user m cReq groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo sendGrpInvitation ct m groupLinkId - toView $ CRSentGroupInvitation user gInfo ct m + toView $ CEvtSentGroupInvitation user gInfo ct m where sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> CM () sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do @@ -757,7 +757,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XInfo _ -> -- TODO Keep rejected member to allow them to appeal against rejection. when (memberStatus m == GSMemRejected) $ do - deleteMemberConnection' user m True + deleteMemberConnection' m True withStore' $ \db -> deleteGroupMember db user m XOk -> pure () _ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok" @@ -776,7 +776,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ chatHasNtfs chatSettings case memberCategory m of GCHostMember -> do - toView $ CRUserJoinedGroup user gInfo {membership = membership {memberStatus = status'}} m {memberStatus = status'} + toView $ CEvtUserJoinedGroup user gInfo {membership = membership {memberStatus = status'}} m {memberStatus = status'} let cd = CDGroupRcv gInfo m createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing createGroupFeatureItems user cd CIRcvGroupFeature gInfo @@ -787,7 +787,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = expectHistory = groupFeatureAllowed SGFHistory gInfo && m `supportsVersion` groupHistoryIncludeWelcomeVersion GCInviteeMember -> do memberConnectedChatItem gInfo m - toView $ CRJoinedGroupMember user gInfo m {memberStatus = status'} + toView $ CEvtJoinedGroupMember user gInfo m {memberStatus = status'} let Connection {viaUserContactLink} = conn when (isJust viaUserContactLink && isNothing (memberContactId m)) sendXGrpLinkMem when (connChatVersion < batchSend2Version) sendGroupAutoReply @@ -831,12 +831,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () forM_ aChatMsgs $ \case Right (ACMsg _ chatMsg) -> - processEvent tags eInfo chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e + processEvent tags eInfo chatMsg `catchChatError` \e -> eToView e Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "group msg=error " <> eInfo <> " " <> tshow e - toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) - forwardMsgs (rights aChatMsgs) `catchChatError` (toView . CRChatError (Just user)) + eToView (ChatError . CEException $ "error parsing chat message: " <> e) + forwardMsgs (rights aChatMsgs) `catchChatError` eToView checkSendRcpt $ rights aChatMsgs where aChatMsgs = parseChatMessages msgBody @@ -867,7 +867,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg brokerTs XGrpMemRestrict memId memRestrictions -> xGrpMemRestrict gInfo m' memId memRestrictions msg brokerTs XGrpMemCon memId -> xGrpMemCon gInfo m' memId - XGrpMemDel memId -> xGrpMemDel gInfo m' memId msg brokerTs + XGrpMemDel memId withMessages -> xGrpMemDel gInfo m' memId withMessages msg brokerTs XGrpLeave -> xGrpLeave gInfo m' msg brokerTs XGrpDel -> xGrpDel gInfo m' msg brokerTs XGrpInfo p' -> xGrpInfo gInfo m' p' msg brokerTs @@ -918,7 +918,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupItemsStatus gInfo m conn msgId GSSSent (Just $ isJust proxy) when continued $ sendPendingGroupMessages user m conn SWITCH qd phase cStats -> do - toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) + toView $ CEvtGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) when (phase == SPStarted || phase == SPCompleted) $ case qd of QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing @@ -930,7 +930,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> setConnectionVerified db user connId Nothing let m' = m {activeConn = Just (conn {connectionCode = Nothing} :: Connection)} :: GroupMember ratchetSyncEventItem m' - toView $ CRGroupMemberVerificationReset user gInfo m' + toViewTE $ TEGroupMemberVerificationReset user gInfo m' createInternalChatItem user (CDGroupRcv gInfo m') (CIRcvConnEvent RCEVerificationCodeReset) Nothing _ -> ratchetSyncEventItem m where @@ -942,12 +942,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = . mapM (\(ci, content') -> updateGroupChatItem db user groupId ci content' False False Nothing) . mdeUpdatedCI e case ci_ of - Just ci -> toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) + Just ci -> toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) _ -> do - toView $ CRGroupMemberRatchetSync user gInfo m (RatchetSyncProgress rss cStats) + toView $ CEvtGroupMemberRatchetSync user gInfo m (RatchetSyncProgress rss cStats) createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvDecryptionError mde n) Nothing ratchetSyncEventItem m' = do - toView $ CRGroupMemberRatchetSync user gInfo m' (RatchetSyncProgress rss cStats) + toView $ CEvtGroupMemberRatchetSync user gInfo m' (RatchetSyncProgress rss cStats) createInternalChatItem user (CDGroupRcv gInfo m') (CIRcvConnEvent $ RCERatchetSync rss) Nothing OK -> -- [async agent commands] continuation on receiving OK @@ -965,16 +965,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = MERR msgId err -> do withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSError $ agentSndError err) -- group errors are silenced to reduce load on UI event log - -- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + -- eToView (ChatErrorAgent err $ Just connEntity) processConnMERR connEntity conn err MERRS msgIds err -> do let newStatus = GSSError $ agentSndError err -- error cannot be AUTH error here withStore' $ \db -> forM_ msgIds $ \msgId -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure () - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -1000,7 +1000,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = msg <- sendGroupMessage' user gInfo [m] (XMsgNew $ MCSimple (extMsgContent mc Nothing)) ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc) withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew - toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32) agentMsgDecryptError = \case @@ -1045,13 +1045,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci <- withStore $ \db -> do liftIO $ updateSndFileStatus db ft FSConnected updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 - toView $ CRSndFileStart user ci ft + toView $ CEvtSndFileStart user ci ft sendFileChunk user ft SENT msgId _proxy -> do withStore' $ \db -> updateSndFileChunkSent db ft msgId unless (fileStatus == FSCancelled) $ sendFileChunk user ft MERR _ err -> do - cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user) + cancelSndFileTransfer user ft True >>= mapM_ deleteAgentConnectionAsync case err of SMP _ SMP.AUTH -> unless (fileStatus == FSCancelled) $ do ci <- withStore $ \db -> do @@ -1059,7 +1059,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just (ChatRef CTDirect _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled _ -> pure () lookupChatItemByFileId db vr user fileId - toView $ CRSndFileRcvCancelled user ci ft + toView $ CEvtSndFileRcvCancelled user ci ft _ -> throwChatError $ CEFileSend fileId err MSG meta _ _ -> withAckMessage' "file msg" agentConnId meta $ pure () @@ -1070,7 +1070,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -1119,10 +1119,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () MERR _ err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) processConnMERR connEntity conn err ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -1131,9 +1131,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case FileChunkCancel -> unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) + cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync ci <- withStore $ \db -> getChatItemByFileId db vr user fileId - toView $ CRRcvFileSndCancelled user ci ft + toView $ CEvtRcvFileSndCancelled user ci ft FileChunk {chunkNo, chunkBytes = chunk} -> do case integrity of MsgOk -> pure () @@ -1156,8 +1156,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateCIFileStatus db user fileId CIFSRcvComplete deleteRcvFileChunks db ft getChatItemByFileId db vr user fileId - toView $ CRRcvFileComplete user ci - forM_ conn_ $ \conn -> deleteAgentConnectionAsync user (aConnId conn) + toView $ CEvtRcvFileComplete user ci + mapM_ (deleteAgentConnectionAsync . aConnId) conn_ RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo @@ -1171,10 +1171,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO show/log error, other events in contact request _ -> pure () MERR _ err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) processConnMERR connEntity conn err ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) + eToView (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -1182,12 +1182,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> CM () profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ reqPQSup = do withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case - CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact - CORGroup gInfo -> toView $ CRBusinessRequestAlreadyAccepted user gInfo + CORContact contact -> toView $ CEvtContactRequestAlreadyAccepted user contact + CORGroup gInfo -> toView $ CEvtBusinessRequestAlreadyAccepted user gInfo CORRequest cReq -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let (UserContactLink {connReqContact, autoAccept}, gLinkInfo_) = ucl - isSimplexTeam = sameConnReqContact connReqContact adminContactReq + let (UserContactLink {connLinkContact = CCLink connReq _, autoAccept}, gLinkInfo_) = ucl + isSimplexTeam = sameConnReqContact connReq adminContactReq v = maxVersion chatVRange case autoAccept of Just AutoAccept {acceptIncognito, businessAddress} @@ -1195,16 +1195,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = if isSimplexTeam && v < businessChatsVersion then do ct <- acceptContactRequestAsync user cReq Nothing reqPQSup - toView $ CRAcceptingContactRequest user ct + toView $ CEvtAcceptingContactRequest user ct else do gInfo <- acceptBusinessJoinRequestAsync user cReq - toView $ CRAcceptingBusinessRequest user gInfo + toView $ CEvtAcceptingBusinessRequest user gInfo | otherwise -> case gLinkInfo_ of Nothing -> do -- [incognito] generate profile to send, create connection with incognito profile incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing ct <- acceptContactRequestAsync user cReq incognitoProfile reqPQSup - toView $ CRAcceptingContactRequest user ct + toView $ CEvtAcceptingContactRequest user ct Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId acceptMember_ <- asks $ acceptMember . chatHooks . config @@ -1216,14 +1216,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo mem <- acceptGroupJoinRequestAsync user gInfo cReq acceptance useRole profileMode createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - toView $ CRAcceptingGroupJoinRequestMember user gInfo mem + toView $ CEvtAcceptingGroupJoinRequestMember user gInfo mem Left rjctReason | v < groupJoinRejectVersion -> messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" | otherwise -> do mem <- acceptGroupJoinSendRejectAsync user gInfo cReq rjctReason toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason - _ -> toView $ CRReceivedContactRequest user cReq + _ -> toView $ CEvtReceivedContactRequest user cReq -- TODO [knocking] review memberCanSend :: GroupMember -> CM () -> CM () @@ -1238,12 +1238,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = authErrCounter' <- withStore' $ \db -> incAuthErrCounter db user conn when (authErrCounter' >= authErrDisableCount) $ case connEntity of RcvDirectMsgConnection ctConn (Just ct) -> do - toView $ CRContactDisabled user ct {activeConn = Just ctConn {authErrCounter = authErrCounter'}} - _ -> toView $ CRConnectionDisabled connEntity + toView $ CEvtContactDisabled user ct {activeConn = Just ctConn {authErrCounter = authErrCounter'}} + _ -> toView $ CEvtConnectionDisabled connEntity SMP _ SMP.QUOTA -> unless (connInactive conn) $ do withStore' $ \db -> setQuotaErrCounter db user conn quotaErrSetOnMERR - toView $ CRConnectionInactive connEntity True + toView $ CEvtConnectionInactive connEntity True _ -> pure () processConnMWARN :: ConnectionEntity -> Connection -> AgentErrorType -> CM () @@ -1253,8 +1253,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = unless (connInactive conn) $ do quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn when (quotaErrCounter' >= quotaErrInactiveCount) $ - toView $ - CRConnectionInactive connEntity True + toView $ CEvtConnectionInactive connEntity True _ -> pure () continueSending :: ConnectionEntity -> Connection -> CM Bool @@ -1262,7 +1261,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = if connInactive conn then do withStore' $ \db -> setQuotaErrCounter db user conn 0 - toView $ CRConnectionInactive connEntity False + toView $ CEvtConnectionInactive connEntity False pure True else pure False @@ -1350,7 +1349,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = badRcvFileChunk :: RcvFileTransfer -> String -> CM () badRcvFileChunk ft err = unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) + cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync throwChatError $ CEFileRcvChunk err memberConnectedChatItem :: GroupInfo -> GroupMember -> CM () @@ -1366,7 +1365,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = notifyMemberConnected gInfo m ct_ = do memberConnectedChatItem gInfo m lift $ mapM_ (`setContactNetworkStatus` NSConnected) ct_ - toView $ CRConnectedToGroupMember user gInfo m ct_ + toView $ CEvtConnectedToGroupMember user gInfo m ct_ probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> CM () probeMatchingContactsAndMembers ct connectedIncognito doProbeContacts = do @@ -1423,10 +1422,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> createSentProbeHash db userId probeId cgm messageWarning :: Text -> CM () - messageWarning = toView . CRMessageError user "warning" + messageWarning = toView . CEvtMessageError user "warning" messageError :: Text -> CM () - messageError = toView . CRMessageError user "error" + messageError = toView . CEvtMessageError user "error" newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> CM () newContentMessage ct mc msg@RcvMessage {sharedMsgId_} msgMeta = do @@ -1452,13 +1451,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = newChatItem content ciFile_ timed_ live = do ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content ciFile_ timed_ live M.empty reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getDirectCIReactions db ct sharedMsgId) sharedMsgId_ - toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions}] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions}] autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM () autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do -- ! autoAcceptFileSize is only used in tests ChatConfig {autoAcceptFileSize = sz} <- asks config - when (sz > fileSize) $ receiveFile' user ft False Nothing Nothing >>= toView + when (sz > fileSize) $ receiveFileEvt' user ft False Nothing Nothing >>= toView messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM () messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do @@ -1482,7 +1481,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure (rfd, ft') when fileDescrComplete $ do ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId - toView $ CRRcvFileDescrReady user ci ft' rfd + toView $ CEvtRcvFileDescrReady user ci ft' rfd case (fileStatus, xftpRcvFile) of (RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd userApprovedRelays cryptoArgs _ -> pure () @@ -1521,7 +1520,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci' <- withStore' $ \db -> do createChatItemVersion db (chatItemId' ci) brokerTs mc updateDirectChatItem' db user contactId ci content True live Nothing Nothing - toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') + toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') where brokerTs = metaBrokerTs msgMeta content = CIRcvMsgContent mc @@ -1540,24 +1539,25 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = reactions <- getDirectCIReactions db ct sharedMsgId let edited = itemLive /= Just True updateDirectChatItem' db user contactId ci {reactions} content edited live Nothing $ Just msgId - toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') + toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' - else toView $ CRChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + else toView $ CEvtChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) _ -> messageError "x.msg.update: contact attempted invalid message update" messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> CM () messageDelete ct@Contact {contactId} sharedMsgId _rcvMessage msgMeta = do - deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct) + deleteRcvChatItem `catchCINotFound` (toView . CEvtChatItemDeletedNotFound user ct) where brokerTs = metaBrokerTs msgMeta deleteRcvChatItem = do cci@(CChatItem msgDir ci) <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId case msgDir of SMDRcv - | rcvItemDeletable ci brokerTs -> - if featureAllowed SCFFullDelete forContact ct - then deleteDirectCIs user ct [cci] False False >>= toView - else markDirectCIsDeleted user ct [cci] False brokerTs >>= toView + | rcvItemDeletable ci brokerTs -> do + deletions <- if featureAllowed SCFFullDelete forContact ct + then deleteDirectCIs user ct [cci] + else markDirectCIsDeleted user ct [cci] brokerTs + toView $ CEvtChatItemsDeleted user deletions False False | otherwise -> messageError "x.msg.del: contact attempted invalid message delete" SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" @@ -1575,7 +1575,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> setDirectReaction db ct sharedMsgId False reaction add msgId brokerTs where updateChatItemReaction = do - cr_ <- withStore $ \db -> do + cEvt_ <- withStore $ \db -> do CChatItem md ci <- getDirectChatItemBySharedMsgId db user (contactId' ct) sharedMsgId if ciReactionAllowed ci then liftIO $ do @@ -1583,9 +1583,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = reactions <- getDirectCIReactions db ct sharedMsgId let ci' = CChatItem md ci {reactions} r = ACIReaction SCTDirect SMDRcv (DirectChat ct) $ CIReaction CIDirectRcv ci' brokerTs reaction - pure $ Just $ CRChatItemReaction user add r + pure $ Just $ CEvtChatItemReaction user add r else pure Nothing - mapM_ toView cr_ + mapM_ toView cEvt_ groupMsgReaction :: GroupInfo -> GroupMember -> SharedMsgId -> MemberId -> MsgReaction -> Bool -> RcvMessage -> UTCTime -> CM () groupMsgReaction g m sharedMsgId itemMemberId reaction add RcvMessage {msgId} brokerTs = do @@ -1596,7 +1596,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs where updateChatItemReaction = do - cr_ <- withStore $ \db -> do + cEvt_ <- withStore $ \db -> do CChatItem md ci <- getGroupMemberCIBySharedMsgId db user g itemMemberId sharedMsgId if ciReactionAllowed ci then liftIO $ do @@ -1604,9 +1604,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = reactions <- getGroupCIReactions db g itemMemberId sharedMsgId let ci' = CChatItem md ci {reactions} r = ACIReaction SCTGroup SMDRcv (GroupChat g) $ CIReaction (CIGroupRcv m) ci' brokerTs reaction - pure $ Just $ CRChatItemReaction user add r + pure $ Just $ CEvtChatItemReaction user add r else pure Nothing - mapM_ toView cr_ + mapM_ toView cEvt_ reactionAllowed :: Bool -> MsgReaction -> [MsgReaction] -> Bool reactionAllowed add reaction rs = (reaction `elem` rs) /= add && not (add && length rs >= maxMsgReactions) @@ -1656,7 +1656,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = do file_ <- processFileInv ci <- createNonLive file_ - toView =<< markGroupCIsDeleted user gInfo [CChatItem SMDRcv ci] False (Just moderator) moderatedAt + deletions <- markGroupCIsDeleted user gInfo [CChatItem SMDRcv ci] (Just moderator) moderatedAt + toView $ CEvtChatItemsDeleted user deletions False False createNonLive file_ = saveRcvCI (CIRcvMsgContent content, ts) (snd <$> file_) timed' False mentions createContentItem = do @@ -1688,7 +1689,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createChatItemVersion db (chatItemId' ci) brokerTs mc ci' <- updateGroupChatItem db user groupId ci content True live Nothing blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' - toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') + toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') where content = CIRcvMsgContent mc ts@(_, ft_) = msgContentTexts mc @@ -1710,9 +1711,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ciMentions <- getRcvCIMentions db user gInfo ft_ mentions ci' <- updateGroupChatItem db user groupId ci {reactions} content edited live $ Just msgId updateGroupCIMentions db gInfo ci' ciMentions - toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') + toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - else toView $ CRChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) + else toView $ CEvtChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) else messageError "x.msg.update: group member attempted to update a message of another member" _ -> messageError "x.msg.update: group member attempted invalid message update" @@ -1730,13 +1731,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- regular deletion Nothing | sameMemberId memberId mem && msgMemberId == memberId && rcvItemDeletable ci brokerTs -> - delete cci Nothing >>= toView + delete cci Nothing | otherwise -> messageError "x.msg.del: member attempted invalid message delete" -- moderation (not limited by time) Just _ | sameMemberId memberId mem && msgMemberId == memberId -> - delete cci (Just m) >>= toView + delete cci (Just m) | otherwise -> moderate mem cci CIGroupSnd -> moderate membership cci @@ -1749,7 +1750,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = moderate mem cci = case sndMemberId_ of Just sndMemberId | sameMemberId sndMemberId mem -> checkRole mem $ do - delete cci (Just m) >>= toView + delete cci (Just m) archiveMessageReports cci m | otherwise -> messageError "x.msg.del: message of another member with incorrect memberId" _ -> messageError "x.msg.del: message of another member without memberId" @@ -1757,14 +1758,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | senderRole < GRModerator || senderRole < memberRole = messageError "x.msg.del: message of another member with insufficient member permissions" | otherwise = a - delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM ChatResponse - delete cci byGroupMember - | groupFeatureMemberAllowed SGFFullDelete m gInfo = deleteGroupCIs user gInfo [cci] False False byGroupMember brokerTs - | otherwise = markGroupCIsDeleted user gInfo [cci] False byGroupMember brokerTs + delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM () + delete cci byGroupMember = do + deletions <- if groupFeatureMemberAllowed SGFFullDelete m gInfo + then deleteGroupCIs user gInfo [cci] byGroupMember brokerTs + else markGroupCIsDeleted user gInfo [cci] byGroupMember brokerTs + toView $ CEvtChatItemsDeleted user deletions False False archiveMessageReports :: CChatItem 'CTGroup -> GroupMember -> CM () archiveMessageReports (CChatItem _ ci) byMember = do ciIds <- withStore' $ \db -> markMessageReportsDeleted db user gInfo ci byMember brokerTs - unless (null ciIds) $ toView $ CRGroupChatItemsDeleted user gInfo ciIds False (Just byMember) + unless (null ciIds) $ toView $ CEvtGroupChatItemsDeleted user gInfo ciIds False (Just byMember) -- TODO remove once XFile is discontinued processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> CM () @@ -1777,7 +1780,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty - toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] where brokerTs = metaBrokerTs msgMeta @@ -1813,9 +1816,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId ft <- withStore (\db -> getRcvFileTransfer db user fileId) unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) + cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync ci <- withStore $ \db -> getChatItemByFileId db vr user fileId - toView $ CRRcvFileSndCancelled user ci ft + toView $ CEvtRcvFileSndCancelled user ci ft xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> CM () xFileAcptInv ct sharedMsgId fileConnReq_ fName = do @@ -1837,7 +1840,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = event <- withStore $ \db -> do ci' <- updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 sft <- createSndDirectInlineFT db ct ft - pure $ CRSndFileStart user ci' sft + pure $ CEvtSndFileStart user ci' sft toView event ifM (allowSendInline fileSize fileInline) @@ -1867,8 +1870,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case file of Just CIFile {fileProtocol = FPXFTP} -> do ft <- withStore $ \db -> getFileTransferMeta db user fileId - toView $ CRSndFileCompleteXFTP user ci ft - _ -> toView $ CRSndFileComplete user ci sft + toView $ CEvtSndFileCompleteXFTP user ci ft + _ -> toView $ CEvtSndFileComplete user ci sft allowSendInline :: Integer -> Maybe InlineFileMode -> CM Bool allowSendInline fileSize = \case @@ -1907,9 +1910,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do ft <- withStore (\db -> getRcvFileTransfer db user fileId) unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) + cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync ci <- withStore $ \db -> getChatItemByFileId db vr user fileId - toView $ CRRcvFileSndCancelled user ci ft + toView $ CEvtRcvFileSndCancelled user ci ft else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id (SMDSnd, _) -> messageError "x.file.cancel: group member attempted invalid file cancel" @@ -1934,7 +1937,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = event <- withStore $ \db -> do ci' <- updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 sft <- liftIO $ createSndGroupInlineFT db m conn ft - pure $ CRSndFileStart user ci' sft + pure $ CEvtSndFileStart user ci' sft toView event ifM (allowSendInline fileSize fileInline) @@ -1945,7 +1948,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMsgToView :: forall d. MsgDirectionI d => GroupInfo -> ChatItem 'CTGroup d -> CM () groupMsgToView gInfo ci = - toView $ CRNewChatItems user [AChatItem SCTGroup (msgDirection @d) (GroupChat gInfo) ci] + toView $ CEvtNewChatItems user [AChatItem SCTGroup (msgDirection @d) (GroupChat gInfo) ci] processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> CM () processGroupInvitation ct inv msg msgMeta = do @@ -1967,13 +1970,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode updateGroupMemberStatusById db userId hostId GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted - toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) + toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) else do let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole ci <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) - toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] - toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + toView $ CEvtReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} where brokerTs = metaBrokerTs msgMeta sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool @@ -1994,16 +1997,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted contactConns <- withStore' $ \db -> getContactConnections db vr userId ct' - deleteAgentConnectionsAsync user $ map aConnId contactConns + deleteAgentConnectionsAsync $ map aConnId contactConns forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} let ct'' = ct' {activeConn = activeConn'} :: Contact ci <- saveRcvChatItemNoParse user (CDDirectRcv ct'') msg brokerTs (CIRcvDirectEvent RDEContactDeleted) - toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct'') ci] - toView $ CRContactDeletedByContact user ct'' + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct'') ci] + toView $ CEvtContactDeletedByContact user ct'' else do contactConns <- withStore' $ \db -> getContactConnections db vr userId c - deleteAgentConnectionsAsync user $ map aConnId contactConns + deleteAgentConnectionsAsync $ map aConnId contactConns withStore $ \db -> deleteContact db user c where brokerTs = metaBrokerTs msgMeta @@ -2020,7 +2023,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (directOrUsed c' && createItems) $ do createProfileUpdatedItem c' lift $ createRcvFeatureItems user c c' - toView $ CRContactUpdated user c c' + toView $ CEvtContactUpdated user c c' pure c' | otherwise = pure c @@ -2070,7 +2073,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupMemberStatus db userId m GSMemConnected updateGroupMemberAccepted db user membership role let m' = m {memberStatus = GSMemConnected} - toView $ CRUserJoinedGroup user gInfo {membership = membership'} m' + toView $ CEvtUserJoinedGroup user gInfo {membership = membership'} m' let connectedIncognito = memberIncognito membership probeMatchingMemberContact m' connectedIncognito @@ -2082,7 +2085,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Nothing -> do m' <- withStore $ \db -> updateMemberProfile db user m p' createProfileUpdatedItem m' - toView $ CRGroupMemberUpdated user gInfo m m' + toView $ CEvtGroupMemberUpdated user gInfo m m' pure m' Just mContactId -> do mCt <- withStore $ \db -> getContact db vr user mContactId @@ -2090,8 +2093,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do (m', ct') <- withStore $ \db -> updateContactMemberProfile db user m mCt p' createProfileUpdatedItem m' - toView $ CRGroupMemberUpdated user gInfo m m' - toView $ CRContactUpdated user mCt ct' + toView $ CEvtGroupMemberUpdated user gInfo m m' + toView $ CEvtContactUpdated user mCt ct' pure m' else pure m where @@ -2106,7 +2109,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of Just bc | isMainBusinessMember bc m -> do g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' - toView $ CRGroupUpdated user g g' (Just m) + toView $ CEvtGroupUpdated user g g' (Just m) _ -> pure () isMainBusinessMember BusinessChatInfo {chatType, businessId, customerId} GroupMember {memberId} = case chatType of BCBusiness -> businessId == memberId @@ -2213,8 +2216,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> createCall db user call' $ chatItemTs' ci call_ <- atomically (TM.lookupInsert contactId call' calls) forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing - toView $ CRCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callUUID, callTs = chatItemTs' ci} - toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + toView $ CEvtCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callUUID, callTs = chatItemTs' ci} + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] else featureRejected CFCalls where brokerTs = metaBrokerTs msgMeta @@ -2222,7 +2225,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = featureRejected f = do let content = ciContentNoParse $ CIRcvChatFeatureRejected f ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty - toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] -- to party initiating call xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> CM () @@ -2233,7 +2236,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey) callState' = CallOfferReceived {localCallType, peerCallType = callType, peerCallSession = rtcSession, sharedKey} askConfirmation = encryptedCall localCallType && not (encryptedCall callType) - toView CRCallOffer {user, contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation} + toView CEvtCallOffer {user, contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation} pure (Just call {callState = callState'}, Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0) _ -> do msgCallStateError "x.call.offer" call @@ -2246,7 +2249,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = \call -> case callState call of CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey} - toView $ CRCallAnswer user ct rtcSession + toView $ CEvtCallAnswer user ct rtcSession pure (Just call {callState = callState'}, Just . ACIContent SMDRcv $ CIRcvCall CISCallNegotiated 0) _ -> do msgCallStateError "x.call.answer" call @@ -2260,12 +2263,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do -- TODO update the list of ice servers in peerCallSession let callState' = CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} - toView $ CRCallExtraInfo user ct rtcExtraInfo + toView $ CEvtCallExtraInfo user ct rtcExtraInfo pure (Just call {callState = callState'}, Nothing) CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} -> do -- TODO update the list of ice servers in peerCallSession let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} - toView $ CRCallExtraInfo user ct rtcExtraInfo + toView $ CEvtCallExtraInfo user ct rtcExtraInfo pure (Just call {callState = callState'}, Nothing) _ -> do msgCallStateError "x.call.extra" call @@ -2275,7 +2278,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xCallEnd :: Contact -> CallId -> RcvMessage -> CM () xCallEnd ct callId msg = msgCurrentCall ct callId "x.call.end" msg $ \Call {chatItemId} -> do - toView $ CRCallEnded user ct + toView $ CEvtCallEnded user ct (Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> (Call -> CM (Maybe Call, Maybe ACIContent)) -> CM () @@ -2317,7 +2320,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where merge c1' c2' = do c2'' <- withStore $ \db -> mergeContactRecords db vr user c1' c2' - toView $ CRContactsMerged user c1' c2' c2'' + toView $ CEvtContactsMerged user c1' c2' c2'' when (directOrUsed c2'') $ showSecurityCodeChanged c2'' pure $ Just c2'' where @@ -2356,14 +2359,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = associateMemberWithContact c1 m2@GroupMember {groupId} = do withStore' $ \db -> associateMemberWithContactRecord db user c1 m2 g <- withStore $ \db -> getGroupInfo db vr user groupId - toView $ CRContactAndMemberAssociated user c1 g m2 c1 + toView $ CEvtContactAndMemberAssociated user c1 g m2 c1 pure c1 associateContactWithMember :: GroupMember -> Contact -> CM Contact associateContactWithMember m1@GroupMember {groupId} c2 = do c2' <- withStore $ \db -> associateContactWithMemberRecord db vr user m1 c2 g <- withStore $ \db -> getGroupInfo db vr user groupId - toView $ CRContactAndMemberAssociated user c2 g m1 c2' + toView $ CEvtContactAndMemberAssociated user c2 g m1 c2' pure c2' saveConnInfo :: Connection -> ConnInfo -> CM (Connection, Bool) @@ -2373,15 +2376,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case chatMsgEvent of XInfo p -> do ct <- withStore $ \db -> createDirectContact db user conn' p - toView $ CRContactConnecting user ct + toView $ CEvtContactConnecting user ct pure (conn', False) XGrpLinkInv glInv -> do (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db vr user conn' glInv - toView $ CRGroupLinkConnecting user gInfo host + toView $ CEvtGroupLinkConnecting user gInfo host pure (conn', True) XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do (gInfo, host) <- withStore $ \db -> createGroupRejectedViaLink db vr user conn' glRjct - toView $ CRGroupLinkConnecting user gInfo host + toView $ CEvtGroupLinkConnecting user gInfo host toViewTE $ TEGroupLinkRejected user gInfo rejectionReason pure (conn', True) -- TODO show/log error, other events in SMP confirmation @@ -2394,7 +2397,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case Right unknownMember@GroupMember {memberStatus = GSMemUnknown} -> do updatedMember <- withStore $ \db -> updateUnknownMemberAnnounced db vr user m unknownMember memInfo - toView $ CRUnknownMemberAnnounced user gInfo m unknownMember updatedMember + toView $ CEvtUnknownMemberAnnounced user gInfo m unknownMember updatedMember memberAnnouncedToView updatedMember Right _ -> messageError "x.grp.mem.new error: member already exists" Left _ -> do @@ -2405,7 +2408,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let event = RGEMemberAdded groupMemberId (fromLocalProfile memberProfile) ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent event) groupMsgToView gInfo ci - toView $ CRJoinedGroupMemberConnecting user gInfo m announcedMember + toView $ CEvtJoinedGroupMemberConnecting user gInfo m announcedMember xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MemberRestrictions -> CM () xGrpMemIntro gInfo@GroupInfo {chatSettings} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memChatVRange _) memRestrictions = do @@ -2444,7 +2447,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist" Right reMember -> do GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv - sendGroupMemberMessage user gInfo reMember (XGrpMemFwd (memberInfo m) introInv) (Just introId) $ + sendGroupMemberMessage gInfo reMember (XGrpMemFwd (memberInfo m) introInv) (Just introId) $ withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded _ -> messageError "x.grp.mem.inv can be only sent by invitee member" @@ -2491,7 +2494,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> updateGroupMemberRole db user member memRole ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent gEvent) groupMsgToView gInfo ci - toView CRMemberRole {user, groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} + toView CEvtMemberRole {user, groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = @@ -2515,15 +2518,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | senderRole < GRModerator || senderRole < memberRole -> messageError "x.grp.mem.restrict with insufficient member permissions" | otherwise -> do bm' <- setMemberBlocked bm - toggleNtf user bm' (not blocked) + toggleNtf bm' (not blocked) let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs ciContent groupMsgToView gInfo ci - toView CRMemberBlockedForAll {user, groupInfo = gInfo, byMember = m, member = bm, blocked} + toView CEvtMemberBlockedForAll {user, groupInfo = gInfo, byMember = m, member = bm, blocked} Left (SEGroupMemberNotFoundByMemberId _) -> do bm <- createUnknownMember gInfo memId bm' <- setMemberBlocked bm - toView $ CRUnknownMemberBlocked user gInfo m bm' + toView $ CEvtUnknownMemberBlocked user gInfo m bm' Left e -> throwError $ ChatErrorStore e where setMemberBlocked bm = withStore' $ \db -> updateGroupMemberBlocked db user gInfo restriction bm @@ -2570,8 +2573,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> updateStatus introId GMIntroReConnected updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> UTCTime -> CM () - xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId msg brokerTs = do + xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> RcvMessage -> UTCTime -> CM () + xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages msg brokerTs = do let GroupMember {memberId = membershipMemId} = membership if membershipMemId == memId then checkRole membership $ do @@ -2580,19 +2583,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = members <- withStore' $ \db -> getGroupMembers db vr user gInfo deleteMembersConnections user members withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved + when withMessages $ deleteMessages membership SMDSnd deleteMemberItem RGEUserDeleted - toView $ CRDeletedMemberUser user gInfo {membership = membership {memberStatus = GSMemRemoved}} m + toView $ CEvtDeletedMemberUser user gInfo {membership = membership {memberStatus = GSMemRemoved}} m withMessages else withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case Left _ -> messageError "x.grp.mem.del with unknown member ID" Right member@GroupMember {groupMemberId, memberProfile} -> checkRole member $ do -- ? prohibit deleting member if it's the sender - sender should use x.grp.leave - deleteMemberConnection user member + deleteMemberConnection member -- undeleted "member connected" chat item will prevent deletion of member record deleteOrUpdateMemberRecord user member + when withMessages $ deleteMessages member SMDRcv deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) - toView $ CRDeletedMember user gInfo m member {memberStatus = GSMemRemoved} + toView $ CEvtDeletedMember user gInfo m member {memberStatus = GSMemRemoved} withMessages where checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = @@ -2601,15 +2606,19 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = deleteMemberItem gEvent = do ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent gEvent) groupMsgToView gInfo ci + deleteMessages :: MsgDirectionI d => GroupMember -> SMsgDirection d -> CM () + deleteMessages delMem msgDir + | groupFeatureMemberAllowed SGFFullDelete m gInfo = deleteGroupMemberCIs user gInfo delMem m msgDir + | otherwise = markGroupMemberCIsDeleted user gInfo delMem m xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () xGrpLeave gInfo m msg brokerTs = do - deleteMemberConnection user m + deleteMemberConnection m -- member record is not deleted to allow creation of "member left" chat item withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEMemberLeft) groupMsgToView gInfo ci - toView $ CRLeftMember user gInfo m {memberStatus = GSMemLeft} + toView $ CEvtLeftMember user gInfo m {memberStatus = GSMemLeft} xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg brokerTs = do @@ -2622,7 +2631,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = deleteMembersConnections user ms ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEGroupDeleted) groupMsgToView gInfo ci - toView $ CRGroupDeleted user gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m + toView $ CEvtGroupDeleted user gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM () xGrpInfo g@GroupInfo {groupProfile = p, businessChat} m@GroupMember {memberRole} p' msg brokerTs @@ -2630,7 +2639,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = case businessChat of Nothing -> unless (p == p') $ do g' <- withStore $ \db -> updateGroupProfile db user g p' - toView $ CRGroupUpdated user g g' (Just m) + toView $ CEvtGroupUpdated user g g' (Just m) let cd = CDGroupRcv g' m unless (sameGroupProfileInfo p p') $ do ci <- saveRcvChatItemNoParse user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') @@ -2647,7 +2656,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupPrefs_ g@GroupInfo {groupProfile = p} m ps' = unless (groupPreferences p == Just ps') $ do g' <- withStore' $ \db -> updateGroupPreferences db user g ps' - toView $ CRGroupUpdated user g g' (Just m) + toView $ CEvtGroupUpdated user g g' (Just m) let cd = CDGroupRcv g' m createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' @@ -2690,14 +2699,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = joinAgentConnectionAsync user True connReq dm subMode createItems mCt' m' = do createInternalChatItem user (CDGroupRcv g m') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing - toView $ CRNewMemberContactReceivedInv user mCt' g m' + toView $ CEvtNewMemberContactReceivedInv user mCt' g m' forM_ mContent_ $ \mc -> do ci <- saveRcvChatItem user (CDDirectRcv mCt') msg brokerTs (CIRcvMsgContent mc, msgContentTexts mc) - toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat mCt') ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat mCt') ci] securityCodeChanged :: Contact -> CM () securityCodeChanged ct = do - toView $ CRContactVerificationReset user ct + toViewTE $ TEContactVerificationReset user ct createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> ChatMessage 'Json -> UTCTime -> CM () @@ -2707,7 +2716,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Right author -> processForwardedMsg author msg Left (SEGroupMemberNotFoundByMemberId _) -> do unknownAuthor <- createUnknownMember gInfo memberId - toView $ CRUnknownMemberCreated user gInfo m unknownAuthor + toView $ CEvtUnknownMemberCreated user gInfo m unknownAuthor processForwardedMsg unknownAuthor msg Left e -> throwError $ ChatErrorStore e where @@ -2726,7 +2735,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XInfo p -> xInfoMember gInfo author p msgTs XGrpMemNew memInfo -> xGrpMemNew gInfo author memInfo rcvMsg msgTs XGrpMemRole memId memRole -> xGrpMemRole gInfo author memId memRole rcvMsg msgTs - XGrpMemDel memId -> xGrpMemDel gInfo author memId rcvMsg msgTs + XGrpMemDel memId withMessages -> xGrpMemDel gInfo author memId withMessages rcvMsg msgTs XGrpLeave -> xGrpLeave gInfo author rcvMsg msgTs XGrpDel -> xGrpDel gInfo author rcvMsg msgTs XGrpInfo p' -> xGrpInfo gInfo author p' rcvMsg msgTs @@ -2757,7 +2766,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateDirectItemsStatusMsgs ct conn msgIds newStatus = do cis <- withStore' $ \db -> forM msgIds $ \msgId -> runExceptT $ updateDirectItemsStatus' db ct conn msgId newStatus let acis = map ctItem $ concat $ rights cis - unless (null acis) $ toView $ CRChatItemsStatusesUpdated user acis + unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis where ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct) @@ -2765,7 +2774,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateDirectItemStatus ct conn msgId newStatus = do cis <- withStore $ \db -> updateDirectItemsStatus' db ct conn msgId newStatus let acis = map ctItem cis - unless (null acis) $ toView $ CRChatItemsStatusesUpdated user acis + unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis where ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct) @@ -2796,7 +2805,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = items <- withStore' (\db -> getGroupChatItemsByAgentMsgId db user groupId connId msgId) cis <- catMaybes <$> withStore (\db -> mapM (updateItem db) items) let acis = map gItem cis - unless (null acis) $ toView $ CRChatItemsStatusesUpdated user acis + unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis where gItem = AChatItem SCTGroup SMDSnd (GroupChat gInfo) updateItem :: DB.Connection -> CChatItem 'CTGroup -> ExceptT StoreError IO (Maybe (ChatItem 'CTGroup 'MDSnd)) diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 0d1432c7e5..9811556fe0 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -4,6 +4,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} {-# HLINT ignore "Use newtype instead of data" #-} @@ -16,7 +17,7 @@ import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A -import Data.Char (isDigit, isPunctuation, isSpace) +import Data.Char (isAlpha, isAscii, isDigit, isPunctuation, isSpace) import Data.Either (fromRight) import Data.Functor (($>)) import Data.List (foldl', intercalate) @@ -29,11 +30,10 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Types -import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..)) +import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnReqUriData (..), ConnShortLink (..), ConnectionLink (..), ConnectionRequestUri (..), ContactConnType (..), SMPQueue (..), simplexConnReqUri, simplexShortLink) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON) import Simplex.Messaging.Protocol (ProtocolServer (..)) -import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8) import System.Console.ANSI.Types import qualified Text.Email.Validate as Email @@ -49,7 +49,7 @@ data Format | Secret | Colored {color :: FormatColor} | Uri - | SimplexLink {linkType :: SimplexLinkType, simplexUri :: Text, smpHosts :: NonEmpty Text} + | SimplexLink {linkType :: SimplexLinkType, simplexUri :: AConnectionLink, smpHosts :: NonEmpty Text} | Mention {memberName :: Text} | Email | Phone @@ -62,7 +62,7 @@ mentionedNames = mapMaybe (\(FormattedText f _) -> mentionedName =<< f) Mention name -> Just name _ -> Nothing -data SimplexLinkType = XLContact | XLInvitation | XLGroup +data SimplexLinkType = XLContact | XLInvitation | XLGroup | XLChannel deriving (Eq, Show) colored :: Color -> Format @@ -205,17 +205,18 @@ markdownP = mconcat <$> A.many' fragmentP else pure $ markdown (colored clr) s mentionP = do c <- A.char '@' *> A.peekChar' - name <- displayNameTextP + (name, punct) <- displayNameTextP_ let sName = if c == '\'' then '\'' `T.cons` name `T.snoc` '\'' else name - pure $ markdown (Mention name) ('@' `T.cons` sName) + mention = markdown (Mention name) ('@' `T.cons` sName) + pure $ if T.null punct then mention else mention :|: unmarked punct colorP = A.anyChar >>= \case - 'r' -> "ed" $> Red <|> pure Red - 'g' -> "reen" $> Green <|> pure Green - 'b' -> "lue" $> Blue <|> pure Blue - 'y' -> "ellow" $> Yellow <|> pure Yellow - 'c' -> "yan" $> Cyan <|> pure Cyan - 'm' -> "agenta" $> Magenta <|> pure Magenta + 'r' -> optional "ed" $> Red + 'g' -> optional "reen" $> Green + 'b' -> optional "lue" $> Blue + 'y' -> optional "ellow" $> Yellow + 'c' -> optional "yan" $> Cyan + 'm' -> optional "agenta" $> Magenta '1' -> pure Red '2' -> pure Green '3' -> pure Blue @@ -237,35 +238,57 @@ markdownP = mconcat <$> A.many' fragmentP wordMD :: Text -> Markdown wordMD s | T.null s = unmarked s - | isUri s = - let t = T.takeWhileEnd isPunctuation' s - uri = uriMarkdown $ T.dropWhileEnd isPunctuation' s - in if T.null t then uri else uri :|: unmarked t - | isEmail s = markdown Email s + | isUri s' = res $ uriMarkdown s' + | isDomain s' = res $ markdown Uri s' + | isEmail s' = res $ markdown Email s' | otherwise = unmarked s + where + punct = T.takeWhileEnd isPunctuation' s + s' = T.dropWhileEnd isPunctuation' s + res md' = if T.null punct then md' else md' :|: unmarked punct isPunctuation' = \case '/' -> False ')' -> False c -> isPunctuation c uriMarkdown s = case strDecode $ encodeUtf8 s of - Right cReq -> markdown (simplexUriFormat cReq) s + Right cLink -> markdown (simplexUriFormat cLink) s _ -> markdown Uri s isUri s = T.length s >= 10 && any (`T.isPrefixOf` s) ["http://", "https://", "simplex:/"] + -- matches what is likely to be a domain, not all valid domain names + isDomain s = case T.splitOn "." s of + [name, tld] -> isDomain_ name tld + [sub, name, tld] -> T.length sub >= 3 && T.length sub <= 8 && isDomain_ name tld + _ -> False + where + isDomain_ name tld = + (let n = T.length name in n >= 1 && n <= 24) + && (let n = T.length tld in n >= 2 && n <= 8) + && (let p c = isAscii c && isAlpha c in T.all p name && T.all p tld) isEmail s = T.any (== '@') s && Email.isValid (encodeUtf8 s) noFormat = pure . unmarked - simplexUriFormat :: AConnectionRequestUri -> Format + simplexUriFormat :: AConnectionLink -> Format simplexUriFormat = \case - ACR _ (CRContactUri crData) -> - let uri = safeDecodeUtf8 . strEncode $ CRContactUri crData {crScheme = SSSimplex} - in SimplexLink (linkType' crData) uri $ uriHosts crData - ACR _ (CRInvitationUri crData e2e) -> - let uri = safeDecodeUtf8 . strEncode $ CRInvitationUri crData {crScheme = SSSimplex} e2e - in SimplexLink XLInvitation uri $ uriHosts crData - where - uriHosts ConnReqUriData {crSmpQueues} = L.map (safeDecodeUtf8 . strEncode) $ sconcat $ L.map (host . qServer) crSmpQueues - linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of - Just (CRDataGroup _) -> XLGroup - Nothing -> XLContact + ACL m (CLFull cReq) -> case cReq of + CRContactUri crData -> SimplexLink (linkType' crData) cLink $ uriHosts crData + CRInvitationUri crData _ -> SimplexLink XLInvitation cLink $ uriHosts crData + where + cLink = ACL m $ CLFull $ simplexConnReqUri cReq + uriHosts ConnReqUriData {crSmpQueues} = L.map strEncodeText $ sconcat $ L.map (host . qServer) crSmpQueues + linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of + Just (CRDataGroup _) -> XLGroup + Nothing -> XLContact + ACL m (CLShort sLnk) -> case sLnk of + CSLContact _ ct srv _ -> SimplexLink (linkType' ct) cLink $ uriHosts srv + CSLInvitation _ srv _ _ -> SimplexLink XLInvitation cLink $ uriHosts srv + where + cLink = ACL m $ CLShort $ simplexShortLink sLnk + uriHosts srv = L.map strEncodeText $ host srv + linkType' = \case + CCTGroup -> XLGroup + CCTChannel -> XLChannel + CCTContact -> XLContact + strEncodeText :: StrEncoding a => a -> Text + strEncodeText = safeDecodeUtf8 . strEncode markdownText :: FormattedText -> Text markdownText (FormattedText f_ t) = case f_ of @@ -298,16 +321,22 @@ markdownText (FormattedText f_ t) = case f_ of White -> Nothing displayNameTextP :: Parser Text -displayNameTextP = quoted '\'' <|> takeNameTill (== ' ') +displayNameTextP = displayNameTextP_ >>= \(t, sfx) -> if T.null sfx then pure t else fail "Name ends with punctuation" +{-# INLINE displayNameTextP #-} + +displayNameTextP_ :: Parser (Text, Text) +displayNameTextP_ = (,"") <$> quoted '\'' <|> splitPunctuation <$> takeNameTill isSpace where takeNameTill p = A.peekChar' >>= \c -> if refChar c then A.takeTill p else fail "invalid first character in display name" + splitPunctuation s = (T.dropWhileEnd isPunctuation s, T.takeWhileEnd isPunctuation s) quoted c = A.char c *> takeNameTill (== c) <* A.char c refChar c = c > ' ' && c /= '#' && c /= '@' && c /= '\'' +-- quotes names that contain spaces or end on punctuation viewName :: Text -> Text -viewName s = if T.any isSpace s then "'" <> s <> "'" else s +viewName s = if T.any isSpace s || maybe False (isPunctuation . snd) (T.unsnoc s) then "'" <> s <> "'" else s $(JQ.deriveJSON (enumJSON $ dropPrefix "XL") ''SimplexLinkType) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index ae88bc796b..79d416dee5 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -50,10 +50,11 @@ import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..)) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, sumTypeJSON) import Simplex.Messaging.Protocol (BlockingInfo, MsgBody) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 3a61e6cdff..194fc1bb06 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -13,6 +13,7 @@ import Control.Concurrent.STM import Control.Exception (SomeException, catch) import Control.Monad.Except import Control.Monad.Reader +import Data.Aeson (ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ import Data.Bifunctor (first) @@ -51,7 +52,7 @@ import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), Migrati import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..)) import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8) import System.IO (utf8) import System.Timeout (timeout) @@ -72,9 +73,19 @@ data DBMigrationResult $(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "DBM") ''DBMigrationResult) -data APIResponse = APIResponse {corr :: Maybe CorrId, remoteHostId :: Maybe RemoteHostId, resp :: ChatResponse} +data APIResult r + = APIResult {remoteHostId :: Maybe RemoteHostId, result :: r} + | APIError {remoteHostId :: Maybe RemoteHostId, error :: ChatError} -$(JQ.deriveToJSON defaultJSON ''APIResponse) +eitherToResult :: Maybe RemoteHostId -> Either ChatError r -> APIResult r +eitherToResult rhId = either (APIError rhId) (APIResult rhId) +{-# INLINE eitherToResult #-} + +$(pure []) + +instance ToJSON r => ToJSON (APIResult r) where + toEncoding = $(JQ.mkToEncoding (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''APIResult) + toJSON = $(JQ.mkToJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''APIResult) foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString @@ -204,10 +215,10 @@ mobileChatOpts dbOptions = logAgent = Nothing, logFile = Nothing, tbqSize = 4096, + deviceName = Nothing, highlyAvailable = False, yesToUpMigrations = False }, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatCmdLog = CCLNone, @@ -286,21 +297,15 @@ chatSendCmd :: ChatController -> B.ByteString -> IO JSONByteString chatSendCmd cc = chatSendRemoteCmd cc Nothing chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO JSONByteString -chatSendRemoteCmd cc rh s = J.encode . APIResponse Nothing rh <$> runReaderT (execChatCommand rh s) cc +chatSendRemoteCmd cc rh s = J.encode . eitherToResult rh <$> runReaderT (execChatCommand rh s) cc chatRecvMsg :: ChatController -> IO JSONByteString -chatRecvMsg ChatController {outputQ} = json <$> readChatResponse +chatRecvMsg ChatController {outputQ} = J.encode . uncurry eitherToResult <$> readChatResponse where - json (corr, remoteHostId, resp) = J.encode APIResponse {corr, remoteHostId, resp} - readChatResponse = do - out@(_, _, cr) <- atomically $ readTBQueue outputQ - if filterEvent cr then pure out else readChatResponse - filterEvent = \case - CRGroupSubscribed {} -> False - CRGroupEmpty {} -> False - CRMemberSubSummary {} -> False - CRPendingSubSummary {} -> False - _ -> True + readChatResponse = + atomically (readTBQueue outputQ) >>= \case + (_, Right CEvtTerminalEvent {}) -> readChatResponse + out -> pure out chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 15135d4e9e..8c4490a2c4 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -50,8 +50,9 @@ import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types (User) import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) @@ -274,6 +275,10 @@ data UserServer' s (p :: ProtocolType) = UserServer } deriving (Show) +presetServerAddress :: UserServer' s p -> ProtocolServer p +presetServerAddress UserServer {server = ProtoServerWithAuth srv _} = srv +{-# INLINE presetServerAddress #-} + data PresetOperator = PresetOperator { operator :: Maybe NewServerOperator, smp :: [NewUserServer 'PSMP], @@ -296,6 +301,9 @@ operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of SPSMP -> useSMP SPXFTP -> useXFTP +presetServer' :: Bool -> ProtocolServer p -> NewUserServer p +presetServer' enabled = presetServer enabled . (`ProtoServerWithAuth` Nothing) + presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p presetServer = newUserServer_ True diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs new file mode 100644 index 0000000000..06c2e19fab --- /dev/null +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -0,0 +1,117 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Operators.Presets where + +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import Simplex.Chat.Operators +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) +import Simplex.Messaging.Protocol (ProtocolType (..), SMPServer) + +operatorSimpleXChat :: NewServerOperator +operatorSimpleXChat = + ServerOperator + { operatorId = DBNewEntity, + operatorTag = Just OTSimplex, + tradeName = "SimpleX Chat", + legalName = Just "SimpleX Chat Ltd", + serverDomains = ["simplex.im"], + conditionsAcceptance = CARequired Nothing, + enabled = True, + smpRoles = allRoles, + xftpRoles = allRoles + } + +operatorFlux :: NewServerOperator +operatorFlux = + ServerOperator + { operatorId = DBNewEntity, + operatorTag = Just OTFlux, + tradeName = "Flux", + legalName = Just "InFlux Technologies Limited", + serverDomains = ["simplexonflux.com"], + conditionsAcceptance = CARequired Nothing, + enabled = False, + smpRoles = ServerRoles {storage = False, proxy = True}, + xftpRoles = ServerRoles {storage = False, proxy = True} + } + +-- Please note: if any servers are removed from the lists below, they MUST be added here. +-- Otherwise previously created short links won't work. +-- +-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two, +-- so that option used for restoring links is updated earlier, for backward/forward compatibility. +allPresetServers :: NonEmpty SMPServer +allPresetServers = enabledSimplexChatSMPServers <> disabledSimplexChatSMPServers <> fluxSMPServers_ + -- added for testing, not preset in the clients + <> ["smp://8Af90NX2TTkKEJAF1RCg69P_Odg2Z-6_J6DOKUqK3rQ=@smp7.simplex.im,dbxqutskmmbkbrs7ofi7pmopeyhgi5cxbjbh4ummgmep4r6bz4cbrcid.onion"] + +simplexChatSMPServers :: [NewUserServer 'PSMP] +simplexChatSMPServers = + map (presetServer' True) (L.toList enabledSimplexChatSMPServers) + <> map (presetServer' False) (L.toList disabledSimplexChatSMPServers) + +-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers. +-- Otherwise previously created short links won't work. +-- +-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two, +-- so that option used for restoring links is updated earlier, for backward/forward compatibility. +enabledSimplexChatSMPServers :: NonEmpty SMPServer +enabledSimplexChatSMPServers = + [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", + "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", + "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", + "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", + "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", + "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", + "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", + "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", + "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", + "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", + "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" + ] + +-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers. +-- Otherwise previously created short links won't work. +-- +-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two, +-- so that option used for restoring links is updated earlier, for backward/forward compatibility. +disabledSimplexChatSMPServers :: NonEmpty SMPServer +disabledSimplexChatSMPServers = + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" + ] + +fluxSMPServers :: [NewUserServer 'PSMP] +fluxSMPServers = map (presetServer' True) (L.toList fluxSMPServers_) + +-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers. +-- Otherwise previously created short links won't work. +-- +-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two, +-- so that option used for restoring links is updated earlier, for backward/forward compatibility. +fluxSMPServers_ :: NonEmpty SMPServer +fluxSMPServers_ = + [ "smp://xQW_ufMkGE20UrTlBl8QqceG1tbuylXhr9VOLPyRJmw=@smp1.simplexonflux.com,qb4yoanyl4p7o33yrknv4rs6qo7ugeb2tu2zo66sbebezs4cpyosarid.onion", + "smp://LDnWZVlAUInmjmdpQQoIo6FUinRXGe0q3zi5okXDE4s=@smp2.simplexonflux.com,yiqtuh3q4x7hgovkomafsod52wvfjucdljqbbipg5sdssnklgongxbqd.onion", + "smp://1jne379u7IDJSxAvXbWb_JgoE7iabcslX0LBF22Rej0=@smp3.simplexonflux.com,a5lm4k7ufei66cdck6fy63r4lmkqy3dekmmb7jkfdm5ivi6kfaojshad.onion", + "smp://xmAmqj75I9mWrUihLUlI0ZuNLXlIwFIlHRq5Pb6cHAU=@smp4.simplexonflux.com,qpcz2axyy66u26hfdd2e23uohcf3y6c36mn7dcuilcgnwjasnrvnxjqd.onion", + "smp://rWvBYyTamuRCBYb_KAn-nsejg879ndhiTg5Sq3k0xWA=@smp5.simplexonflux.com,4ao347qwiuluyd45xunmii4skjigzuuox53hpdsgbwxqafd4yrticead.onion", + "smp://PN7-uqLBToqlf1NxHEaiL35lV2vBpXq8Nj8BW11bU48=@smp6.simplexonflux.com,hury6ot3ymebbr2535mlp7gcxzrjpc6oujhtfxcfh2m4fal4xw5fq6qd.onion" + ] + +fluxXFTPServers :: [NewUserServer 'PXFTP] +fluxXFTPServers = + map + (presetServer True) + [ "xftp://92Sctlc09vHl_nAqF2min88zKyjdYJ9mgxRCJns5K2U=@xftp1.simplexonflux.com,apl3pumq3emwqtrztykyyoomdx4dg6ysql5zek2bi3rgznz7ai3odkid.onion", + "xftp://YBXy4f5zU1CEhnbbCzVWTNVNsaETcAGmYqGNxHntiE8=@xftp2.simplexonflux.com,c5jjecisncnngysah3cz2mppediutfelco4asx65mi75d44njvua3xid.onion", + "xftp://ARQO74ZSvv2OrulRF3CdgwPz_AMy27r0phtLSq5b664=@xftp3.simplexonflux.com,dc4mohiubvbnsdfqqn7xhlhpqs5u4tjzp7xpz6v6corwvzvqjtaqqiqd.onion", + "xftp://ub2jmAa9U0uQCy90O-fSUNaYCj6sdhl49Jh3VpNXP58=@xftp4.simplexonflux.com,4qq5pzier3i4yhpuhcrhfbl6j25udc4czoyascrj4yswhodhfwev3nyd.onion", + "xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion", + "xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion" + ] diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index c58c792819..ff2af56b85 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -29,7 +29,7 @@ import Numeric.Natural (Natural) import Options.Applicative import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) import Simplex.FileTransfer.Description (mb) -import Simplex.Messaging.Client (HostMode (..), SocksMode (..), textToHostMode) +import Simplex.Messaging.Client (HostMode (..), SMPWebPortServers (..), SocksMode (..), textToHostMode) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth) @@ -38,7 +38,6 @@ import Simplex.Chat.Options.DB data ChatOpts = ChatOpts { coreOptions :: CoreChatOpts, - deviceName :: Maybe Text, chatCmd :: String, chatCmdDelay :: Int, chatCmdLog :: ChatCmdLog, @@ -64,6 +63,7 @@ data CoreChatOpts = CoreChatOpts logAgent :: Maybe LogLevel, logFile :: Maybe FilePath, tbqSize :: Natural, + deviceName :: Maybe Text, highlyAvailable :: Bool, yesToUpMigrations :: Bool } @@ -153,11 +153,17 @@ coreChatOptsP appDir defaultDbName = do <> metavar "SMP_PROXY_FALLBACK_MODE" <> help "Allow downgrade and connect directly: no, [when IP address is] protected (default), yes" ) - smpWebPort <- - switch + smpWebPortServers <- + flag' SWPAll ( long "smp-web-port" <> help "Use port 443 with SMP servers when not specified" ) + <|> option + strParse + ( long "smp-web-port-servers" + <> help "Use port 443 with SMP servers when not specified: all, preset (default), off" + <> value SWPPreset + ) t <- option auto @@ -212,6 +218,13 @@ coreChatOptsP appDir defaultDbName = do <> value 1024 <> showDefault ) + deviceName <- + optional $ + strOption + ( long "device-name" + <> metavar "DEVICE" + <> help "Device name to use in connections with remote hosts and controller" + ) highlyAvailable <- switch ( long "ha" @@ -236,7 +249,7 @@ coreChatOptsP appDir defaultDbName = do requiredHostMode, smpProxyMode_, smpProxyFallback_, - smpWebPort, + smpWebPortServers, tcpTimeout_ = Just $ useTcpTimeout socksProxy t, logTLSErrors }, @@ -246,6 +259,7 @@ coreChatOptsP appDir defaultDbName = do logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing, logFile, tbqSize, + deviceName, highlyAvailable, yesToUpMigrations } @@ -260,13 +274,6 @@ defaultHostMode = \case chatOptsP :: FilePath -> FilePath -> Parser ChatOpts chatOptsP appDir defaultDbName = do coreOptions <- coreChatOptsP appDir defaultDbName - deviceName <- - optional $ - strOption - ( long "device-name" - <> metavar "DEVICE" - <> help "Device name to use in connections with remote hosts and controller" - ) chatCmd <- strOption ( long "execute" @@ -356,7 +363,6 @@ chatOptsP appDir defaultDbName = do pure ChatOpts { coreOptions, - deviceName, chatCmd, chatCmdDelay, chatCmdLog, diff --git a/src/Simplex/Chat/Options/Postgres.hs b/src/Simplex/Chat/Options/Postgres.hs index b174ecd02e..f7c429e93e 100644 --- a/src/Simplex/Chat/Options/Postgres.hs +++ b/src/Simplex/Chat/Options/Postgres.hs @@ -7,11 +7,14 @@ module Simplex.Chat.Options.Postgres where import qualified Data.ByteString.Char8 as B import Foreign.C.String import Options.Applicative +import Numeric.Natural (Natural) import Simplex.Messaging.Agent.Store.Interface (DBOpts (..)) data ChatDbOpts = ChatDbOpts { dbConnstr :: String, - dbSchemaPrefix :: String + dbSchemaPrefix :: String, + dbPoolSize :: Natural, + dbCreateSchema :: Bool } chatDbOptsP :: FilePath -> String -> Parser ChatDbOpts @@ -33,16 +36,32 @@ chatDbOptsP _appDir defaultDbName = do <> value "simplex_v1" <> showDefault ) - pure ChatDbOpts {dbConnstr, dbSchemaPrefix} + dbPoolSize <- + option + auto + ( long "pool-size" + <> metavar "DB_POOL_SIZE" + <> help "Database connection pool size" + <> value 1 + <> showDefault + ) + dbCreateSchema <- + switch + ( long "create-schema" + <> help "Create database schema when it does not exist" + ) + pure ChatDbOpts {dbConnstr, dbSchemaPrefix, dbPoolSize, dbCreateSchema} dbString :: ChatDbOpts -> String dbString ChatDbOpts {dbConnstr} = dbConnstr toDBOpts :: ChatDbOpts -> String -> Bool -> DBOpts -toDBOpts ChatDbOpts {dbConnstr, dbSchemaPrefix} dbSuffix _keepKey = +toDBOpts ChatDbOpts {dbConnstr, dbSchemaPrefix, dbPoolSize, dbCreateSchema} dbSuffix _keepKey = DBOpts { connstr = B.pack dbConnstr, - schema = if null dbSchemaPrefix then "simplex_v1" <> dbSuffix else dbSchemaPrefix <> dbSuffix + schema = B.pack $ if null dbSchemaPrefix then "simplex_v1" <> dbSuffix else dbSchemaPrefix <> dbSuffix, + poolSize = dbPoolSize, + createSchema = dbCreateSchema } chatSuffix :: String @@ -58,11 +77,13 @@ mobileDbOpts schemaPrefix connstr = do pure $ ChatDbOpts { dbConnstr, - dbSchemaPrefix + dbSchemaPrefix, + dbPoolSize = 1, + dbCreateSchema = True } removeDbKey :: ChatDbOpts -> ChatDbOpts removeDbKey = id errorDbStr :: DBOpts -> String -errorDbStr DBOpts {schema} = schema +errorDbStr DBOpts {schema} = B.unpack schema diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index a145914ce7..2281c1aefa 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -33,6 +33,7 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w, w2c) import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Either (fromRight) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -52,11 +53,12 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Compression (Compressed, compress1, decompress1) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Protocol (MsgBody) import Simplex.Messaging.Util (decodeJSON, eitherToMaybe, encodeJSON, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version hiding (version) @@ -343,7 +345,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpMemRestrict :: MemberId -> MemberRestrictions -> ChatMsgEvent 'Json XGrpMemCon :: MemberId -> ChatMsgEvent 'Json XGrpMemConAll :: MemberId -> ChatMsgEvent 'Json -- TODO not implemented - XGrpMemDel :: MemberId -> ChatMsgEvent 'Json + XGrpMemDel :: MemberId -> Bool -> ChatMsgEvent 'Json XGrpLeave :: ChatMsgEvent 'Json XGrpDel :: ChatMsgEvent 'Json XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json @@ -384,7 +386,7 @@ isForwardedGroupMsg ev = case ev of XGrpMemNew _ -> True XGrpMemRole {} -> True XGrpMemRestrict {} -> True - XGrpMemDel _ -> True -- TODO there should be a special logic when deleting host member (e.g., host forwards it before deleting connections) + XGrpMemDel {} -> True -- TODO there should be a special logic when deleting host member (e.g., host forwards it before deleting connections) XGrpLeave -> True XGrpDel -> True -- TODO there should be a special logic - host should forward before deleting connections XGrpInfo _ -> True @@ -991,7 +993,7 @@ toCMEventTag msg = case msg of XGrpMemRestrict _ _ -> XGrpMemRestrict_ XGrpMemCon _ -> XGrpMemCon_ XGrpMemConAll _ -> XGrpMemConAll_ - XGrpMemDel _ -> XGrpMemDel_ + XGrpMemDel {} -> XGrpMemDel_ XGrpLeave -> XGrpLeave_ XGrpDel -> XGrpDel_ XGrpInfo _ -> XGrpInfo_ @@ -1094,7 +1096,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpMemRestrict_ -> XGrpMemRestrict <$> p "memberId" <*> p "memberRestrictions" XGrpMemCon_ -> XGrpMemCon <$> p "memberId" XGrpMemConAll_ -> XGrpMemConAll <$> p "memberId" - XGrpMemDel_ -> XGrpMemDel <$> p "memberId" + XGrpMemDel_ -> XGrpMemDel <$> p "memberId" <*> Right (fromRight False $ p "messages") XGrpLeave_ -> pure XGrpLeave XGrpDel_ -> pure XGrpDel XGrpInfo_ -> XGrpInfo <$> p "groupProfile" @@ -1158,7 +1160,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ XGrpMemRestrict memId memRestrictions -> o ["memberId" .= memId, "memberRestrictions" .= memRestrictions] XGrpMemCon memId -> o ["memberId" .= memId] XGrpMemConAll memId -> o ["memberId" .= memId] - XGrpMemDel memId -> o ["memberId" .= memId] + XGrpMemDel memId messages -> o $ ("messages" .=? if messages then Just True else Nothing) ["memberId" .= memId] XGrpLeave -> JM.empty XGrpDel -> JM.empty XGrpInfo p -> o ["groupProfile" .= p] diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 3e64b2bcea..d41957bbac 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 3, 0, 7] +minRemoteCtrlVersion = AppVersion [6, 3, 4, 1] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 3, 0, 7] +minRemoteHostVersion = AppVersion [6, 3, 4, 1] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version @@ -192,7 +192,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do RHSessionConnecting _inv rhs' -> Right ((), RHSessionPendingConfirmation sessionCode tls rhs') _ -> Left $ ChatErrorRemoteHost rhKey RHEBadState let rh_' = (\rh -> (rh :: RemoteHostInfo) {sessionState = Just RHSPendingConfirmation {sessionCode}}) <$> remoteHost_ - toView CRRemoteHostSessionCode {remoteHost_ = rh_', sessionCode} + toView CEvtRemoteHostSessionCode {remoteHost_ = rh_', sessionCode} (RCHostSession {sessionKeys}, rhHello, pairing') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars' hostInfo@HostAppInfo {deviceName = hostDeviceName} <- liftError (ChatErrorRemoteHost rhKey) $ parseHostAppInfo rhHello @@ -203,7 +203,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do let rhKey' = RHId remoteHostId -- rhKey may be invalid after upserting on RHNew when (rhKey' /= rhKey) $ do atomically $ writeTVar rhKeyVar rhKey' - toView $ CRNewRemoteHost rhi + toView $ CEvtNewRemoteHost rhi -- set up HTTP transport and remote profile protocol disconnected <- toIO $ onDisconnected rhKey' sseq httpClient <- liftError' (httpError remoteHostId) $ attachRevHTTP2Client disconnected tls @@ -213,7 +213,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do RHSessionConfirmed _ RHPendingSession {rchClient} -> Right ((), RHSessionConnected {rchClient, tls, rhClient, pollAction, storePath}) _ -> Left $ ChatErrorRemoteHost rhKey RHEBadState chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host - toView $ CRRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}} + toView $ CEvtRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}} upsertRemoteHost :: RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> CM RemoteHostInfo upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ rcAddr_ hostDeviceName sseq state = do KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_ @@ -235,7 +235,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do oq <- asks outputQ forever $ do r_ <- liftRH rhId $ remoteRecv rhClient 10000000 - forM r_ $ \r -> atomically $ writeTBQueue oq (Nothing, Just rhId, r) + forM r_ $ \r -> atomically $ writeTBQueue oq (Just rhId, r) httpError :: RemoteHostId -> HTTP2ClientError -> ChatError httpError rhId = ChatErrorRemoteHost (RHId rhId) . RHEProtocolError . RPEHTTP2 . tshow @@ -271,7 +271,7 @@ cancelRemoteHostSession handlerInfo_ rhKey = do forM_ deregistered $ \session -> do liftIO $ cancelRemoteHost handlingError session `catchAny` (logError . tshow) forM_ (snd <$> handlerInfo_) $ \rhStopReason -> - toView CRRemoteHostStopped {remoteHostId_, rhsState = rhsSessionState session, rhStopReason} + toView CEvtRemoteHostStopped {remoteHostId_, rhsState = rhsSessionState session, rhStopReason} where handlingError = isJust handlerInfo_ remoteHostId_ = case rhKey of @@ -370,16 +370,17 @@ processRemoteCommand :: RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteS processRemoteCommand remoteHostId c cmd s = case cmd of SendFile chatName f -> sendFile "/f" chatName f SendImage chatName f -> sendFile "/img" chatName f - _ -> liftRH remoteHostId $ remoteSend c s + _ -> chatRemoteSend s where sendFile cmdName chatName (CryptoFile path cfArgs) = do -- don't encrypt in host if already encrypted locally CryptoFile path' cfArgs' <- storeRemoteFile remoteHostId (cfArgs $> False) path let f = CryptoFile path' (cfArgs <|> cfArgs') -- use local or host encryption - liftRH remoteHostId $ remoteSend c $ B.unwords [cmdName, B.pack (chatNameStr chatName), cryptoFileStr f] + chatRemoteSend $ B.unwords [cmdName, B.pack (chatNameStr chatName), cryptoFileStr f] cryptoFileStr CryptoFile {filePath, cryptoArgs} = maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs <> encodeUtf8 (T.pack filePath) + chatRemoteSend = either throwError pure <=< liftRH remoteHostId . remoteSend c liftRH :: RemoteHostId -> ExceptT RemoteProtocolError IO a -> CM a liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError) @@ -417,7 +418,7 @@ findKnownRemoteCtrl = do Just rc -> pure rc atomically $ putTMVar foundCtrl (rc, inv) let compatible = isJust $ compatibleAppVersion hostAppVersionRange . appVersionRange =<< ctrlAppInfo_ - toView CRRemoteCtrlFound {remoteCtrl = remoteCtrlInfo rc (Just RCSSearching), ctrlAppInfo_, appVersion = currentAppVersion, compatible} + toView CEvtRemoteCtrlFound {remoteCtrl = remoteCtrlInfo rc (Just RCSSearching), ctrlAppInfo_, appVersion = currentAppVersion, compatible} updateRemoteCtrlSession sseq $ \case RCSessionStarting -> Right RCSessionSearching {action, foundCtrl} _ -> Left $ ChatErrorRemoteCtrl RCEBadState @@ -482,7 +483,7 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) let remoteCtrlId_ = remoteCtrlId' <$> rc_ in Right RCSessionPendingConfirmation {remoteCtrlId_, ctrlDeviceName = ctrlName, rcsClient, tls, sessionCode, rcsWaitSession, rcsWaitConfirmation} _ -> Left $ ChatErrorRemoteCtrl RCEBadState - toView CRRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode} + toView CEvtRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode} checkAppVersion CtrlAppInfo {appVersionRange} = case compatibleAppVersion hostAppVersionRange appVersionRange of Just (AppCompatible v) -> pure v @@ -496,7 +497,7 @@ parseCtrlAppInfo :: JT.Value -> CM CtrlAppInfo parseCtrlAppInfo ctrlAppInfo = do liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo -handleRemoteCommand :: (ByteString -> CM' ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> CM' () +handleRemoteCommand :: (ByteString -> CM' (Either ChatError ChatResponse)) -> RemoteCrypto -> TBQueue (Either ChatError ChatEvent) -> HTTP2Request -> CM' () handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do logDebug "handleRemoteCommand" liftIO (tryRemoteError' parseRequest) >>= \case @@ -510,7 +511,7 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque parseRequest = do (rfKN, header, getNext) <- parseDecryptHTTP2Body encryption request reqBody (rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecode header) - replyError = reply . RRChatResponse . CRChatCmdError Nothing + replyError = reply . RRChatResponse . RRError processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM () processCommand user rfKN getNext = \case RCSend {command} -> lift $ handleSend execChatCommand command >>= reply @@ -527,7 +528,7 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque send resp attach sfKN send flush - Left e -> toView' . CRChatError Nothing . ChatErrorRemoteCtrl $ RCEProtocolError e + Left e -> eToView' $ ChatErrorRemoteCtrl $ RCEProtocolError e takeRCStep :: RCStepTMVar a -> CM a takeRCStep = liftError' (\e -> ChatErrorAgent {agentError = RCP e, connectionEntity_ = Nothing}) . atomically . takeTMVar @@ -549,17 +550,17 @@ tryRemoteError' :: ExceptT RemoteProtocolError IO a -> IO (Either RemoteProtocol tryRemoteError' = tryAllErrors' (RPEException . tshow) {-# INLINE tryRemoteError' #-} -handleSend :: (ByteString -> CM' ChatResponse) -> Text -> CM' RemoteResponse +handleSend :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM' RemoteResponse handleSend execChatCommand command = do logDebug $ "Send: " <> tshow command -- execChatCommand checks for remote-allowed commands -- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper - RRChatResponse <$> execChatCommand (encodeUtf8 command) + RRChatResponse . eitherToResult <$> execChatCommand (encodeUtf8 command) -handleRecv :: Int -> TBQueue ChatResponse -> IO RemoteResponse +handleRecv :: Int -> TBQueue (Either ChatError ChatEvent) -> IO RemoteResponse handleRecv time events = do logDebug $ "Recv: " <> tshow time - RRChatEvent <$> (timeout time . atomically $ readTBQueue events) + RRChatEvent . fmap eitherToResult <$> (timeout time . atomically $ readTBQueue events) -- TODO this command could remember stored files and return IDs to allow removing files that are not needed. -- Also, there should be some process removing unused files uploaded to remote host (possibly, all unused files). @@ -614,7 +615,7 @@ remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlDeviceName} sessionState = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState} -- | Take a look at emoji of tlsunique, commit pairing, and start session server -verifyRemoteCtrlSession :: (ByteString -> CM' ChatResponse) -> Text -> CM RemoteCtrlInfo +verifyRemoteCtrlSession :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo verifyRemoteCtrlSession execChatCommand sessCode' = do (sseq, client, ctrlName, sessionCode, vars) <- chatReadVar remoteCtrlSession >>= \case @@ -675,7 +676,7 @@ cancelActiveRemoteCtrl handlerInfo_ = handleAny (logError . tshow) $ do forM_ session_ $ \session -> do liftIO $ cancelRemoteCtrl handlingError session forM_ (snd <$> handlerInfo_) $ \rcStopReason -> - toView CRRemoteCtrlStopped {rcsState = rcsSessionState session, rcStopReason} + toView CEvtRemoteCtrlStopped {rcsState = rcsSessionState session, rcStopReason} where handlingError = isJust handlerInfo_ diff --git a/src/Simplex/Chat/Remote/Protocol.hs b/src/Simplex/Chat/Remote/Protocol.hs index 00fc56f897..207eade665 100644 --- a/src/Simplex/Chat/Remote/Protocol.hs +++ b/src/Simplex/Chat/Remote/Protocol.hs @@ -16,11 +16,11 @@ import Control.Monad.Except import Control.Monad.Reader import Crypto.Hash (SHA512) import qualified Crypto.Hash as CH -import Data.Aeson ((.=)) +import Data.Aeson (FromJSON (..), ToJSON (..), (.=)) import qualified Data.Aeson as J import qualified Data.Aeson.Key as JK import qualified Data.Aeson.KeyMap as JM -import Data.Aeson.TH (deriveJSON) +import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.Types as JT import qualified Data.ByteArray as BA import Data.ByteString (ByteString) @@ -42,7 +42,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.Lazy (LazyByteString) import Simplex.Messaging.Encoding -import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON, pattern SingleFieldJSONTag, pattern TaggedObjectJSONData, pattern TaggedObjectJSONTag) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON, pattern SingleFieldJSONTag, pattern TaggedObjectJSONData, pattern TaggedObjectJSONTag) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (TSbChainKeys) import Simplex.Messaging.Transport.Buffer (getBuffered) @@ -64,16 +64,40 @@ data RemoteCommand deriving (Show) data RemoteResponse - = RRChatResponse {chatResponse :: ChatResponse} - | RRChatEvent {chatEvent :: Maybe ChatResponse} -- 'Nothing' on poll timeout + = RRChatResponse {chatResponse :: RRResult ChatResponse} + | RRChatEvent {chatEvent :: Maybe (RRResult ChatEvent)} -- 'Nothing' on poll timeout | RRFileStored {filePath :: String} | RRFile {fileSize :: Word32, fileDigest :: FileDigest} -- provides attachment , fileDigest :: FileDigest | RRProtocolError {remoteProcotolError :: RemoteProtocolError} -- The protocol error happened on the server side deriving (Show) +data RRResult r + = RRResult {result :: r} + | RRError {error :: ChatError} + deriving (Show) + +resultToEither :: RRResult r -> Either ChatError r +resultToEither = \case + RRResult r -> Right r + RRError e -> Left e +{-# INLINE resultToEither #-} + +eitherToResult :: Either ChatError r -> RRResult r +eitherToResult = either RRError RRResult +{-# INLINE eitherToResult #-} + +$(pure []) + -- Force platform-independent encoding as the types aren't UI-visible -$(deriveJSON (taggedObjectJSON $ dropPrefix "RC") ''RemoteCommand) -$(deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse) +instance ToJSON r => ToJSON (RRResult r) where + toEncoding = $(JQ.mkToEncoding (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult) + toJSON = $(JQ.mkToJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult) + +instance FromJSON r => FromJSON (RRResult r) where + parseJSON = $(JQ.mkParseJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult) + +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "RC") ''RemoteCommand) +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse) -- * Client side / desktop @@ -109,16 +133,16 @@ closeRemoteHostClient RemoteHostClient {httpClient} = closeHTTP2Client httpClien -- ** Commands -remoteSend :: RemoteHostClient -> ByteString -> ExceptT RemoteProtocolError IO ChatResponse +remoteSend :: RemoteHostClient -> ByteString -> ExceptT RemoteProtocolError IO (Either ChatError ChatResponse) remoteSend c cmd = sendRemoteCommand' c Nothing RCSend {command = decodeUtf8 cmd} >>= \case - RRChatResponse cr -> pure cr + RRChatResponse cr -> pure $ resultToEither cr r -> badResponse r -remoteRecv :: RemoteHostClient -> Int -> ExceptT RemoteProtocolError IO (Maybe ChatResponse) +remoteRecv :: RemoteHostClient -> Int -> ExceptT RemoteProtocolError IO (Maybe (Either ChatError ChatEvent)) remoteRecv c ms = sendRemoteCommand' c Nothing RCRecv {wait = ms} >>= \case - RRChatEvent cr_ -> pure cr_ + RRChatEvent cEvt_ -> pure $ resultToEither <$> cEvt_ r -> badResponse r remoteStoreFile :: RemoteHostClient -> FilePath -> FilePath -> ExceptT RemoteProtocolError IO FilePath @@ -172,7 +196,7 @@ convertJSON :: PlatformEncoding -> PlatformEncoding -> J.Value -> J.Value convertJSON _remote@PEKotlin _local@PEKotlin = id convertJSON PESwift PESwift = id convertJSON PESwift PEKotlin = owsf2tagged -convertJSON PEKotlin PESwift = error "unsupported convertJSON: K/S" -- guarded by handshake +convertJSON PEKotlin PESwift = Prelude.error "unsupported convertJSON: K/S" -- guarded by handshake -- | Convert swift single-field sum encoding into tagged/discriminator-field owsf2tagged :: J.Value -> J.Value diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index d43f7169ea..5c177969b9 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} @@ -12,6 +13,7 @@ module Simplex.Chat.Store.Connections ( getChatLockEntity, getConnectionEntity, getConnectionEntityByConnReq, + getConnectionEntityViaShortLink, getContactConnEntityByConnReqHash, getConnectionsToSubscribe, unsetConnectionToSubscribe, @@ -33,7 +35,7 @@ import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Types -import Simplex.Messaging.Agent.Protocol (ConnId) +import Simplex.Messaging.Agent.Protocol (ConnId, ConnShortLink, ConnectionMode (..)) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -134,7 +136,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -204,6 +206,26 @@ getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1" (userId, cReqSchema1, cReqSchema2) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_ +getConnectionEntityViaShortLink :: DB.Connection -> VersionRangeChat -> User -> ConnShortLink 'CMInvitation -> IO (Maybe (ConnReqInvitation, ConnectionEntity)) +getConnectionEntityViaShortLink db vr user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do + (cReq, connId) <- ExceptT getConnReqConnId + (cReq,) <$> getConnectionEntity db vr user connId + where + getConnReqConnId = + firstRow' toConnReqConnId (SEInternalError "connection not found") $ + DB.query + db + [sql| + SELECT conn_req_inv, agent_conn_id + FROM connections + WHERE user_id = ? AND short_link_inv = ? LIMIT 1 + |] + (userId, shortLink) + -- cReq is Maybe - it is removed when connection is established + toConnReqConnId = \case + (Just cReq, connId) -> Right (cReq, connId) + _ -> Left $ SEInternalError "no connection request" + -- search connection for connection plan: -- multiple connections can have same via_contact_uri_hash if request was repeated; -- this function searches for latest connection with contact so that "known contact" plan would be chosen; diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index d473c81758..9318f62f76 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -66,7 +66,6 @@ module Simplex.Chat.Store.Direct updateContactAccepted, getUserByContactRequestId, getPendingContactConnections, - updatePCCUser, getContactConnections, getConnectionById, getConnectionsContacts, @@ -100,7 +99,7 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId) +import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), InvitationId, UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -122,7 +121,7 @@ getPendingContactConnection db userId connId = do DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND connection_id = ? @@ -148,14 +147,14 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact) -createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode chatV pqSup = do - PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode chatV pqSup +createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact) +createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash sLnk xContactId incognitoProfile subMode chatV pqSup = do + PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash sLnk xContactId incognitoProfile Nothing subMode chatV pqSup liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId) (pccConnId,) <$> getContact db vr user contactId -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection -createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup = do +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection +createConnReqConnection db userId acId cReqHash sLnk xContactId incognitoProfile groupLinkId subMode chatV pqSup = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let pccConnStatus = ConnJoined @@ -164,16 +163,16 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, - via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, + via_contact_uri_hash, via_short_link_contact, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, pccConnStatus, ConnContact, BI True, cReqHash, xContactId) + ( (userId, acId, pccConnStatus, ConnContact, BI True, cReqHash, sLnk, xContactId) :. (customUserProfileId, BI (isJust groupLinkId), groupLinkId) :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) ) pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connLinkInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) getConnReqContactXContactId db vr user@User {userId} cReqHash = do @@ -214,8 +213,8 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = do (userId, cReqHash, CSActive) mapM (addDirectChatTags db) ct_ -createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection -createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode chatV pqSup = do +createDirectConnection :: DB.Connection -> User -> ConnId -> CreatedLinkInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection +createDirectConnection db User {userId} acId ccLink@(CCLink cReq shortLinkInv) pccConnStatus incognitoProfile subMode chatV pqSup = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let contactConnInitiated = pccConnStatus == ConnNew @@ -223,15 +222,15 @@ createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile db [sql| INSERT INTO connections - (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, + (user_id, agent_conn_id, conn_req_inv, short_link_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, cReq, pccConnStatus, ConnContact, BI contactConnInitiated, customUserProfileId) + ( (userId, acId, cReq, shortLinkInv, pccConnStatus, ConnContact, BI contactConnInitiated, customUserProfileId) :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) ) pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt} + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connLinkInv = Just ccLink, localAlias = "", createdAt, updatedAt = createdAt} createIncognitoProfile :: DB.Connection -> User -> Profile -> IO Int64 createIncognitoProfile db User {userId} p = do @@ -440,19 +439,6 @@ updatePCCIncognito db User {userId} conn customUserProfileId = do (customUserProfileId, updatedAt, userId, pccConnId conn) pure (conn :: PendingContactConnection) {customUserProfileId, updatedAt} -updatePCCUser :: DB.Connection -> UserId -> PendingContactConnection -> UserId -> IO PendingContactConnection -updatePCCUser db userId conn newUserId = do - updatedAt <- getCurrentTime - DB.execute - db - [sql| - UPDATE connections - SET user_id = ?, custom_user_profile_id = NULL, updated_at = ? - WHERE user_id = ? AND connection_id = ? - |] - (newUserId, updatedAt, userId, pccConnId conn) - pure (conn :: PendingContactConnection) {customUserProfileId = Nothing, updatedAt} - deletePCCIncognitoProfile :: DB.Connection -> User -> ProfileId -> IO () deletePCCIncognitoProfile db User {userId} profileId = DB.execute @@ -904,7 +890,7 @@ getPendingContactConnections db User {userId} = do <$> DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND conn_type = ? @@ -989,7 +975,7 @@ updateConnectionStatus_ :: DB.Connection -> Int64 -> ConnStatus -> IO () updateConnectionStatus_ db connId connStatus = do currentTs <- getCurrentTime if connStatus == ConnReady - then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId) + then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL, short_link_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId) else DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId) updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 0094e20cb8..fc23c9ef44 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -39,6 +39,7 @@ module Simplex.Chat.Store.Groups getGroup, getGroupInfo, getGroupInfoByUserContactLinkConnReq, + getGroupInfoViaUserShortLink, getGroupInfoByGroupLinkHash, updateGroupProfile, updateGroupPreferences, @@ -157,14 +158,14 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConnId, UserId) +import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff) import Simplex.Messaging.Protocol (SubscriptionMode (..)) -import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>)) +import Simplex.Messaging.Util (eitherToMaybe, firstRow', ($>>=), (<$$>)) import Simplex.Messaging.Version import UnliftIO.STM #if defined(dbPostgres) @@ -175,21 +176,21 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) +type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt)) = Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences) :. (createdAt, updatedAt)) toMaybeGroupMember _ _ = Nothing -createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () -createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId memberRole subMode = +createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () +createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId (CCLink cReq shortLink) groupLinkId memberRole subMode = checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do currentTs <- getCurrentTime DB.execute db - "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, BI True, currentTs, currentTs) + "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" + (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, shortLink, memberRole, BI True, currentTs, currentTs) userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff @@ -250,12 +251,12 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do (userId, groupId) DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId) -getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, ConnReqContact, GroupMemberRole) +getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, CreatedLinkContact, GroupMemberRole) getGroupLink db User {userId} gInfo@GroupInfo {groupId} = ExceptT . firstRow groupLink (SEGroupLinkNotFound gInfo) $ - DB.query db "SELECT user_contact_link_id, conn_req_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) + DB.query db "SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) where - groupLink (linkId, cReq, mRole_) = (linkId, cReq, fromMaybe GRMember mRole_) + groupLink (linkId, cReq, shortLink, mRole_) = (linkId, CCLink cReq shortLink, fromMaybe GRMember mRole_) getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId) getGroupLinkId db User {userId} GroupInfo {groupId} = @@ -276,7 +277,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = do SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -317,7 +318,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = do -- | creates completely new group with a single member - the current user createNewGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do - let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile + let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile fullGroupPreferences = mergeGroupPreferences groupPreferences currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile @@ -325,8 +326,8 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc groupId <- liftIO $ do DB.execute db - "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs) + "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) profileId <- insertedRowId db DB.execute db @@ -386,7 +387,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId) createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation_ = do - let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile + let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile fullGroupPreferences = mergeGroupPreferences groupPreferences ExceptT $ withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do @@ -394,8 +395,8 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ groupId <- liftIO $ do DB.execute db - "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs) + "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) profileId <- insertedRowId db DB.execute db @@ -553,13 +554,13 @@ createGroupViaLink' (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId where insertGroup_ currentTs = ExceptT $ do - let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile + let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do liftIO $ do DB.execute db - "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs) + "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) profileId <- insertedRowId db DB.execute db @@ -762,7 +763,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do [sql| SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, @@ -1543,7 +1544,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -1600,7 +1601,7 @@ getViaGroupContact db vr user@User {userId} GroupMember {groupMemberId} = do maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db vr user) contactId_ updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo -updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences} +updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences, memberAdmission} | displayName == newName = liftIO $ do currentTs <- getCurrentTime updateGroupProfile_ currentTs @@ -1618,14 +1619,14 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, db [sql| UPDATE group_profiles - SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, updated_at = ? + SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ? ) |] - (newName, fullName, description, image, groupPreferences, currentTs, userId, groupId) + (newName, fullName, description, image, groupPreferences, memberAdmission, currentTs, userId, groupId) updateGroup_ ldn currentTs = do DB.execute db @@ -1663,14 +1664,14 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName DB.query db [sql| - SELECT gp.display_name, gp.full_name, gp.description, gp.image, gp.preferences + SELECT gp.display_name, gp.full_name, gp.description, gp.image, gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id WHERE g.group_id = ? |] (Only groupId) - toGroupProfile (displayName, fullName, description, image, groupPreferences) = - GroupProfile {displayName, fullName, description, image, groupPreferences} + toGroupProfile (displayName, fullName, description, image, groupPreferences, memberAdmission) = + GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo getGroupInfo db vr User {userId, userContactId} groupId = ExceptT $ do @@ -1683,8 +1684,9 @@ getGroupInfo db vr User {userId, userContactId} groupId = ExceptT $ do getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do + -- fmap join is to support group_id = NULL if non-group contact request is sent to this function (e.g., if client data is appended). groupId_ <- - maybeFirstRow fromOnly $ + fmap join . maybeFirstRow fromOnly $ DB.query db [sql| @@ -1695,6 +1697,26 @@ getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReq (userId, cReqSchema1, cReqSchema2) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_ +getGroupInfoViaUserShortLink :: DB.Connection -> VersionRangeChat -> User -> ShortLinkContact -> IO (Maybe (ConnReqContact, GroupInfo)) +getGroupInfoViaUserShortLink db vr user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do + (cReq, groupId) <- ExceptT getConnReqGroup + (cReq,) <$> getGroupInfo db vr user groupId + where + getConnReqGroup = + firstRow' toConnReqGroupId (SEInternalError "group link not found") $ + DB.query + db + [sql| + SELECT conn_req_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? + |] + (userId, shortLink) + toConnReqGroupId = \case + -- cReq is "not null", group_id is nullable + (cReq, Just groupId) -> Right (cReq, groupId) + _ -> Left $ SEInternalError "no conn req or group ID" + getGroupInfoByGroupLinkHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do groupId_ <- diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 00158e0054..752a4a2c6d 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -21,6 +21,7 @@ module Simplex.Chat.Store.Messages -- * Message and chat item functions deleteContactCIs, getGroupFileInfo, + getGroupMemberFileInfo, deleteGroupChatItemsMessages, createNewSndMessage, createSndMsgDelivery, @@ -57,8 +58,10 @@ module Simplex.Chat.Store.Messages updateGroupCIMentions, deleteGroupChatItem, updateGroupChatItemModerated, + updateMemberCIsModerated, updateGroupCIBlockedByAdmin, markGroupChatItemDeleted, + markMemberCIsDeleted, markGroupChatItemBlocked, markGroupCIBlockedByAdmin, markMessageReportsDeleted, @@ -159,7 +162,7 @@ import Simplex.Chat.Store.NoteFolders import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Shared -import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId) +import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, ConnShortLink, ConnectionMode (..), MsgMeta (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -168,7 +171,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM #if defined(dbPostgres) -import Database.PostgreSQL.Simple (FromRow, Only (..), Query, ToRow, (:.) (..)) +import Database.PostgreSQL.Simple (FromRow, In (..), Only (..), Query, ToRow, (:.) (..)) import Database.PostgreSQL.Simple.SqlQQ (sql) #else import Database.SQLite.Simple (FromRow, Only (..), Query, ToRow, (:.) (..)) @@ -193,6 +196,11 @@ getGroupFileInfo db User {userId} GroupInfo {groupId} = map toFileInfo <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ?") (userId, groupId) +getGroupMemberFileInfo :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO [CIFileInfo] +getGroupMemberFileInfo db User {userId} GroupInfo {groupId} GroupMember {groupMemberId} = + map toFileInfo + <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ? AND i.group_member_id = ?") (userId, groupId, groupMemberId) + deleteGroupChatItemsMessages :: DB.Connection -> User -> GroupInfo -> IO () deleteGroupChatItemsMessages db User {userId} GroupInfo {groupId} = do DB.execute db "DELETE FROM messages WHERE group_id = ?" (Only groupId) @@ -958,7 +966,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of [sql| SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, - custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND conn_type = ? @@ -974,7 +982,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of PTLast count -> DB.query db (query <> " ORDER BY updated_at DESC LIMIT ?") (params search :. Only count) PTAfter ts count -> DB.query db (query <> " AND updated_at > ? ORDER BY updated_at ASC LIMIT ?") (params search :. (ts, count)) PTBefore ts count -> DB.query db (query <> " AND updated_at < ? ORDER BY updated_at DESC LIMIT ?") (params search :. (ts, count)) - toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChatPreviewData + toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, Maybe (ConnShortLink 'CMInvitation), LocalAlias, UTCTime, UTCTime) -> AChatPreviewData toPreview connRow = let conn@PendingContactConnection {updatedAt} = toPendingContactConnection connRow aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] emptyChatStats @@ -2150,18 +2158,18 @@ deleteDirectChatItem db User {userId} Contact {contactId} ci = do (userId, contactId, itemId) deleteChatItemMessages_ :: DB.Connection -> ChatItemId -> IO () -deleteChatItemMessages_ db itemId = - DB.execute - db - [sql| - DELETE FROM messages - WHERE message_id IN ( - SELECT message_id - FROM chat_item_messages - WHERE chat_item_id = ? - ) - |] - (Only itemId) +deleteChatItemMessages_ db itemId = DB.execute db deleteChatItemMessagesQuery (Only itemId) + +deleteChatItemMessagesQuery :: Query +deleteChatItemMessagesQuery = + [sql| + DELETE FROM messages + WHERE message_id IN ( + SELECT message_id + FROM chat_item_messages + WHERE chat_item_id = ? + ) + |] deleteChatItemVersions_ :: DB.Connection -> ChatItemId -> IO () deleteChatItemVersions_ db itemId = @@ -2359,6 +2367,40 @@ updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMemb (deletedTs, groupMemberId, toContent, toText, currentTs, userId, groupId, itemId) pure ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} +updateMemberCIsModerated :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO () +updateMemberCIsModerated db User {userId} GroupInfo {groupId, membership} member byGroupMember md deletedTs = do + itemIds <- updateCIs =<< getCurrentTime +#if defined(dbPostgres) + let inItemIds = Only $ In (map fromOnly itemIds) + DB.execute db "DELETE FROM messages WHERE message_id IN (SELECT message_id FROM chat_item_messages WHERE chat_item_id IN ?)" inItemIds + DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id IN ?" inItemIds +#else + DB.executeMany db deleteChatItemMessagesQuery itemIds + DB.executeMany db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" itemIds +#endif + where + memId = groupMemberId' member + updateQuery = + [sql| + UPDATE chat_items + SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + |] + updateCIs :: UTCTime -> IO [Only Int64] + updateCIs currentTs + | memId == groupMemberId' membership = + DB.query + db + (updateQuery <> " AND group_member_id IS NULL AND item_sent = 1 RETURNING chat_item_id") + (columns :. (userId, groupId)) + | otherwise = + DB.query + db + (updateQuery <> " AND group_member_id = ? RETURNING chat_item_id") + (columns :. (userId, groupId, memId)) + where + columns = (deletedTs, groupMemberId' byGroupMember, msgDirToModeratedContent_ md, ciModeratedText, currentTs) + updateGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> UTCTime -> IO (ChatItem 'CTGroup d) updateGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci deletedTs = do currentTs <- getCurrentTime @@ -2405,6 +2447,31 @@ markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} (DBCIDeleted, deletedTs, deletedByGroupMemberId, currentTs, userId, groupId, itemId) pure ci {meta = meta {itemDeleted, editable = False, deletable = False}} +markMemberCIsDeleted :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> UTCTime -> IO () +markMemberCIsDeleted db User {userId} GroupInfo {groupId, membership} member byGroupMember deletedTs = + updateCIs =<< getCurrentTime + where + memId = groupMemberId' member + updateQuery = + [sql| + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + |] + updateCIs currentTs + | memId == groupMemberId' membership = + DB.execute + db + (updateQuery <> " AND group_member_id IS NULL AND item_sent = 1") + (columns :. (userId, groupId)) + | otherwise = + DB.execute + db + (updateQuery <> " AND group_member_id = ?") + (columns :. (userId, groupId, memId)) + where + columns = (DBCIDeleted, deletedTs, groupMemberId' byGroupMember, currentTs) + markGroupChatItemBlocked :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv) markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do deletedTs <- getCurrentTime @@ -2826,7 +2893,7 @@ getGroupCIMentions db ciId = SELECT r.display_name, r.member_id, m.group_member_id, m.member_role, p.display_name, p.local_alias FROM chat_item_mentions r LEFT JOIN group_members m ON r.group_id = m.group_id AND r.member_id = m.member_id - LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) WHERE r.chat_item_id = ? |] (Only ciId) diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 285a952279..c392c17db1 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -5,11 +5,15 @@ module Simplex.Chat.Store.Postgres.Migrations (migrations) where import Data.List (sortOn) import Data.Text (Text) import Simplex.Chat.Store.Postgres.Migrations.M20241220_initial +import Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links +import Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] schemaMigrations = - [ ("20241220_initial", m20241220_initial, Nothing) + [ ("20241220_initial", m20241220_initial, Nothing), + ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), + ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250402_short_links.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250402_short_links.hs new file mode 100644 index 0000000000..4b3b7e9640 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250402_short_links.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250402_short_links :: Text +m20250402_short_links = + T.pack + [r| +ALTER TABLE user_contact_links ADD COLUMN short_link_contact BYTEA; +ALTER TABLE connections ADD COLUMN short_link_inv BYTEA; +ALTER TABLE connections ADD COLUMN via_short_link_contact BYTEA; +|] + +down_m20250402_short_links :: Text +down_m20250402_short_links = + T.pack + [r| +ALTER TABLE user_contact_links DROP COLUMN short_link_contact; +ALTER TABLE connections DROP COLUMN short_link_inv; +ALTER TABLE connections DROP COLUMN via_short_link_contact; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250512_member_admission.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250512_member_admission.hs new file mode 100644 index 0000000000..eb0d73a523 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250512_member_admission.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250512_member_admission :: Text +m20250512_member_admission = + T.pack + [r| +ALTER TABLE group_profiles ADD COLUMN member_admission TEXT; +|] + +down_m20250512_member_admission :: Text +down_m20250512_member_admission = + T.pack + [r| +ALTER TABLE group_profiles DROP COLUMN member_admission; +|] diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 22d2a7b1f5..a7dc154d9d 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -50,6 +50,7 @@ module Simplex.Chat.Store.Profiles getUserContactLinkById, getGroupLinkInfo, getUserContactLinkByConnReq, + getUserContactLinkViaShortLink, getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, @@ -100,7 +101,7 @@ import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) -import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -326,11 +327,13 @@ setUserProfileContactLink db user@User {userId, profile = p@LocalProfile {profil SET contact_link = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? |] - (connReqContact_, ts, userId, profileId) - pure (user :: User) {profile = p {contactLink = connReqContact_}} + (contactLink, ts, userId, profileId) + pure (user :: User) {profile = p {contactLink}} where - connReqContact_ = case ucl_ of - Just UserContactLink {connReqContact} -> Just connReqContact + -- TODO [short links] this should be replaced with short links once they are supported by all clients. + -- Or, maybe, we want to allow both, when both are optional. + contactLink = case ucl_ of + Just UserContactLink {connLinkContact = CCLink cReq _} -> Just $ CLFull cReq _ -> Nothing -- only used in tests @@ -346,17 +349,17 @@ getUserContactProfiles db User {userId} = |] (Only userId) where - toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) -> Profile + toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) -> Profile toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences} -createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> SubscriptionMode -> ExceptT StoreError IO () -createUserContactLink db User {userId} agentConnId cReq subMode = +createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> ExceptT StoreError IO () +createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode = checkConstraint SEDuplicateContactLink . liftIO $ do currentTs <- getCurrentTime DB.execute db - "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" - (userId, cReq, currentTs, currentTs) + "INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, created_at, updated_at) VALUES (?,?,?,?,?)" + (userId, cReq, shortLink, currentTs, currentTs) userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff @@ -450,7 +453,7 @@ data UserMsgReceiptSettings = UserMsgReceiptSettings deriving (Show) data UserContactLink = UserContactLink - { connReqContact :: ConnReqContact, + { connLinkContact :: CreatedLinkContact, autoAccept :: Maybe AutoAccept } deriving (Show) @@ -472,35 +475,27 @@ $(J.deriveJSON defaultJSON ''AutoAccept) $(J.deriveJSON defaultJSON ''UserContactLink) -toUserContactLink :: (ConnReqContact, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink -toUserContactLink (connReq, BI autoAccept, BI businessAddress, BI acceptIncognito, autoReply) = - UserContactLink connReq $ +toUserContactLink :: (ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink +toUserContactLink (connReq, shortLink, BI autoAccept, BI businessAddress, BI acceptIncognito, autoReply) = + UserContactLink (CCLink connReq shortLink) $ if autoAccept then Just AutoAccept {businessAddress, acceptIncognito, autoReply} else Nothing getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink getUserAddress db User {userId} = ExceptT . firstRow toUserContactLink SEUserContactLinkNotFound $ - DB.query - db - [sql| - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content - FROM user_contact_links - WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL - |] - (Only userId) + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL") (Only userId) getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupLinkInfo) getUserContactLinkById db userId userContactLinkId = ExceptT . firstRow (\(ucl :. gli) -> (toUserContactLink ucl, toGroupLinkInfo gli)) SEUserContactLinkNotFound $ - DB.query db (groupLinkInfoQuery <> " AND user_contact_link_id = ?") (userId, userContactLinkId) - -groupLinkInfoQuery :: Query -groupLinkInfoQuery = - [sql| - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role - FROM user_contact_links - WHERE user_id = ? - |] + DB.query + db + [sql| + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? AND user_contact_link_id = ? + |] + (userId, userContactLinkId) toGroupLinkInfo :: (Maybe GroupId, Maybe GroupMemberRole) -> Maybe GroupLinkInfo toGroupLinkInfo (groupId_, mRole_) = @@ -510,19 +505,31 @@ toGroupLinkInfo (groupId_, mRole_) = getGroupLinkInfo :: DB.Connection -> UserId -> GroupId -> IO (Maybe GroupLinkInfo) getGroupLinkInfo db userId groupId = fmap join $ maybeFirstRow toGroupLinkInfo $ - DB.query db (groupLinkInfoQuery <> " AND group_id = ?") (userId, groupId) + DB.query + db + [sql| + SELECT group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? AND group_id = ? + |] + (userId, groupId) getUserContactLinkByConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink) getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = maybeFirstRow toUserContactLink $ - DB.query - db - [sql| - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content - FROM user_contact_links - WHERE user_id = ? AND conn_req_contact IN (?,?) - |] - (userId, cReqSchema1, cReqSchema2) + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND conn_req_contact IN (?,?)") (userId, cReqSchema1, cReqSchema2) + +getUserContactLinkViaShortLink :: DB.Connection -> User -> ShortLinkContact -> IO (Maybe UserContactLink) +getUserContactLinkViaShortLink db User {userId} shortLink = + maybeFirstRow toUserContactLink $ + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink) + +userContactLinkQuery :: Query +userContactLinkQuery = + [sql| + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content + FROM user_contact_links + |] getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 5865cd180e..183d699f01 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -128,6 +128,8 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250122_chat_items_include_in_hist import Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions import Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts import Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links +import Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -255,7 +257,9 @@ schemaMigrations = ("20250122_chat_items_include_in_history", m20250122_chat_items_include_in_history, Just down_m20250122_chat_items_include_in_history), ("20250126_mentions", m20250126_mentions, Just down_m20250126_mentions), ("20250129_delete_unused_contacts", m20250129_delete_unused_contacts, Just down_m20250129_delete_unused_contacts), - ("20250130_indexes", m20250130_indexes, Just down_m20250130_indexes) + ("20250130_indexes", m20250130_indexes, Just down_m20250130_indexes), + ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), + ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250402_short_links.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250402_short_links.hs new file mode 100644 index 0000000000..62637c0782 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250402_short_links.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250402_short_links :: Query +m20250402_short_links = + [sql| +ALTER TABLE user_contact_links ADD COLUMN short_link_contact BLOB; +ALTER TABLE connections ADD COLUMN short_link_inv BLOB; +ALTER TABLE connections ADD COLUMN via_short_link_contact BLOB; + +|] + +down_m20250402_short_links :: Query +down_m20250402_short_links = + [sql| +ALTER TABLE user_contact_links DROP COLUMN short_link_contact; +ALTER TABLE connections DROP COLUMN short_link_inv; +ALTER TABLE connections DROP COLUMN via_short_link_contact; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250512_member_admission.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250512_member_admission.hs new file mode 100644 index 0000000000..e1f45beef1 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250512_member_admission.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250512_member_admission :: Query +m20250512_member_admission = + [sql| +ALTER TABLE group_profiles ADD COLUMN member_admission TEXT; +|] + +down_m20250512_member_admission :: Query +down_m20250512_member_admission = + [sql| +ALTER TABLE group_profiles DROP COLUMN member_admission; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index 31bc2feb81..a85ba4a4cb 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -443,6 +443,22 @@ Query: Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) +Query: + SELECT link_id, snd_private_key + FROM inv_short_links + WHERE host = ? AND port = ? AND snd_id = ? + +Plan: +SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=?) + +Query: + SELECT link_key, snd_private_key, snd_id + FROM inv_short_links + WHERE host = ? AND port = ? AND link_id = ? + +Plan: +SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?) + Query: SELECT s.internal_id, m.msg_type, s.internal_hash, s.rcpt_internal_id, s.rcpt_status FROM snd_messages s @@ -466,6 +482,19 @@ Query: Plan: +Query: + INSERT INTO inv_short_links + (host, port, server_key_hash, link_id, link_key, snd_private_key, snd_id) + VALUES (?,?,?,?,?,?,?) + ON CONFLICT (host, port, link_id) + DO UPDATE SET + server_key_hash = EXCLUDED.server_key_hash, + link_key = EXCLUDED.link_key, + snd_private_key = EXCLUDED.snd_private_key, + snd_id = EXCLUDED.snd_id + +Plan: + Query: INSERT INTO messages (conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body, pq_encryption) @@ -524,7 +553,10 @@ SEARCH messages USING COVERING INDEX idx_messages_conn_id_internal_rcv_id (conn_ Query: INSERT INTO rcv_queues - (host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, snd_secure, status, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); + ( host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, + snd_id, queue_mode, status, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash, + link_id, link_key, link_priv_sig_key, link_enc_fixed_data + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); Plan: @@ -546,14 +578,14 @@ SEARCH messages USING COVERING INDEX idx_messages_conn_id_internal_snd_id (conn_ Query: INSERT INTO snd_queues - (host, port, snd_id, snd_secure, conn_id, snd_public_key, snd_private_key, e2e_pub_key, e2e_dh_secret, + (host, port, snd_id, queue_mode, conn_id, snd_public_key, snd_private_key, e2e_pub_key, e2e_dh_secret, status, snd_queue_id, snd_primary, replace_snd_queue_id, smp_client_version, server_key_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT (host, port, snd_id) DO UPDATE SET host=EXCLUDED.host, port=EXCLUDED.port, snd_id=EXCLUDED.snd_id, - snd_secure=EXCLUDED.snd_secure, + queue_mode=EXCLUDED.queue_mode, conn_id=EXCLUDED.conn_id, snd_public_key=EXCLUDED.snd_public_key, snd_private_key=EXCLUDED.snd_private_key, @@ -631,6 +663,14 @@ Query: Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) +Query: + UPDATE inv_short_links + SET snd_id = ? + WHERE host = ? AND port = ? AND link_id = ? + +Plan: +SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?) + Query: UPDATE ratchets SET x3dh_priv_key_1 = ?, x3dh_priv_key_2 = ?, pq_priv_kem = ? @@ -691,7 +731,7 @@ SEARCH snd_queues USING PRIMARY KEY (host=? AND port=? AND snd_id=?) Query: SELECT - c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.snd_id, q.snd_secure, + c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.snd_id, q.queue_mode, q.snd_public_key, q.snd_private_key, q.e2e_pub_key, q.e2e_dh_secret, q.status, q.snd_queue_id, q.snd_primary, q.replace_snd_queue_id, q.switch_status, q.smp_client_version FROM snd_queues q @@ -705,9 +745,10 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, - q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port JOIN connections c ON q.conn_id = c.conn_id @@ -719,9 +760,10 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, - q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port JOIN connections c ON q.conn_id = c.conn_id @@ -733,9 +775,10 @@ SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, - q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port JOIN connections c ON q.conn_id = c.conn_id @@ -747,9 +790,10 @@ SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, - q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port JOIN connections c ON q.conn_id = c.conn_id @@ -761,9 +805,10 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, - q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port JOIN connections c ON q.conn_id = c.conn_id @@ -799,6 +844,10 @@ Query: DELETE FROM deleted_snd_chunk_replicas WHERE deleted_snd_chunk_replica_id Plan: SEARCH deleted_snd_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) +Query: DELETE FROM inv_short_links WHERE host = ? AND port = ? AND link_id = ? +Plan: +SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?) + Query: DELETE FROM messages WHERE conn_id = ? AND internal_id = ?; Plan: SEARCH messages USING PRIMARY KEY (conn_id=? AND internal_id=?) @@ -1022,10 +1071,6 @@ Query: UPDATE connections SET smp_agent_version = ? WHERE conn_id = ? Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) -Query: UPDATE connections SET user_id = ? WHERE conn_id = ? and user_id = ? -Plan: -SEARCH connections USING PRIMARY KEY (conn_id=?) - Query: UPDATE messages SET msg_body = x'' WHERE conn_id = ? AND internal_id = ? Plan: SEARCH messages USING PRIMARY KEY (conn_id=? AND internal_id=?) @@ -1070,9 +1115,9 @@ Query: UPDATE rcv_messages SET user_ack = ? WHERE conn_id = ? AND internal_id = Plan: SEARCH rcv_messages USING COVERING INDEX idx_rcv_messages_conn_id_internal_id (conn_id=? AND internal_id=?) -Query: UPDATE rcv_queues SET last_broker_ts = ? WHERE conn_id = ? AND rcv_queue_id = ? +Query: UPDATE rcv_queues SET last_broker_ts = ? WHERE conn_id = ? AND rcv_queue_id = ? AND (last_broker_ts IS NULL OR last_broker_ts < ?) Plan: -SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=? AND rcv_queue_id=?) +SEARCH rcv_queues USING INDEX idx_rcv_queue_id (conn_id=? AND rcv_queue_id=?) Query: UPDATE rcv_queues SET rcv_primary = ? WHERE conn_id = ? Plan: diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 88836035e5..e9ade30f93 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -35,7 +35,7 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -497,6 +497,14 @@ Plan: SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?) USE TEMP B-TREE FOR ORDER BY +Query: + SELECT conn_req_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? + +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) + Query: SELECT conn_req_contact, group_id FROM user_contact_links @@ -505,6 +513,14 @@ Query: Plan: SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT conn_req_inv, agent_conn_id + FROM connections + WHERE user_id = ? AND short_link_inv = ? LIMIT 1 + +Plan: +SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) + Query: SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, @@ -528,7 +544,7 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT gp.display_name, gp.full_name, gp.description, gp.image, gp.preferences + SELECT gp.display_name, gp.full_name, gp.description, gp.image, gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id WHERE g.group_id = ? @@ -789,7 +805,7 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -834,7 +850,7 @@ SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group Query: SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, @@ -1219,7 +1235,7 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE group_profiles - SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, updated_at = ? + SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups @@ -1347,7 +1363,7 @@ SCAN cc Query: SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, - custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND conn_type = ? @@ -1364,7 +1380,7 @@ SEARCH connections USING INDEX idx_connections_updated_at (user_id=? AND updated Query: SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, - custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND conn_type = ? @@ -1381,7 +1397,7 @@ SEARCH connections USING INDEX idx_connections_updated_at (user_id=? AND updated Query: SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, - custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND conn_type = ? @@ -2935,20 +2951,12 @@ Plan: SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role FROM user_contact_links - WHERE user_id = ? AND conn_req_contact IN (?,?) + WHERE user_id = ? AND user_contact_link_id = ? Plan: -SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) - -Query: - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content - FROM user_contact_links - WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL - -Plan: -SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, @@ -2962,7 +2970,7 @@ Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND conn_type = ? @@ -2972,7 +2980,7 @@ Plan: SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) Query: - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND connection_id = ? @@ -3178,7 +3186,7 @@ Query: SELECT r.display_name, r.member_id, m.group_member_id, m.member_role, p.display_name, p.local_alias FROM chat_item_mentions r LEFT JOIN group_members m ON r.group_id = m.group_id AND r.member_id = m.member_id - LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) WHERE r.chat_item_id = ? Plan: @@ -3314,6 +3322,22 @@ Query: Plan: SEARCH usage_conditions USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE chat_items + SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_member_id = ? RETURNING chat_item_id +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + +Query: + UPDATE chat_items + SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_member_id IS NULL AND item_sent = 1 RETURNING chat_item_id +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + Query: UPDATE chat_items SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? @@ -3322,6 +3346,22 @@ Query: Plan: SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_member_id = ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_member_id IS NULL AND item_sent = 1 +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + Query: UPDATE chat_items SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? @@ -3861,25 +3901,6 @@ LIST SUBQUERY 1 SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) SEARCH groups USING COVERING INDEX idx_groups_group_profile_id (group_profile_id=?) -Query: - DELETE FROM messages - WHERE message_id IN ( - SELECT message_id - FROM chat_item_messages - WHERE chat_item_id = ? - ) - -Plan: -SEARCH messages USING INTEGER PRIMARY KEY (rowid=?) -LIST SUBQUERY 1 -SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) -SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_message_id (message_id=?) -SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_created_by_msg_id (created_by_msg_id=?) -SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_created_by_msg_id (created_by_msg_id=?) -SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_1 (message_id=?) -SEARCH chat_items USING COVERING INDEX sqlite_autoindex_chat_items_1 (created_by_msg_id=?) -SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_message_id (message_id=?) - Query: INSERT INTO calls (contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts, user_id, created_at, updated_at) @@ -3923,9 +3944,9 @@ Plan: Query: INSERT INTO connections - (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, + (user_id, agent_conn_id, conn_req_inv, short_link_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -3941,9 +3962,9 @@ Plan: Query: INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, - via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, + via_contact_uri_hash, via_short_link_contact, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -4194,14 +4215,6 @@ Query: Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) -Query: - UPDATE connections - SET user_id = ?, custom_user_profile_id = NULL, updated_at = ? - WHERE user_id = ? AND connection_id = ? - -Plan: -SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE contact_profiles SET contact_link = ?, updated_at = ? @@ -4414,11 +4427,30 @@ Query: Plan: SEARCH xftp_file_descriptions USING INTEGER PRIMARY KEY (rowid=?) +Query: + DELETE FROM messages + WHERE message_id IN ( + SELECT message_id + FROM chat_item_messages + WHERE chat_item_id = ? + ) + +Plan: +SEARCH messages USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_message_id (message_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_1 (message_id=?) +SEARCH chat_items USING COVERING INDEX sqlite_autoindex_chat_items_1 (created_by_msg_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_message_id (message_id=?) + Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -4440,7 +4472,7 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -4611,12 +4643,25 @@ CORRELATED SCALAR SUBQUERY 1 SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) Query: - SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content FROM user_contact_links - WHERE user_id = ? - AND user_contact_link_id = ? + WHERE user_id = ? AND conn_req_contact IN (?,?) Plan: -SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) + +Query: + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content + FROM user_contact_links + WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) + +Query: + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) Query: SELECT f.file_id, f.ci_file_status, f.file_path @@ -4654,6 +4699,15 @@ Plan: SEARCH i USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) +Query: + SELECT f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.group_id = ? AND i.group_member_id = ? +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) + Query: SELECT f.file_id, f.ci_file_status, f.file_path FROM chat_items i @@ -5325,7 +5379,7 @@ Plan: Query: INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) Plan: -Query: INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?) +Query: INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) Plan: Query: INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?) @@ -5368,10 +5422,10 @@ SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) Query: INSERT INTO temp_conn_ids (conn_id) VALUES (?) Plan: -Query: INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?) +Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, created_at, updated_at) VALUES (?,?,?,?,?) Plan: -Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) +Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) Plan: Query: INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?) @@ -5596,7 +5650,7 @@ Query: SELECT user_contact_link_id FROM contact_requests WHERE contact_request_i Plan: SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) -Query: SELECT user_contact_link_id, conn_req_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1 +Query: SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1 Plan: SEARCH user_contact_links USING INDEX idx_user_contact_links_group_id (group_id=?) @@ -5644,7 +5698,7 @@ Query: UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_i Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) -Query: UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ? +Query: UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL, short_link_inv = NULL WHERE connection_id = ? Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index f4cf2bccb8..6fbed97d27 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -108,7 +108,8 @@ CREATE TABLE group_profiles( image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE, preferences TEXT, - description TEXT NULL + description TEXT NULL, + member_admission TEXT ); CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID @@ -297,6 +298,8 @@ CREATE TABLE connections( pq_snd_enabled INTEGER, pq_rcv_enabled INTEGER, quota_err_counter INTEGER NOT NULL DEFAULT 0, + short_link_inv BLOB, + via_short_link_contact BLOB, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE @@ -316,6 +319,7 @@ CREATE TABLE user_contact_links( group_link_id BLOB, group_link_member_role TEXT NULL, business_address INTEGER DEFAULT 0, + short_link_contact BLOB, UNIQUE(user_id, local_display_name) ); CREATE TABLE contact_requests( diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index a556fd0a49..b32fd07bb5 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} @@ -35,7 +36,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConnId, UserId) +import Simplex.Messaging.Agent.Protocol (ConnId, ConnShortLink, ConnectionMode (..), CreatedConnLink (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -416,7 +417,7 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId = |] (userId, profileId, userId, profileId, userId, profileId) -type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) +type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) type ContactRow = Only ContactId :. ContactRow' @@ -441,10 +442,10 @@ getProfileById db userId profileId = |] (userId, profileId) where - toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile + toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) -> LocalProfile toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} -type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) toContactRequest :: ContactRequestRow -> UserContactRequest toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do @@ -462,7 +463,7 @@ userQuery = JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User +toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder, displayName, fullName, image, contactLink, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) = User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt, uiThemes} where @@ -470,9 +471,10 @@ toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder, dis fullPreferences = mergePreferences Nothing userPreferences viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_ -toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> PendingContactConnection -toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt) = - PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt} +toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, Maybe (ConnShortLink 'CMInvitation), LocalAlias, UTCTime, UTCTime) -> PendingContactConnection +toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, shortLinkInv, localAlias, createdAt, updatedAt) = + let connLinkInv = (`CCLink` shortLinkInv) <$> connReqInv + in PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connLinkInv, localAlias, createdAt, updatedAt} getConnReqInv :: DB.Connection -> Int64 -> ExceptT StoreError IO ConnReqInvitation getConnReqInv db connId = @@ -577,16 +579,16 @@ safeDeleteLDN db User {userId} localDisplayName = do type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64) :. GroupMemberRow -type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) +type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL) :. userMemberRow) = +toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL) :. userMemberRow) = let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences - groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} + groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} businessChat = toBusinessChatInfo businessRow in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, chatTags, chatItemTTL, uiThemes, customData} @@ -610,7 +612,7 @@ groupInfoQuery = SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 958cd9d75f..e432343839 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -9,12 +9,13 @@ module Simplex.Chat.Terminal where import Control.Monad import qualified Data.List.NonEmpty as L -import Simplex.Chat (defaultChatConfig, operatorSimpleXChat) +import Simplex.Chat (defaultChatConfig) import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Help (chatWelcome) import Simplex.Chat.Library.Commands (_defaultNtfServers) import Simplex.Chat.Operators +import Simplex.Chat.Operators.Presets (operatorSimpleXChat) import Simplex.Chat.Options import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Output diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index bf48d1d4f5..7c52f59a50 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -64,12 +64,14 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do rh' = if either (const False) allowRemoteCommand cmd then rh else Nothing unless (isMessage cmd) $ echo s r <- runReaderT (execChatCommand rh' bs) cc - processResp s cmd rh r + case r of + Right r' -> processResp cmd rh r' + Left _ -> when (isMessage cmd) $ echo s printRespToTerminal ct cc False rh r - startLiveMessage cmd r + mapM_ (startLiveMessage cmd) r where echo s = printToTerminal ct [plain s] - processResp s cmd rh = \case + processResp cmd rh = \case CRActiveUser u -> case rh of Nothing -> setActive ct "" Just rhId -> updateRemoteUser ct u rhId @@ -80,8 +82,6 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c CRGroupDeletedUser u g -> whenCurrUser cc u $ unsetActiveGroup ct g CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g - CRChatCmdError _ _ -> when (isMessage cmd) $ echo s - CRChatError _ _ -> when (isMessage cmd) $ echo s CRCmdOk _ -> case cmd of Right APIDeleteUser {} -> setActive ct "" _ -> pure () @@ -133,7 +133,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do updateLiveMessage typedMsg lm = case liveMessageToSend typedMsg lm of Just sentMsg -> sendUpdatedLiveMessage cc sentMsg lm True >>= \case - CRChatItemUpdated {} -> setLiveMessage lm {sentMsg, typedMsg} + Right CRChatItemUpdated {} -> setLiveMessage lm {sentMsg, typedMsg} _ -> do -- TODO print error setLiveMessage lm {typedMsg} @@ -147,10 +147,10 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do | otherwise = (s <> reverse (c : w), "") startLiveMessage _ _ = pure () -sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO ChatResponse +sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO (Either ChatError ChatResponse) sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg - either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc + runExceptT (processChatCommand cmd) `runReaderT` cc runTerminalInput :: ChatTerminal -> ChatController -> IO () runTerminalInput ct cc = withChatTerm ct $ do diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs index aa9adb059f..38b0e91a8a 100644 --- a/src/Simplex/Chat/Terminal/Main.hs +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} module Simplex.Chat.Terminal.Main where @@ -7,15 +8,13 @@ import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.STM import Control.Monad import Data.Maybe (fromMaybe) -import Data.Time.Clock (getCurrentTime) -import Data.Time.LocalTime (getCurrentTimeZone) import Network.Socket -import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatResponse (..), PresetServers (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatError, ChatEvent (..), PresetServers (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) import Simplex.Chat.Core import Simplex.Chat.Options import Simplex.Chat.Options.DB import Simplex.Chat.Terminal -import Simplex.Chat.View (serializeChatResponse, smpProxyModeStr) +import Simplex.Chat.View (ChatResponseEvent, smpProxyModeStr) import Simplex.Messaging.Client (NetworkConfig (..), SocksMode (..)) import System.Directory (getAppUserDataDirectory) import System.Exit (exitFailure) @@ -43,18 +42,17 @@ simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServer simplexChatTerminal cfg opts t runCommand user cc = do when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do - (_, _, r') <- atomically . readTBQueue $ outputQ cc - case r' of - CRNewChatItems {} -> printResponse r' - _ -> when (chatCmdLog == CCLAll) $ printResponse r' + (_, r) <- atomically . readTBQueue $ outputQ cc + case r of + Right CEvtNewChatItems {} -> printResponse r + _ -> when (chatCmdLog == CCLAll) $ printResponse r sendChatCmdStr cc chatCmd >>= printResponse threadDelay $ chatCmdDelay * 1000000 where + printResponse :: ChatResponseEvent r => Either ChatError r -> IO () printResponse r = do - ts <- getCurrentTime - tz <- getCurrentTimeZone rh <- readTVarIO $ currentRemoteHost cc - putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r + printResponseEvent (rh, Just user) cfg r welcome :: ChatConfig -> ChatOpts -> IO () welcome ChatConfig {presetServers = PresetServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbOptions, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} = diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 3694b20c67..79fc08397c 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -7,6 +7,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} module Simplex.Chat.Terminal.Output where @@ -146,19 +147,19 @@ withTermLock ChatTerminal {termLock} action = do runTerminalOutput :: ChatTerminal -> ChatController -> ChatOpts -> IO () runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} ChatOpts {markRead} = do forever $ do - (_, outputRH, r) <- atomically $ readTBQueue outputQ - case r of - CRNewChatItems u (ci : _) -> when markRead $ markChatItemRead u ci -- At the moment of writing received items are created one at a time - CRChatItemUpdated u ci -> when markRead $ markChatItemRead u ci - CRRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId - CRRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_ + (outputRH, r_) <- atomically $ readTBQueue outputQ + forM_ r_ $ \case + CEvtNewChatItems u (ci : _) -> when markRead $ markChatItemRead u ci -- At the moment of writing received items are created one at a time + CEvtChatItemUpdated u ci -> when markRead $ markChatItemRead u ci + CEvtRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId + CEvtRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_ _ -> pure () - let printResp = case logFilePath of - Just path -> if logResponseToFile r then logResponse path else printToTerminal ct + let printEvent = case logFilePath of + Just path -> if either (const True) logEventToFile r_ then logResponse path else printToTerminal ct _ -> printToTerminal ct liveItems <- readTVarIO showLiveItems - responseString ct cc liveItems outputRH r >>= printResp - responseNotification ct cc r + responseString ct cc liveItems outputRH r_ >>= printEvent + mapM_ (chatEventNotification ct cc) r_ where markChatItemRead u (AChatItem _ _ chat ci@ChatItem {chatDir, meta = CIMeta {itemStatus}}) = case (chatDirNtf u chat chatDir (isUserMention ci), itemStatus) of @@ -170,14 +171,14 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s getRemoteUser rhId = runReaderT (execChatCommand (Just rhId) "/user") cc >>= \case - CRActiveUser {user} -> updateRemoteUser ct user rhId + Right CRActiveUser {user} -> updateRemoteUser ct user rhId cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct) -responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO () -responseNotification t@ChatTerminal {sendNotification} cc = \case +chatEventNotification :: ChatTerminal -> ChatController -> ChatEvent -> IO () +chatEventNotification t@ChatTerminal {sendNotification} cc = \case -- At the moment of writing received items are created one at a time - CRNewChatItems u ((AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) : _) -> + CEvtNewChatItems u ((AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) : _) -> when (chatDirNtf u cInfo chatDir $ isUserMention ci) $ do whenCurrUser cc u $ setActiveChat t cInfo case (cInfo, chatDir) of @@ -186,29 +187,29 @@ responseNotification t@ChatTerminal {sendNotification} cc = \case _ -> pure () where text = msgText mc formattedText - CRChatItemUpdated u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent _}) -> + CEvtChatItemUpdated u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent _}) -> whenCurrUser cc u $ when (chatDirNtf u cInfo chatDir $ isUserMention ci) $ setActiveChat t cInfo - CRContactConnected u ct _ -> when (contactNtf u ct False) $ do + CEvtContactConnected u ct _ -> when (contactNtf u ct False) $ do whenCurrUser cc u $ setActiveContact t ct sendNtf (viewContactName ct <> "> ", "connected") - CRContactSndReady u ct -> + CEvtContactSndReady u ct -> whenCurrUser cc u $ setActiveContact t ct - CRContactAnotherClient u ct -> do + CEvtContactAnotherClient u ct -> do whenCurrUser cc u $ unsetActiveContact t ct when (contactNtf u ct False) $ sendNtf (viewContactName ct <> "> ", "connected to another client") - CRContactsDisconnected srv _ -> serverNtf srv "disconnected" - CRContactsSubscribed srv _ -> serverNtf srv "connected" - CRReceivedGroupInvitation u g ct _ _ -> + CEvtContactsDisconnected srv _ -> serverNtf srv "disconnected" + CEvtContactsSubscribed srv _ -> serverNtf srv "connected" + CEvtReceivedGroupInvitation u g ct _ _ -> when (contactNtf u ct False) $ sendNtf ("#" <> viewGroupName g <> " " <> viewContactName ct <> "> ", "invited you to join the group") - CRUserJoinedGroup u g _ -> when (groupNtf u g False) $ do + CEvtUserJoinedGroup u g _ -> when (groupNtf u g False) $ do whenCurrUser cc u $ setActiveGroup t g sendNtf ("#" <> viewGroupName g, "you are connected to group") - CRJoinedGroupMember u g m -> + CEvtJoinedGroupMember u g m -> when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected") - CRConnectedToGroupMember u g m _ -> + CEvtConnectedToGroupMember u g m _ -> when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected") - CRReceivedContactRequest u UserContactRequest {localDisplayName = n} -> + CEvtReceivedContactRequest u UserContactRequest {localDisplayName = n} -> when (userNtf u) $ sendNtf (viewName n <> ">", "wants to connect to you") _ -> pure () where @@ -271,15 +272,17 @@ whenCurrUser cc u a = do where sameUser User {userId = uId} = maybe False $ \User {userId} -> userId == uId -printRespToTerminal :: ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> ChatResponse -> IO () +printRespToTerminal :: ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> Either ChatError ChatResponse -> IO () printRespToTerminal ct cc liveItems outputRH r = responseString ct cc liveItems outputRH r >>= printToTerminal ct -responseString :: ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> ChatResponse -> IO [StyledString] -responseString ct cc liveItems outputRH r = do - cu <- getCurrentUser ct cc - ts <- getCurrentTime - tz <- getCurrentTimeZone - pure $ responseToView cu (config cc) liveItems ts tz outputRH r +responseString :: forall r. ChatResponseEvent r => ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> Either ChatError r -> IO [StyledString] +responseString ct cc liveItems outputRH = \case + Right r -> do + cu <- getCurrentUser ct cc + ts <- getCurrentTime + tz <- getCurrentTimeZone + pure $ responseToView cu (config cc) liveItems ts tz outputRH r + Left e -> pure $ chatErrorToView (isCommandResponse @r) (config cc) e updateRemoteUser :: ChatTerminal -> User -> RemoteHostId -> IO () updateRemoteUser ct user rhId = atomically $ TM.insert rhId user (currentRemoteUsers ct) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 5ba7565611..0063f711c7 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -51,13 +51,13 @@ import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) -import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId) -import Simplex.Messaging.Agent.Store.DB (Binary (..)) +import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (blobFieldDecoder, defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal #if defined(dbPostgres) @@ -220,6 +220,8 @@ contactConnId c = aConnId <$> contactConn c type IncognitoEnabled = Bool +type CreateShortLink = Bool + contactConnIncognito :: Contact -> IncognitoEnabled contactConnIncognito = maybe False connIncognito . contactConn @@ -559,7 +561,7 @@ data Profile = Profile { displayName :: ContactName, fullName :: Text, image :: Maybe ImageData, - contactLink :: Maybe ConnReqContact, + contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences -- fields that should not be read into this data type to prevent sending them as part of profile to contacts: -- - contact_profile_id @@ -592,7 +594,7 @@ data LocalProfile = LocalProfile displayName :: ContactName, fullName :: Text, image :: Maybe ImageData, - contactLink :: Maybe ConnReqContact, + contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences, localAlias :: LocalAlias } @@ -614,10 +616,24 @@ data GroupProfile = GroupProfile fullName :: Text, description :: Maybe Text, image :: Maybe ImageData, - groupPreferences :: Maybe GroupPreferences + groupPreferences :: Maybe GroupPreferences, + memberAdmission :: Maybe GroupMemberAdmission } deriving (Eq, Show) +data GroupMemberAdmission = GroupMemberAdmission + { -- names :: Maybe MemberCriteria, + -- captcha :: Maybe MemberCriteria, + review :: Maybe MemberCriteria + } + deriving (Eq, Show) + +data MemberCriteria = MCAll + deriving (Eq, Show) + +emptyGroupMemberAdmission :: GroupMemberAdmission +emptyGroupMemberAdmission = GroupMemberAdmission Nothing + newtype ImageData = ImageData Text deriving (Eq, Show) @@ -1407,6 +1423,14 @@ type ConnReqInvitation = ConnectionRequestUri 'CMInvitation type ConnReqContact = ConnectionRequestUri 'CMContact +type CreatedLinkInvitation = CreatedConnLink 'CMInvitation + +type CreatedLinkContact = CreatedConnLink 'CMContact + +type ConnLinkContact = ConnectionLink 'CMContact + +type ShortLinkContact = ConnShortLink 'CMContact + data Connection = Connection { connId :: Int64, agentConnId :: AgentConnId, @@ -1484,7 +1508,7 @@ data PendingContactConnection = PendingContactConnection viaUserContactLink :: Maybe Int64, groupLinkId :: Maybe GroupLinkId, customUserProfileId :: Maybe Int64, - connReqInv :: Maybe ConnReqInvitation, + connLinkInv :: Maybe CreatedLinkInvitation, localAlias :: Text, createdAt :: UTCTime, updatedAt :: UTCTime @@ -1806,6 +1830,16 @@ $(JQ.deriveJSON defaultJSON ''LocalProfile) $(JQ.deriveJSON defaultJSON ''UserContactRequest) +$(JQ.deriveJSON (enumJSON $ dropPrefix "MC") {J.tagSingleConstructors = True} ''MemberCriteria) + +$(JQ.deriveJSON defaultJSON ''GroupMemberAdmission) + +instance ToField GroupMemberAdmission where + toField = toField . encodeJSON + +instance FromField GroupMemberAdmission where + fromField = fromTextField_ decodeJSON + $(JQ.deriveJSON defaultJSON ''GroupProfile) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "IB") ''InvitedBy) diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index 2e704c5cf5..63d80657dc 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -32,8 +32,9 @@ import qualified Data.Text as T import GHC.Records.Compat import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types.Shared +import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (blobFieldDecoder, defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>)) data ChatFeature diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index e22610cfe5..4c6adae4e9 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -7,8 +7,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import Simplex.Chat.Options.DB (FromField (..), ToField (..)) +import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (blobFieldDecoder) import Simplex.Messaging.Util ((<$?>)) data GroupMemberRole diff --git a/src/Simplex/Chat/Types/UITheme.hs b/src/Simplex/Chat/Types/UITheme.hs index f2512a3a5a..d0f23a7307 100644 --- a/src/Simplex/Chat/Types/UITheme.hs +++ b/src/Simplex/Chat/Types/UITheme.hs @@ -15,8 +15,9 @@ import Data.Maybe (fromMaybe) import Data.Text (Text) import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types.Util +import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Util (decodeJSON, encodeJSON) data UITheme = UITheme diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 1c4f4258d8..4ba5acbb43 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} @@ -7,6 +8,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} @@ -37,7 +39,6 @@ import Data.Time.Format (defaultTimeLocale, formatTime) import qualified Data.Version as V import qualified Network.HTTP.Types as Q import Numeric (showFFloat) -import Simplex.Chat (defaultChatConfig) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Help @@ -86,29 +87,45 @@ data WCallCommand $(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "WCCall") ''WCallCommand) -serializeChatResponse :: (Maybe RemoteHostId, Maybe User) -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> String -serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig False ts tz remoteHost_ +serializeChatError :: Bool -> ChatConfig -> ChatError -> String +serializeChatError isCmd cfg = unlines . map unStyle . chatErrorToView isCmd cfg -responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] -responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case +serializeChatResponse :: ChatResponseEvent r => (Maybe RemoteHostId, Maybe User) -> ChatConfig -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> r -> String +serializeChatResponse hu cfg ts tz remoteHost_ = unlines . map unStyle . responseToView hu cfg False ts tz remoteHost_ + +class ChatResponseEvent r where + responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> r -> [StyledString] + isCommandResponse :: Bool + +instance ChatResponseEvent ChatResponse where + responseToView = chatResponseToView + isCommandResponse = True + +instance ChatResponseEvent ChatEvent where + responseToView = chatEventToView + isCommandResponse = False + +chatErrorToView :: Bool -> ChatConfig -> ChatError -> [StyledString] +chatErrorToView isCmd ChatConfig {logLevel, testView} = viewChatError isCmd logLevel testView + +chatResponseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] +chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveItems ts tz outputRH = \case CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes CRUsersList users -> viewUsersList users CRChatStarted -> ["chat started"] CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] - CRChatSuspended -> ["chat suspended"] CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats] CRChats chats -> viewChats ts tz chats CRApiChat u chat _ -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] CRChatTags u tags -> ttyUser u $ [viewJSON tags] - CRApiParsedMarkdown ft -> [viewJSON ft] CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure CRServerOperatorConditions (ServerOperatorConditions ops _ ca) -> viewServerOperators ops ca CRUserServers u uss -> ttyUser u $ concatMap viewUserServers uss <> (if testView then [] else serversUserHelp) CRUserServersValidation {} -> [] CRUsageConditions current _ accepted_ -> viewUsageConditions current accepted_ CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl - CRNetworkConfig cfg -> viewNetworkConfig cfg + CRNetworkConfig netCfg -> viewNetworkConfig netCfg CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats @@ -120,52 +137,23 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRGroupMemberSwitchStarted {} -> ["switch started"] CRContactSwitchAborted {} -> ["switch aborted"] CRGroupMemberSwitchAborted {} -> ["switch aborted"] - CRContactSwitch u ct progress -> ttyUser u $ viewContactSwitch ct progress - CRGroupMemberSwitch u g m progress -> ttyUser u $ viewGroupMemberSwitch g m progress CRContactRatchetSyncStarted {} -> ["connection synchronization started"] CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"] - CRContactRatchetSync u ct progress -> ttyUser u $ viewContactRatchetSync ct progress - CRGroupMemberRatchetSync u g m progress -> ttyUser u $ viewGroupMemberRatchetSync g m progress - CRContactVerificationReset u ct -> ttyUser u $ viewContactVerificationReset ct - CRGroupMemberVerificationReset u g m -> ttyUser u $ viewGroupMemberVerificationReset g m CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView - CRNewChatItems u chatItems - | length chatItems > 20 -> - if - | all (\aci -> aChatItemDir aci == MDRcv) chatItems -> ttyUser u [sShow (length chatItems) <> " new messages"] - | all (\aci -> aChatItemDir aci == MDSnd) chatItems -> ttyUser u [sShow (length chatItems) <> " messages sent"] - | otherwise -> ttyUser u [sShow (length chatItems) <> " new messages created"] - | otherwise -> - concatMap - (\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item) - chatItems + CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId] - CRChatItemsStatusesUpdated u chatItems - | length chatItems <= 20 -> - concatMap - (\ci -> ttyUser u $ viewChatItemStatusUpdated ci ts tz testView showReceipts) - chatItems - | testView && showReceipts -> - ttyUser u [sShow (length chatItems) <> " message statuses updated"] - | otherwise -> [] CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz CRChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci CRTagsUpdated u _ _ -> ttyUser u ["chat tags updated"] - CRChatItemsDeleted u deletions byUser timed -> case deletions of - [ChatItemDeletion (AChatItem _ _ chat deletedItem) toItem] -> - ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView - deletions' -> ttyUser u [sShow (length deletions') <> " messages deleted"] - CRGroupChatItemsDeleted u g ciIds byUser member_ -> ttyUser u [ttyGroup' g <> ": " <> sShow (length ciIds) <> " messages deleted by " <> if byUser then "user" else "member" <> maybe "" (\m -> " " <> ttyMember m) member_] + CRChatItemsDeleted u deletions byUser timed -> ttyUser u $ viewChatItemsDeleted (unmuted u) deletions byUser timed ts tz testView + CRGroupChatItemsDeleted u g ciIds byUser member_ -> ttyUser u $ viewGroupChatItemsDeleted g ciIds byUser member_ CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz CRReactionMembers u memberReactions -> ttyUser u $ viewReactionMembers memberReactions - CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] CRBroadcastSent u mc s f t -> ttyUser u $ viewSentBroadcast mc s f ts tz t - CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr - CRCmdAccepted _ -> [] CRCmdOk u_ -> ttyUser' u_ ["ok"] CRChatHelp section -> case section of HSMain -> chatHelpInfo @@ -181,56 +169,39 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe HSDatabase -> databaseHelpInfo CRWelcome user -> chatWelcome user CRContactsList u cs -> ttyUser u $ viewContactsList cs - CRUserContactLink u UserContactLink {connReqContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept + CRUserContactLink u UserContactLink {connLinkContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connLinkContact <> autoAcceptStatus_ autoAccept CRUserContactLinkUpdated u UserContactLink {autoAccept} -> ttyUser u $ autoAcceptStatus_ autoAccept CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"] CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRGroupsList u gs -> ttyUser u $ viewGroupsList gs - CRSentGroupInvitation u g c _ -> - ttyUser u $ - case contactConn c of - Just Connection {viaGroupLink} - | viaGroupLink -> [ttyContact' c <> " invited to group " <> ttyGroup' g <> " via your group link"] - | otherwise -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] - Nothing -> [] + CRSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus CRFileTransferStatusXFTP u ci -> ttyUser u $ viewFileTransferStatusXFTP ci CRUserProfile u p -> ttyUser u $ viewUserProfile p CRUserProfileNoChange u -> ttyUser u ["user profile did not change"] - CRUserPrivacy u u' -> ttyUserPrefix u $ viewUserPrivacy u u' + CRUserPrivacy u u' -> ttyUserPrefix hu outputRH u $ viewUserPrivacy u u' CRVersionInfo info _ _ -> viewVersionInfo logLevel info - CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq + CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation ccLink CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' - CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan + CRConnectionPlan u _ connectionPlan -> ttyUser u $ viewConnectionPlan cfg connectionPlan CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"] CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] - CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c - CRAcceptingBusinessRequest u g -> ttyUser u $ viewAcceptingBusinessRequest g CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] - CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] - CRBusinessRequestAlreadyAccepted u g -> ttyUser u [ttyFullGroup g <> ": sent you a duplicate connection request, but you are already connected, no action needed"] - CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq + CRUserContactLinkCreated u ccLink -> ttyUser u $ connReqContact_ "Your new chat address is created!" ccLink CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] - CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] - CRBusinessLinkConnecting u g _ _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] - CRUserDeletedMembers u g members -> case members of - [m] -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] - mems' -> ttyUser u [ttyGroup' g <> ": you removed " <> sShow (length mems') <> " members from the group"] + CRUserDeletedMembers u g members wm -> case members of + [m] -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group" <> withMessages wm] + mems' -> ttyUser u [ttyGroup' g <> ": you removed " <> sShow (length mems') <> " members from the group" <> withMessages wm] CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g - CRUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um] - CRUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um] - CRUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m] CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"] CRForwardPlan u count itemIds fc -> ttyUser u $ viewForwardPlan count itemIds fc - CRRcvFileDescrReady _ _ _ _ -> [] - CRRcvFileProgressXFTP {} -> [] CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts @@ -241,110 +212,27 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRContactAliasUpdated u c -> ttyUser u $ viewContactAliasUpdated c CRGroupAliasUpdated u g -> ttyUser u $ viewGroupAliasUpdated g CRConnectionAliasUpdated u c -> ttyUser u $ viewConnectionAliasUpdated c - CRContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c' - CRGroupMemberUpdated {} -> [] - CRContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct' - CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile CRRcvStandaloneFileCreated u ft -> ttyUser u $ receivingFileStandalone "started" ft - CRRcvFileStart u ci -> ttyUser u $ receivingFile_' hu testView "started" ci - CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' hu testView "completed" ci - CRRcvStandaloneFileComplete u _ ft -> ttyUser u $ receivingFileStandalone "completed" ft - CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft - CRRcvFileError u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "error" ci <> [sShow e] - CRRcvFileError u Nothing e ft -> ttyUser u $ receivingFileStandalone "error" ft <> [sShow e] - CRRcvFileWarning u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "warning: " ci <> [sShow e] - CRRcvFileWarning u Nothing e ft -> ttyUser u $ receivingFileStandalone "warning: " ft <> [sShow e] - CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft - CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft - CRSndFileStartXFTP {} -> [] - CRSndFileProgressXFTP {} -> [] - CRSndFileRedirectStartXFTP u ft ftRedirect -> ttyUser u $ standaloneUploadRedirect ft ftRedirect - CRSndStandaloneFileComplete u ft uris -> ttyUser u $ standaloneUploadComplete ft uris - CRSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadingFile "completed" ci - CRSndFileCancelledXFTP {} -> [] - CRSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [plain e] - CRSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e] - CRSndFileWarning u Nothing ft e -> ttyUser u $ uploadingFileStandalone "warning: " ft <> [plain e] - CRSndFileWarning u (Just ci) _ e -> ttyUser u $ uploadingFile "warning: " ci <> [plain e] - CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} -> - ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft] CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_ - CRContactConnecting u _ -> ttyUser u [] - CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView - CRContactSndReady u ct -> ttyUser u [ttyFullContact ct <> ": you can send messages to contact"] - CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"] - CRSubscriptionEnd u acEntity -> - let Connection {connId} = entityConnection acEntity - in ttyUser u [sShow connId <> ": END"] - CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] - CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] - CRContactSubError u c e -> ttyUser u [ttyContact' c <> ": contact error " <> sShow e] - CRContactSubSummary u summary -> - ttyUser u $ [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" - where - (errors, subscribed) = partition (isJust . contactError) summary - CRUserContactSubSummary u summary -> - ttyUser u $ - map addressSS addresses - <> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors") - where - (addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary - addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError - (groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks - CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else [] CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else [] - CRGroupInvitation u g -> ttyUser u [groupInvitationSub g] - CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r - CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m - CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] - CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] - CRJoinedGroupMemberConnecting u g host m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] - CRConnectedToGroupMember u g m _ -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] - CRMemberRole u g by m r r' -> ttyUser u $ viewMemberRoleChanged g by m r r' CRMembersRoleUser u g members r' -> ttyUser u $ viewMemberRoleUserChanged g members r' - CRMemberBlockedForAll u g by m blocked -> ttyUser u $ viewMemberBlockedForAll g by m blocked CRMembersBlockedForAllUser u g members blocked -> ttyUser u $ viewMembersBlockedForAllUser g members blocked - CRDeletedMemberUser u g by -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g - CRDeletedMember u g by m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group"] - CRLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"] - CRGroupEmpty u ShortGroupInfo {groupName = g} -> ttyUser u [ttyGroup g <> ": group is empty"] - CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the local copy of the group"] CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m CRGroupProfile u g -> ttyUser u $ viewGroupProfile g CRGroupDescription u g -> ttyUser u $ viewGroupDescription g - CRGroupLinkCreated u g cReq mRole -> ttyUser u $ groupLink_ "Group link is created!" g cReq mRole - CRGroupLink u g cReq mRole -> ttyUser u $ groupLink_ "Group link:" g cReq mRole + CRGroupLinkCreated u g ccLink mRole -> ttyUser u $ groupLink_ "Group link is created!" g ccLink mRole + CRGroupLink u g ccLink mRole -> ttyUser u $ groupLink_ "Group link:" g ccLink mRole CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g - CRAcceptingGroupJoinRequestMember _ g m -> [ttyFullMember m <> ": accepting request to join group " <> ttyGroup' g <> "..."] - CRNoMemberContactCreating u g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " does not have direct connection, creating"] CRNewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"] CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m] - CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] - CRContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct' - CRMemberSubError u ShortGroupInfo {groupName = g} ShortGroupMember {memberName = n} e -> ttyUser u [ttyGroup g <> " member " <> ttyContact n <> " error: " <> sShow e] - CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors" - CRGroupSubscribed u ShortGroupInfo {groupName = g} -> ttyUser u $ viewGroupSubscribed g - CRPendingSubSummary u _ -> ttyUser u [] - CRSndFileSubError u SndFileTransfer {fileId, fileName} e -> - ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] - CRRcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> - ttyUser u ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] - CRCallInvitation RcvCallInvitation {user, contact, callType, sharedKey} -> ttyUser user $ viewCallInvitation contact callType sharedKey - CRCallOffer {user = u, contact, callType, offer, sharedKey} -> ttyUser u $ viewCallOffer contact callType offer sharedKey - CRCallAnswer {user = u, contact, answer} -> ttyUser u $ viewCallAnswer contact answer - CRCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact] - CRCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"] CRCallInvitations _ -> [] - CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"] - CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"] CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"] CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)] CRNtfToken _ status mode srv -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode) <> ", server: " <> sShow srv] - CRNtfConns {} -> [] - CRConnNtfMessages {} -> [] - CRNtfMessage {} -> [] + CRNtfConns {ntfConns} -> map (\NtfConn {agentConnId, expectedMsg_} -> plain $ show agentConnId <> " " <> show expectedMsg_) ntfConns + CRConnNtfMessages ntfMsgs -> [sShow ntfMsgs] CRCurrentRemoteHost rhi_ -> [ maybe "Using local profile" @@ -359,40 +247,16 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ] where started = " started on " <> B.unpack (strEncode address) <> ":" <> ctrlPort - CRRemoteHostSessionCode {remoteHost_, sessionCode} -> - [ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_, - "Compare session code with host:", - plain sessionCode - ] - CRNewRemoteHost RemoteHostInfo {remoteHostId = rhId, hostDeviceName} -> ["new remote host " <> sShow rhId <> " added: " <> plain hostDeviceName] - CRRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} -> ["remote host " <> sShow rhId <> " connected"] - CRRemoteHostStopped {remoteHostId_} -> - [ maybe "new remote host" (mappend "remote host " . sShow) remoteHostId_ <> " stopped" - ] CRRemoteFileStored rhId (CryptoFile filePath cfArgs_) -> [plain $ "file " <> filePath <> " stored on remote host " <> show rhId] <> maybe [] ((: []) . cryptoFileArgsStr testView) cfArgs_ CRRemoteCtrlList cs -> viewRemoteCtrls cs - CRRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} -> - [ ("remote controller " <> sShow remoteCtrlId <> " found: ") - <> maybe (deviceName <> "not compatible") (\info -> viewRemoteCtrl info appVersion compatible) ctrlAppInfo_ - ] - <> ["use " <> highlight ("/confirm remote ctrl " <> show remoteCtrlId) <> " to connect" | isJust ctrlAppInfo_ && compatible] - where - deviceName = if T.null ctrlDeviceName then "" else plain ctrlDeviceName <> ", " CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion} -> [ (maybe "connecting new remote controller" (\RemoteCtrlInfo {remoteCtrlId} -> "connecting remote controller " <> sShow remoteCtrlId) remoteCtrl_ <> ": ") <> viewRemoteCtrl ctrlAppInfo appVersion True ] - CRRemoteCtrlSessionCode {remoteCtrl_, sessionCode} -> - [ maybe "new remote controller connected" (\RemoteCtrlInfo {remoteCtrlId} -> "remote controller " <> sShow remoteCtrlId <> " connected") remoteCtrl_, - "Compare session code with controller and use:", - "/verify remote ctrl " <> plain sessionCode -- TODO maybe pass rcId - ] CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} -> ["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName] - CRRemoteCtrlStopped {rcStopReason} -> viewRemoteCtrlStopped rcStopReason - CRContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"] CRSQLResult rows -> map plain rows #if !defined(dbPostgres) CRArchiveExported archiveErrs -> if null archiveErrs then ["ok"] else ["archive export errors: " <> plain (show archiveErrs)] @@ -433,41 +297,15 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe [ "agent queues info:", plain . LB.unpack $ J.encode agentQueuesInfo ] - CRContactDisabled u c -> ttyUser u ["[" <> ttyContact' c <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)] - CRConnectionDisabled entity -> viewConnectionEntityDisabled entity - CRConnectionInactive entity inactive -> viewConnectionEntityInactive entity inactive - CRAgentRcvQueuesDeleted delQs -> ["completed deleting rcv queues: " <> sShow (length delQs) | logLevel <= CLLInfo] - CRAgentConnsDeleted acIds -> ["completed deleting connections: " <> sShow (length acIds) | logLevel <= CLLInfo] - CRAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""] - CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning] - CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError True logLevel testView e - CRChatError u e -> ttyUser' u $ viewChatError False logLevel testView e - CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs CRAppSettings as -> ["app settings: " <> viewJSON as] - CRTimedAction _ _ -> [] CRCustomChatResponse u r -> ttyUser' u $ map plain $ T.lines r - CRTerminalEvent te -> case te of - TERejectingGroupJoinRequestMember _ g m reason -> [ttyFullMember m <> ": rejecting request to join group " <> ttyGroup' g <> ", reason: " <> sShow reason] - TEGroupLinkRejected u g reason -> ttyUser u [ttyGroup' g <> ": join rejected, reason: " <> sShow reason] where ttyUser :: User -> [StyledString] -> [StyledString] ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss - | (showNtfs && isNothing viewPwdHash) || activeUser = ttyUserPrefix user ss + | (showNtfs && isNothing viewPwdHash) || activeUser = ttyUserPrefix hu outputRH user ss | otherwise = [] - ttyUserPrefix :: User -> [StyledString] -> [StyledString] - ttyUserPrefix _ [] = [] - ttyUserPrefix User {userId, localDisplayName = u} ss - | null prefix = ss - | otherwise = prependFirst ("[" <> mconcat prefix <> "] ") ss - where - prefix = intersperse ", " $ remotePrefix <> userPrefix - remotePrefix = [maybe "local" (("remote: " <>) . highlight . show) outputRH | outputRH /= currentRH] - userPrefix = ["user: " <> highlight u | Just userId /= currentUserId] - currentUserId = (\User {userId = uId} -> uId) <$> user_ ttyUser' :: Maybe User -> [StyledString] -> [StyledString] ttyUser' = maybe id ttyUser - ttyUserPrefix' :: Maybe User -> [StyledString] -> [StyledString] - ttyUserPrefix' = maybe id ttyUserPrefix testViewChats :: [AChat] -> [StyledString] testViewChats chats = [sShow $ map toChatView chats] where @@ -498,10 +336,203 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ = let deleted_ = maybe "" (\t -> " [" <> t <> "]") (chatItemDeletedText ci membership_) in itemText <> deleted_ - viewErrorsSummary :: [a] -> StyledString -> [StyledString] - viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)] - contactList :: [ContactRef] -> String - contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs + unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString] + unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isUserMention ci + unmutedReaction :: User -> ChatInfo c -> CIReaction c d -> [StyledString] -> [StyledString] + unmutedReaction u chat CIReaction {chatDir} = unmuted' u chat chatDir False + unmuted' :: User -> ChatInfo c -> CIDirection c d -> Bool -> [StyledString] -> [StyledString] + unmuted' u chat chatDir mention s + | chatDirNtf u chat chatDir mention = s + | testView = map (<> " ") s + | otherwise = [] + withMessages wm = if wm then " with all messages" else "" + +ttyUserPrefix :: (Maybe RemoteHostId, Maybe User) -> Maybe RemoteHostId -> User -> [StyledString] -> [StyledString] +ttyUserPrefix _ _ _ [] = [] +ttyUserPrefix (currentRH, user_) outputRH User {userId, localDisplayName = u} ss + | null prefix = ss + | otherwise = prependFirst ("[" <> mconcat prefix <> "] ") ss + where + prefix = intersperse ", " $ remotePrefix <> userPrefix + remotePrefix = [maybe "local" (("remote: " <>) . highlight . show) outputRH | outputRH /= currentRH] + userPrefix = ["user: " <> highlight u | Just userId /= currentUserId] + currentUserId = (\User {userId = uId} -> uId) <$> user_ + +viewErrorsSummary :: [a] -> StyledString -> [StyledString] +viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)] + +contactList :: [ContactRef] -> String +contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs + +chatEventToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatEvent -> [StyledString] +chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case + CEvtChatSuspended -> ["chat suspended"] + CEvtContactSwitch u ct progress -> ttyUser u $ viewContactSwitch ct progress + CEvtGroupMemberSwitch u g m progress -> ttyUser u $ viewGroupMemberSwitch g m progress + CEvtContactRatchetSync u ct progress -> ttyUser u $ viewContactRatchetSync ct progress + CEvtGroupMemberRatchetSync u g m progress -> ttyUser u $ viewGroupMemberRatchetSync g m progress + CEvtNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz + CEvtChatItemsStatusesUpdated u chatItems + | length chatItems <= 20 -> + concatMap + (\ci -> ttyUser u $ viewChatItemStatusUpdated ci ts tz testView showReceipts) + chatItems + | testView && showReceipts -> + ttyUser u [sShow (length chatItems) <> " message statuses updated"] + | otherwise -> [] + CEvtChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz + CEvtChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci + CEvtChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz + CEvtChatItemsDeleted u deletions byUser timed -> ttyUser u $ viewChatItemsDeleted (unmuted u) deletions byUser timed ts tz testView + CEvtGroupChatItemsDeleted u g ciIds byUser member_ -> ttyUser u $ viewGroupChatItemsDeleted g ciIds byUser member_ + CEvtChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] + CEvtUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] + CEvtSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c + CEvtContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] + CEvtAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c + CEvtAcceptingBusinessRequest u g -> ttyUser u $ viewAcceptingBusinessRequest g + CEvtContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] + CEvtBusinessRequestAlreadyAccepted u g -> ttyUser u [ttyFullGroup g <> ": sent you a duplicate connection request, but you are already connected, no action needed"] + CEvtGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] + CEvtBusinessLinkConnecting u g _ _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] + CEvtUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um] + CEvtUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um] + CEvtUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m] + CEvtRcvFileDescrReady _ _ _ _ -> [] + CEvtRcvFileAccepted u ci -> ttyUser u $ savingFile' ci + CEvtRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft + CEvtRcvFileProgressXFTP {} -> [] + CEvtContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c' + CEvtGroupMemberUpdated {} -> [] + CEvtContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct' + CEvtReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile + CEvtRcvFileStart u ci -> ttyUser u $ receivingFile_' hu testView "started" ci + CEvtRcvFileComplete u ci -> ttyUser u $ receivingFile_' hu testView "completed" ci + CEvtRcvStandaloneFileComplete u _ ft -> ttyUser u $ receivingFileStandalone "completed" ft + CEvtRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft + CEvtRcvFileError u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "error" ci <> [sShow e] + CEvtRcvFileError u Nothing e ft -> ttyUser u $ receivingFileStandalone "error" ft <> [sShow e] + CEvtRcvFileWarning u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "warning: " ci <> [sShow e] + CEvtRcvFileWarning u Nothing e ft -> ttyUser u $ receivingFileStandalone "warning: " ft <> [sShow e] + CEvtSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft + CEvtSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft + CEvtSndFileStartXFTP {} -> [] + CEvtSndFileProgressXFTP {} -> [] + CEvtSndFileRedirectStartXFTP u ft ftRedirect -> ttyUser u $ standaloneUploadRedirect ft ftRedirect + CEvtSndStandaloneFileComplete u ft uris -> ttyUser u $ standaloneUploadComplete ft uris + CEvtSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadingFile "completed" ci + CEvtSndFileCancelledXFTP {} -> [] + CEvtSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [plain e] + CEvtSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e] + CEvtSndFileWarning u Nothing ft e -> ttyUser u $ uploadingFileStandalone "warning: " ft <> [plain e] + CEvtSndFileWarning u (Just ci) _ e -> ttyUser u $ uploadingFile "warning: " ci <> [plain e] + CEvtSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} -> + ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft] + CEvtContactConnecting u _ -> ttyUser u [] + CEvtContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView + CEvtContactSndReady u ct -> ttyUser u [ttyFullContact ct <> ": you can send messages to contact"] + CEvtContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"] + CEvtSubscriptionEnd u acEntity -> + let Connection {connId} = entityConnection acEntity + in ttyUser u [sShow connId <> ": END"] + CEvtContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] + CEvtContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] + CEvtContactSubError u c e -> ttyUser u [ttyContact' c <> ": contact error " <> sShow e] + CEvtContactSubSummary u summary -> + ttyUser u $ [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" + where + (errors, subscribed) = partition (isJust . contactError) summary + CEvtUserContactSubSummary u summary -> + ttyUser u $ + map addressSS addresses + <> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors") + where + (addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary + addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError + (groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks + CEvtNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else [] + CEvtNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else [] + CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r + CEvtUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g + CEvtJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m + CEvtHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] + CEvtHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] + CEvtJoinedGroupMemberConnecting u g host m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] + CEvtConnectedToGroupMember u g m _ -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] + CEvtMemberRole u g by m r r' -> ttyUser u $ viewMemberRoleChanged g by m r r' + CEvtMemberBlockedForAll u g by m blocked -> ttyUser u $ viewMemberBlockedForAll g by m blocked + CEvtDeletedMemberUser u g by wm -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group" <> withMessages wm] <> groupPreserved g + CEvtDeletedMember u g by m wm -> ttyUser u [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group" <> withMessages wm] + CEvtLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"] + CEvtGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the local copy of the group"] + CEvtGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m + CEvtAcceptingGroupJoinRequestMember _ g m -> [ttyFullMember m <> ": accepting request to join group " <> ttyGroup' g <> "..."] + CEvtNoMemberContactCreating u g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " does not have direct connection, creating"] + CEvtNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] + CEvtContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct' + CEvtCallInvitation RcvCallInvitation {user, contact, callType, sharedKey} -> ttyUser user $ viewCallInvitation contact callType sharedKey + CEvtCallOffer {user = u, contact, callType, offer, sharedKey} -> ttyUser u $ viewCallOffer contact callType offer sharedKey + CEvtCallAnswer {user = u, contact, answer} -> ttyUser u $ viewCallAnswer contact answer + CEvtCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact] + CEvtCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"] + CEvtNtfMessage {} -> [] + CEvtRemoteHostSessionCode {remoteHost_, sessionCode} -> + [ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_, + "Compare session code with host:", + plain sessionCode + ] + CEvtNewRemoteHost RemoteHostInfo {remoteHostId = rhId, hostDeviceName} -> ["new remote host " <> sShow rhId <> " added: " <> plain hostDeviceName] + CEvtRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} -> ["remote host " <> sShow rhId <> " connected"] + CEvtRemoteHostStopped {remoteHostId_} -> + [ maybe "new remote host" (mappend "remote host " . sShow) remoteHostId_ <> " stopped" + ] + CEvtRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} -> + [ ("remote controller " <> sShow remoteCtrlId <> " found: ") + <> maybe (deviceName <> "not compatible") (\info -> viewRemoteCtrl info appVersion compatible) ctrlAppInfo_ + ] + <> ["use " <> highlight ("/confirm remote ctrl " <> show remoteCtrlId) <> " to connect" | isJust ctrlAppInfo_ && compatible] + where + deviceName = if T.null ctrlDeviceName then "" else plain ctrlDeviceName <> ", " + CEvtRemoteCtrlSessionCode {remoteCtrl_, sessionCode} -> + [ maybe "new remote controller connected" (\RemoteCtrlInfo {remoteCtrlId} -> "remote controller " <> sShow remoteCtrlId <> " connected") remoteCtrl_, + "Compare session code with controller and use:", + "/verify remote ctrl " <> plain sessionCode -- TODO maybe pass rcId + ] + CEvtRemoteCtrlStopped {rcStopReason} -> viewRemoteCtrlStopped rcStopReason + CEvtContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"] + CEvtContactDisabled u c -> ttyUser u ["[" <> ttyContact' c <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)] + CEvtConnectionDisabled entity -> viewConnectionEntityDisabled entity + CEvtConnectionInactive entity inactive -> viewConnectionEntityInactive entity inactive + CEvtAgentRcvQueuesDeleted delQs -> ["completed deleting rcv queues: " <> sShow (length delQs) | logLevel <= CLLInfo] + CEvtAgentConnsDeleted acIds -> ["completed deleting connections: " <> sShow (length acIds) | logLevel <= CLLInfo] + CEvtAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""] + CEvtMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning] + CEvtChatErrors errs -> concatMap (viewChatError False logLevel testView) errs + CEvtTimedAction _ _ -> [] + CEvtTerminalEvent te -> case te of + TERejectingGroupJoinRequestMember _ g m reason -> [ttyFullMember m <> ": rejecting request to join group " <> ttyGroup' g <> ", reason: " <> sShow reason] + TEGroupLinkRejected u g reason -> ttyUser u [ttyGroup' g <> ": join rejected, reason: " <> sShow reason] + TENewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"] + TEContactVerificationReset u ct -> ttyUser u $ viewContactVerificationReset ct + TEGroupMemberVerificationReset u g m -> ttyUser u $ viewGroupMemberVerificationReset g m + TEGroupSubscribed u ShortGroupInfo {groupName = g} -> ttyUser u $ viewGroupSubscribed g + TEGroupInvitation u g -> ttyUser u [groupInvitationSub g] + TEGroupEmpty u ShortGroupInfo {groupName = g} -> ttyUser u [ttyGroup g <> ": group is empty"] + TEMemberSubError u ShortGroupInfo {groupName = g} ShortGroupMember {memberName = n} e -> ttyUser u [ttyGroup g <> " member " <> ttyContact n <> " error: " <> sShow e] + TEMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors" + TEPendingSubSummary u _ -> ttyUser u [] + TESndFileSubError u SndFileTransfer {fileId, fileName} e -> + ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] + TERcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> + ttyUser u ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] + where + ttyUser :: User -> [StyledString] -> [StyledString] + ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss + | (showNtfs && isNothing viewPwdHash) || activeUser = ttyUserPrefix hu outputRH user ss + | otherwise = [] + ttyUser' :: Maybe User -> [StyledString] -> [StyledString] + ttyUser' = maybe id ttyUser + withMessages wm = if wm then " with all messages" else "" unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString] unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isUserMention ci unmutedReaction :: User -> ChatInfo c -> CIReaction c d -> [StyledString] -> [StyledString] @@ -591,6 +622,23 @@ viewChats ts tz = concatMap chatPreview . reverse GroupChat g -> [" " <> ttyToGroup g] _ -> [] +viewChatItems :: + (User -> [StyledString] -> [StyledString]) -> + (forall c d. User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]) -> + User -> + [AChatItem] -> + UTCTime -> + TimeZone -> + [StyledString] +viewChatItems ttyUser unmuted u chatItems ts tz + | length chatItems <= 20 = + concatMap + (\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item) + chatItems + | all (\aci -> aChatItemDir aci == MDRcv) chatItems = ttyUser u [sShow (length chatItems) <> " new messages"] + | all (\aci -> aChatItemDir aci == MDSnd) chatItems = ttyUser u [sShow (length chatItems) <> " messages sent"] + | otherwise = ttyUser u [sShow (length chatItems) <> " new messages created"] + viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString] viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwardedByMember, userMention}, content, quotedItem, file} doShow ts tz = withGroupMsgForwarded . withItemDeleted <$> viewCI @@ -724,14 +772,10 @@ localTs tz ts = do viewChatItemStatusUpdated :: AChatItem -> CurrentTime -> TimeZone -> Bool -> Bool -> [StyledString] viewChatItemStatusUpdated (AChatItem _ _ chat item@ChatItem {meta = CIMeta {itemStatus}}) ts tz testView showReceipts = case itemStatus of - CISSndRcvd rcptStatus SSPPartial -> - if testView && showReceipts - then prependFirst (viewDeliveryReceiptPartial rcptStatus <> " ") $ viewChatItem chat item False ts tz - else [] - CISSndRcvd rcptStatus SSPComplete -> - if testView && showReceipts - then prependFirst (viewDeliveryReceipt rcptStatus <> " ") $ viewChatItem chat item False ts tz - else [] + CISSndRcvd rcptStatus SSPPartial | testView && showReceipts -> + prependFirst (viewDeliveryReceiptPartial rcptStatus <> " ") $ viewChatItem chat item False ts tz + CISSndRcvd rcptStatus SSPComplete | testView && showReceipts -> + prependFirst (viewDeliveryReceipt rcptStatus <> " ") $ viewChatItem chat item False ts tz _ -> [] viewDeliveryReceiptPartial :: MsgReceiptStatus -> StyledString @@ -795,6 +839,23 @@ viewItemNotChanged (AChatItem _ msgDir _ _) = case msgDir of SMDSnd -> ["message didn't change"] SMDRcv -> [] +viewChatItemsDeleted :: + (forall c d. ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]) -> + [ChatItemDeletion] -> + Bool -> + Bool -> + UTCTime -> + TimeZone -> + Bool -> + [StyledString] +viewChatItemsDeleted unmuted deletions byUser timed ts tz testView = case deletions of + [ChatItemDeletion (AChatItem _ _ chat deletedItem) toItem] -> + unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView + deletions' -> [sShow (length deletions') <> " messages deleted"] + +viewGroupChatItemsDeleted :: GroupInfo -> [ChatItemId] -> Bool -> Maybe GroupMember -> [StyledString] +viewGroupChatItemsDeleted g ciIds byUser member_ = [ttyGroup' g <> ": " <> sShow (length ciIds) <> " messages deleted by " <> if byUser then "user" else "member" <> maybe "" (\m -> " " <> ttyMember m) member_] + viewItemDelete :: ChatInfo c -> ChatItem c d -> Maybe AChatItem -> Bool -> Bool -> CurrentTime -> TimeZone -> Bool -> [StyledString] viewItemDelete chat ci@ChatItem {chatDir, meta, content = deletedContent} toItem byUser timed ts tz testView | timed = [plain ("timed message deleted: " <> T.unpack (ciContentToText deletedContent)) | testView] @@ -910,14 +971,17 @@ viewInvalidConnReq = plain updateStr ] -viewConnReqInvitation :: ConnReqInvitation -> [StyledString] -viewConnReqInvitation cReq = +viewConnReqInvitation :: CreatedLinkInvitation -> [StyledString] +viewConnReqInvitation (CCLink cReq shortLink) = [ "pass this invitation link to your contact (via another channel): ", "", - (plain . strEncode) (simplexChatInvitation cReq), + plain $ maybe cReqStr strEncode shortLink, "", "and ask them to connect: " <> highlight' "/c " ] + <> ["The invitation link for old clients: " <> plain cReqStr | isJust shortLink] + where + cReqStr = strEncode $ simplexChatInvitation cReq simplexChatInvitation :: ConnReqInvitation -> ConnReqInvitation simplexChatInvitation (CRInvitationUri crData e2e) = CRInvitationUri crData {crScheme = simplexChat} e2e @@ -972,21 +1036,29 @@ viewForwardPlan count itemIds = maybe [forwardCount] $ \fc -> [confirmation fc, | otherwise = plain $ show len <> " message(s) out of " <> show count <> " can be forwarded" len = length itemIds -connReqContact_ :: StyledString -> ConnReqContact -> [StyledString] -connReqContact_ intro cReq = +connReqContact_ :: StyledString -> CreatedLinkContact -> [StyledString] +connReqContact_ intro (CCLink cReq shortLink) = [ intro, "", - (plain . strEncode) (simplexChatContact cReq), + plain $ maybe cReqStr strEncode shortLink, "", "Anybody can send you contact requests with: " <> highlight' "/c ", "to show it again: " <> highlight' "/sa", "to share with your contacts: " <> highlight' "/profile_address on", "to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)" ] + <> ["The contact link for old clients: " <> plain cReqStr | isJust shortLink] + where + cReqStr = strEncode $ simplexChatContact cReq simplexChatContact :: ConnReqContact -> ConnReqContact simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simplexChat} +simplexChatContact' :: ConnLinkContact -> ConnLinkContact +simplexChatContact' = \case + CLFull (CRContactUri crData) -> CLFull $ CRContactUri crData {crScheme = simplexChat} + l@(CLShort _) -> l + autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString] autoAcceptStatus_ = \case Just AutoAccept {businessAddress, acceptIncognito, autoReply} -> @@ -999,16 +1071,19 @@ autoAcceptStatus_ = \case | otherwise = "" _ -> ["auto_accept off"] -groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole -> [StyledString] -groupLink_ intro g cReq mRole = +groupLink_ :: StyledString -> GroupInfo -> CreatedLinkContact -> GroupMemberRole -> [StyledString] +groupLink_ intro g (CCLink cReq shortLink) mRole = [ intro, "", - (plain . strEncode) (simplexChatContact cReq), + plain $ maybe cReqStr strEncode shortLink, "", "Anybody can connect to you and join group as " <> showRole mRole <> " with: " <> highlight' "/c ", "to show it again: " <> highlight ("/show link #" <> viewGroupName g), "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" ] + <> ["The group link for old clients: " <> plain cReqStr | isJust shortLink] + where + cReqStr = strEncode $ simplexChatContact cReq viewGroupLinkDeleted :: GroupInfo -> [StyledString] viewGroupLinkDeleted g = @@ -1207,6 +1282,13 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs | localAlias == "" = "" | otherwise = " (alias: " <> plain localAlias <> ")" +viewSentGroupInvitation :: GroupInfo -> Contact -> [StyledString] +viewSentGroupInvitation g c = case contactConn c of + Just Connection {viaGroupLink} + | viaGroupLink -> [ttyContact' c <> " invited to group " <> ttyGroup' g <> " via your group link"] + | otherwise -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] + Nothing -> [] + groupInvitation' :: GroupInfo -> StyledString groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} = highlight ("#" <> viewName ldn) @@ -1404,7 +1486,7 @@ viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledS viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> maybe [] viewConnectionStats stats - <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink + <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink <> maybe ["you've shared main profile with this contact"] (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) @@ -1436,7 +1518,7 @@ viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProf "member ID: " <> sShow groupMemberId ] <> maybe ["member not connected"] viewConnectionStats stats - <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink + <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink <> ["alias: " <> plain localAlias | localAlias /= ""] <> [viewConnectionVerified (memberSecurityCode m) | isJust stats] <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn @@ -1598,10 +1680,10 @@ countactUserPrefText cup = case cup of viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> [StyledString] viewGroupUpdated - GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, description, image, groupPreferences = gps}} - g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', description = description', image = image', groupPreferences = gps'}} + GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, description, image, groupPreferences = gps, memberAdmission = ma}} + g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', description = description', image = image', groupPreferences = gps', memberAdmission = ma'}} m = do - let update = groupProfileUpdated <> groupPrefsUpdated + let update = groupProfileUpdated <> groupPrefsUpdated <> memberAdmissionUpdated if null update then [] else memberUpdated <> update @@ -1622,6 +1704,9 @@ viewGroupUpdated | otherwise = Just . plain $ groupPreferenceText (pref gps') where pref = getGroupPreference f . mergeGroupPreferences + memberAdmissionUpdated + | ma == ma' = [] + | otherwise = ["changed member admission rules"] viewGroupProfile :: GroupInfo -> [StyledString] viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {description, image, groupPreferences = gps}} = @@ -1662,21 +1747,23 @@ viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserPr | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] viewConnectionUserChanged :: User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString] -viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId, connReqInv} User {localDisplayName = n'} PendingContactConnection {connReqInv = connReqInv'} = - case (connReqInv, connReqInv') of - (Just cReqInv, Just cReqInv') - | cReqInv /= cReqInv' -> [userChangedStr <> ", new link:"] <> newLink cReqInv' +viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId} User {localDisplayName = n'} PendingContactConnection {connLinkInv = connLinkInv'} = + case connLinkInv' of + Just ccLink' -> [userChangedStr <> ", new link:"] <> newLink ccLink' _ -> [userChangedStr] where userChangedStr = "connection " <> sShow pccConnId <> " changed from user " <> plain n <> " to user " <> plain n' - newLink cReqInv = + newLink (CCLink cReq shortLink) = [ "", - (plain . strEncode) (simplexChatInvitation cReqInv), + plain $ maybe cReqStr strEncode shortLink, "" ] + <> ["The invitation link for old clients: " <> plain cReqStr | isJust shortLink] + where + cReqStr = strEncode $ simplexChatInvitation cReq -viewConnectionPlan :: ConnectionPlan -> [StyledString] -viewConnectionPlan = \case +viewConnectionPlan :: ChatConfig -> ConnectionPlan -> [StyledString] +viewConnectionPlan ChatConfig {logLevel, testView} = \case CPInvitationLink ilp -> case ilp of ILPOk -> [invLink "ok to connect"] ILPOwnLink -> [invLink "own link"] @@ -1715,6 +1802,7 @@ viewConnectionPlan = \case grpOrBiz GroupInfo {businessChat} = case businessChat of Just _ -> "business" Nothing -> "group" + CPError e -> viewChatError False logLevel testView e viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated @@ -2137,8 +2225,8 @@ viewChatError isCmd logLevel testView = \case CEChatNotStarted -> ["error: chat not started"] CEChatNotStopped -> ["error: chat not stopped"] CEChatStoreChanged -> ["error: chat store changed, please restart chat"] - CEConnectionPlan connectionPlan -> viewConnectionPlan connectionPlan CEInvalidConnReq -> viewInvalidConnReq + CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr] CEInvalidChatMessage Connection {connId} msgMeta_ msg e -> [ plain $ ("chat message error: " <> e <> " (" <> T.unpack (T.take 120 msg) <> ")") diff --git a/tests/Bots/BroadcastTests.hs b/tests/Bots/BroadcastTests.hs index 751a7d1e05..71142c8b60 100644 --- a/tests/Bots/BroadcastTests.hs +++ b/tests/Bots/BroadcastTests.hs @@ -78,10 +78,11 @@ testBroadcastMessages ps = do bob <## "I broadcast messages to all connected users from @alice." cath `connectVia` botLink alice #> "@broadcast_bot hello all!" + alice <# "broadcast_bot> hello all!" -- we broadcast to the sender too, /feed is used by bot bob <# "broadcast_bot> hello all!" cath <# "broadcast_bot> hello all!" alice <# "broadcast_bot> > hello all!" - alice <## " Forwarded to 2 contact(s)" + alice <## " Forwarded to 3 contact(s), 0 errors" where cc `connectVia` botLink = do cc ##> ("/c " <> botLink) diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 31b36159fd..f8a5aa8b80 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -13,14 +13,15 @@ import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Exception (finally) import Control.Monad (forM_, when) import qualified Data.Text as T -import qualified Directory.Events as DE +import Directory.Captcha import Directory.Options import Directory.Service import Directory.Store import GHC.IO.Handle (hClose) import Simplex.Chat.Bot.KnownContacts -import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) import Simplex.Chat.Core +import qualified Simplex.Chat.Markdown as MD import Simplex.Chat.Options (CoreChatOpts (..)) import Simplex.Chat.Options.DB import Simplex.Chat.Types (Profile (..)) @@ -33,6 +34,7 @@ directoryServiceTests = do it "should register group" testDirectoryService it "should suspend and resume group, send message to owner" testSuspendResume it "should delete group registration" testDeleteGroup + it "admin should delete group registration" testDeleteGroupAdmin it "should change initial member role" testSetRole it "should join found group via link" testJoinGroup it "should support group names with spaces" testGroupNameWithSpaces @@ -51,10 +53,12 @@ directoryServiceTests = do it "should NOT allow approving if roles are incorrect" testNotApprovedBadRoles describe "should require re-approval if profile is changed by" $ do it "the registration owner" testRegOwnerChangedProfile - it "another owner" testAnotherOwnerChangedProfile -- TODO fix - doesn't work if another owner is not connected as contact + it "another owner" testAnotherOwnerChangedProfile + it "another owner not connected to directory" testNotConnectedOwnerChangedProfile describe "should require profile update if group link is removed by " $ do it "the registration owner" testRegOwnerRemovedLink - it "another owner" testAnotherOwnerRemovedLink -- TODO fix - doesn't work if another owner is not connected as contact + it "another owner" testAnotherOwnerRemovedLink + it "another owner not connected to directory" testNotConnectedOwnerRemovedLink describe "duplicate groups (same display name and full name)" $ do it "should ask for confirmation if a duplicate group is submitted" testDuplicateAskConfirmation it "should prohibit registration if a duplicate group is listed" testDuplicateProhibitRegistration @@ -63,8 +67,12 @@ directoryServiceTests = do it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval describe "list groups" $ do it "should list user's groups" testListUserGroups + describe "member admission" $ do + it "should ask member to pass captcha screen" testCapthaScreening describe "store log" $ do it "should restore directory service state" testRestoreDirectory + describe "captcha" $ do + it "should accept some incorrect spellings" testCaptcha directoryProfile :: Profile directoryProfile = Profile {displayName = "SimpleX-Directory", fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} @@ -103,7 +111,7 @@ serviceDbPrefix :: FilePath serviceDbPrefix = "directory_service" viewName :: String -> String -viewName = T.unpack . DE.viewName . T.pack +viewName = T.unpack . MD.viewName . T.pack testDirectoryService :: HasCallStack => TestParams -> IO () testDirectoryService ps = @@ -181,10 +189,13 @@ testDirectoryService ps = superUser #> "@SimpleX-Directory /approve 1:PSA 1" superUser <# "SimpleX-Directory> > /approve 1:PSA 1" superUser <## " Group approved!" - bob <# "SimpleX-Directory> The group ID 1 (PSA) is approved and listed in directory!" + bob <# "SimpleX-Directory> The group ID 1 (PSA) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." bob <## "" - bob <## "Use /filter 1 to configure anti-spam filter and /role 1 to set default member role." + bob <## "Supported commands:" + bob <## "- /filter 1 - to configure anti-spam filter." + bob <## "- /role 1 - to set default member role." + bob <## "- /help commands - other commands." search bob "privacy" welcomeWithLink' search bob "security" welcomeWithLink' cath `connectVia` dsLink @@ -261,6 +272,40 @@ testDeleteGroup ps = bob <## " Your group privacy is deleted from the directory" groupNotFound bob "privacy" +testDeleteGroupAdmin :: HasCallStack => TestParams -> IO () +testDeleteGroupAdmin ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + cath `connectVia` dsLink + registerGroupId superUser cath "security" "Security" 2 1 + groupFound bob "privacy" + groupFound bob "security" + listUserGroup bob "privacy" "Privacy" + listUserGroup cath "security" "Security" + superUser #> "@SimpleX-Directory /last" + superUser <# "SimpleX-Directory> > /last" + superUser <## " 2 registered group(s)" + memberGroupListing superUser bob 1 "privacy" "Privacy" 2 "active" + memberGroupListing superUser cath 2 "security" "Security" 2 "active" + -- trying to register group with the same name + submitGroup bob "security" "Security" + bob <# "SimpleX-Directory> The group security (Security) is already listed in the directory, please choose another name." + bob ##> "/d #security" + bob <## "#security: you deleted the group" + -- admin can delete the group + superUser #> "@SimpleX-Directory /delete 2:security" + superUser <# "SimpleX-Directory> > /delete 2:security" + superUser <## " The group security is deleted from the directory" + groupFound cath "privacy" + listUserGroup bob "privacy" "Privacy" + groupNotFound bob "security" + sendListCommand cath 0 + -- another user can register the group with the same name + registerGroupId superUser bob "security" "Security" 4 2 + testSetRole :: HasCallStack => TestParams -> IO () testSetRole ps = withDirectoryService ps $ \superUser dsLink -> @@ -721,13 +766,34 @@ testAnotherOwnerChangedProfile ps = cath <## "full name changed to: Privacy and Security" bob <## "cath updated group #privacy:" bob <## "full name changed to: Privacy and Security" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath!" bob <## "It is hidden from the directory until approved." groupNotFound cath "privacy" - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" +testNotConnectedOwnerChangedProfile :: HasCallStack => TestParams -> IO () +testNotConnectedOwnerChangedProfile ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + bob `connectVia` dsLink + dan `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + cath ##> "/gp privacy privacy Privacy and Security" + cath <## "full name changed to: Privacy and Security" + bob <## "cath updated group #privacy:" + bob <## "full name changed to: Privacy and Security" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath!" + bob <## "It is hidden from the directory until approved." + groupNotFound dan "privacy" + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath." + reapproveGroup 3 superUser bob + groupFoundN 3 dan "privacy" + testRegOwnerRemovedLink :: HasCallStack => TestParams -> IO () testRegOwnerRemovedLink ps = withDirectoryService ps $ \superUser dsLink -> @@ -753,14 +819,15 @@ testRegOwnerRemovedLink ps = cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" cath <## "use @SimpleX-Directory to send messages" groupNotFound cath "privacy" - bob ##> ("/set welcome #privacy " <> welcomeWithLink) + let withChangedLink = T.unpack $ T.replace "contact#/?v=2-7&" "contact#/?v=3-7&" $ T.pack welcomeWithLink + bob ##> ("/set welcome #privacy " <> withChangedLink) bob <## "description changed to:" - bob <## welcomeWithLink + bob <## withChangedLink bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." cath <## "bob updated group #privacy:" cath <## "description changed to:" - cath <## welcomeWithLink + cath <## withChangedLink reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" @@ -784,7 +851,7 @@ testAnotherOwnerRemovedLink ps = bob <## "cath updated group #privacy:" bob <## "description changed to:" bob <## "Welcome!" - bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message." + bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message by cath." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." @@ -795,20 +862,55 @@ testAnotherOwnerRemovedLink ps = bob <## "cath updated group #privacy:" bob <## "description changed to:" bob <## welcomeWithLink - bob <# "SimpleX-Directory> The group link is added by another group member, your registration will not be processed." - bob <## "" - bob <## "Please update the group profile yourself." - bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!") - bob <## "description changed to:" - bob <## (welcomeWithLink <> " - welcome!") - bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." + bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." - cath <## "bob updated group #privacy:" - cath <## "description changed to:" - cath <## (welcomeWithLink <> " - welcome!") reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" +testNotConnectedOwnerRemovedLink :: HasCallStack => TestParams -> IO () +testNotConnectedOwnerRemovedLink ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + bob `connectVia` dsLink + dan `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + bob ##> "/show welcome #privacy" + bob <## "Welcome message:" + welcomeWithLink <- getTermLine bob + cath ##> "/set welcome #privacy Welcome!" + cath <## "description changed to:" + cath <## "Welcome!" + bob <## "cath updated group #privacy:" + bob <## "description changed to:" + bob <## "Welcome!" + bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message by cath." + bob <## "" + bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." + superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." + groupNotFound dan "privacy" + cath ##> ("/set welcome #privacy " <> welcomeWithLink) + cath <## "description changed to:" + cath <## welcomeWithLink + bob <## "cath updated group #privacy:" + bob <## "description changed to:" + bob <## welcomeWithLink + -- bob <# "SimpleX-Directory> The group link is added by another group member, your registration will not be processed." + -- bob <## "" + -- bob <## "Please update the group profile yourself." + -- bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!") + -- bob <## "description changed to:" + -- bob <## (welcomeWithLink <> " - welcome!") + bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." + -- cath <## "bob updated group #privacy:" + -- cath <## "description changed to:" + -- cath <## (welcomeWithLink <> " - welcome!") + reapproveGroup 3 superUser bob + groupFoundN 3 dan "privacy" + testDuplicateAskConfirmation :: HasCallStack => TestParams -> IO () testDuplicateAskConfirmation ps = withDirectoryService ps $ \superUser dsLink -> @@ -932,14 +1034,7 @@ testListUserGroups ps = cath <## "use @SimpleX-Directory to send messages" registerGroupId superUser bob "security" "Security" 2 2 registerGroupId superUser cath "anonymity" "Anonymity" 3 1 - cath #> "@SimpleX-Directory /list" - cath <# "SimpleX-Directory> > /list" - cath <## " 1 registered group(s)" - cath <# "SimpleX-Directory> 1. anonymity (Anonymity)" - cath <## "Welcome message:" - cath <##. "Link to join the group anonymity: " - cath <## "2 members" - cath <## "Status: active" + listUserGroup cath "anonymity" "Anonymity" -- with de-listed group groupFound cath "anonymity" cath ##> "/mr anonymity SimpleX-Directory member" @@ -951,6 +1046,88 @@ testListUserGroups ps = groupNotFound cath "anonymity" listGroups superUser bob cath +testCapthaScreening :: HasCallStack => TestParams -> IO () +testCapthaScreening ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + -- check default role + bob #> "@SimpleX-Directory /role 1" + bob <# "SimpleX-Directory> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /role 1 observer to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + -- enable captcha + bob #> "@SimpleX-Directory /filter 1 captcha" + bob <# "SimpleX-Directory> > /filter 1 captcha" + bob <## " Spam filter settings for group privacy set to:" + bob <## "- reject long/inappropriate names: disabled" + bob <## "- pass captcha to join: enabled" + bob <## "" + bob <## "Use /filter 1 [name] [captcha] to enable and /filter 1 off to disable filter." + -- connect with captcha screen + _ <- join cath groupLink + cath #> "#privacy 123" -- sending incorrect captcha + cath <# "#privacy SimpleX-Directory!> > cath 123" + cath <## " Incorrect text, please try again." + captcha <- dropStrPrefix "#privacy SimpleX-Directory> " . dropTime <$> getTermLine cath + sendCaptcha cath captcha + cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath is connected" + cath #> "#privacy hello" + bob <# "#privacy cath> hello" + cath ##> "/l privacy" + cath <## "#privacy: you left the group" + cath <## "use /d #privacy to delete the group" + bob <## "#privacy: cath left the group" + cath ##> "/d #privacy" + cath <## "#privacy: you deleted the group" + -- change default role to observer + bob #> "@SimpleX-Directory /role 1 observer" + bob <# "SimpleX-Directory> > /role 1 observer" + bob <## " The initial member role for the group privacy is set to observer" + bob <## "" + bob <##. "Please note: it applies only to members joining via this link: https://" + -- connect with captcha screen again, as observer + captcha' <- join cath groupLink + sendCaptcha cath captcha' + -- message from cath that left + pastMember <- dropStrPrefix "#privacy: SimpleX-Directory forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath + cath <# ("#privacy " <> pastMember <> "> hello [>>]") + cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: SimpleX-Directory added cath_1 (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath_1 is connected" + cath ##> "#privacy hello" + cath <## "#privacy: you don't have permission to send messages" + (bob "/ms privacy" + cath <## "cath (Catherine): observer, you, connected" + cath <## "SimpleX-Directory: admin, host, connected" + cath <## "bob (Bob): owner, connected" + cath <## (pastMember <> ": author, status unknown") + where + join cath groupLink = do + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy SimpleX-Directory> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + dropStrPrefix "#privacy SimpleX-Directory> " . dropTime <$> getTermLine cath + sendCaptcha cath captcha = do + cath #> ("#privacy " <> captcha) + cath <# ("#privacy SimpleX-Directory!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" + cath <## "#privacy: you joined the group" + testRestoreDirectory :: HasCallStack => TestParams -> IO () testRestoreDirectory ps = do testListUserGroups ps @@ -974,29 +1151,26 @@ testRestoreDirectory ps = do cath #> "@SimpleX-Directory security" groupFoundN' 2 cath "security" +testCaptcha :: HasCallStack => TestParams -> IO () +testCaptcha _ps = do + let captcha = "23456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghijkmnpqrty" + matchCaptchaStr captcha captcha `shouldBe` True + matchCaptchaStr captcha "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" `shouldBe` True + matchCaptchaStr "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" captcha `shouldBe` True + matchCaptchaStr "OOIICPSUVWXZ" "OOIICPSUVWXZ" `shouldBe` True + matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxz" `shouldBe` True + matchCaptchaStr "0o1lcpsuvwxz" "OOIICPSUVWXZ" `shouldBe` True + matchCaptchaStr "OOIICPSUVWXZ" "" `shouldBe` False + matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwx" `shouldBe` False + matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxzz" `shouldBe` False + listGroups :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () listGroups superUser bob cath = do - bob #> "@SimpleX-Directory /list" - bob <# "SimpleX-Directory> > /list" - bob <## " 2 registered group(s)" - bob <# "SimpleX-Directory> 1. privacy (Privacy)" - bob <## "Welcome message:" - bob <##. "Link to join the group privacy: " - bob <## "3 members" - bob <## "Status: active" - bob <# "SimpleX-Directory> 2. security (Security)" - bob <## "Welcome message:" - bob <##. "Link to join the group security: " - bob <## "2 members" - bob <## "Status: active" - cath #> "@SimpleX-Directory /list" - cath <# "SimpleX-Directory> > /list" - cath <## " 1 registered group(s)" - cath <# "SimpleX-Directory> 1. anonymity (Anonymity)" - cath <## "Welcome message:" - cath <##. "Link to join the group anonymity: " - cath <## "2 members" - cath <## "Status: suspended because roles changed" + sendListCommand bob 2 + groupListing bob 1 "privacy" "Privacy" 3 "active" + groupListing bob 2 "security" "Security" 2 "active" + sendListCommand cath 1 + groupListing cath 1 "anonymity" "Anonymity" 2 "suspended because roles changed" -- superuser lists all groups bob #> "@SimpleX-Directory /last" bob <# "SimpleX-Directory> > /last" @@ -1004,34 +1178,42 @@ listGroups superUser bob cath = do superUser #> "@SimpleX-Directory /last" superUser <# "SimpleX-Directory> > /last" superUser <## " 3 registered group(s)" - superUser <# "SimpleX-Directory> 1. privacy (Privacy)" - superUser <## "Welcome message:" - superUser <##. "Link to join the group privacy: " - superUser <## "Owner: bob" - superUser <## "3 members" - superUser <## "Status: active" - superUser <# "SimpleX-Directory> 2. security (Security)" - superUser <## "Welcome message:" - superUser <##. "Link to join the group security: " - superUser <## "Owner: bob" - superUser <## "2 members" - superUser <## "Status: active" - superUser <# "SimpleX-Directory> 3. anonymity (Anonymity)" - superUser <## "Welcome message:" - superUser <##. "Link to join the group anonymity: " - superUser <## "Owner: cath" - superUser <## "2 members" - superUser <## "Status: suspended because roles changed" + memberGroupListing superUser bob 1 "privacy" "Privacy" 3 "active" + memberGroupListing superUser bob 2 "security" "Security" 2 "active" + memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed" -- showing last 1 group superUser #> "@SimpleX-Directory /last 1" superUser <# "SimpleX-Directory> > /last 1" superUser <## " 3 registered group(s), showing the last 1" - superUser <# "SimpleX-Directory> 3. anonymity (Anonymity)" - superUser <## "Welcome message:" - superUser <##. "Link to join the group anonymity: " - superUser <## "Owner: cath" - superUser <## "2 members" - superUser <## "Status: suspended because roles changed" + memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed" + +listUserGroup :: HasCallStack => TestCC -> String -> String -> IO () +listUserGroup u n fn = do + sendListCommand u 1 + groupListing u 1 n fn 2 "active" + +sendListCommand :: HasCallStack => TestCC -> Int -> IO () +sendListCommand u count = do + u #> "@SimpleX-Directory /list" + u <# "SimpleX-Directory> > /list" + u <## (" " <> show count <> " registered group(s)") + +groupListing :: HasCallStack => TestCC -> Int -> String -> String -> Int -> String -> IO () +groupListing u = groupListing_ u Nothing + +memberGroupListing :: HasCallStack => TestCC -> TestCC -> Int -> String -> String -> Int -> String -> IO () +memberGroupListing su owner = groupListing_ su (Just owner) + +groupListing_ :: HasCallStack => TestCC -> Maybe TestCC -> Int -> String -> String -> Int -> String -> IO () +groupListing_ su owner_ gId n fn count status = do + su <# ("SimpleX-Directory> " <> show gId <> ". " <> n <> " (" <> fn <> ")") + su <## "Welcome message:" + su <##. ("Link to join the group " <> n <> ": ") + forM_ owner_ $ \owner -> do + ownerName <- userName owner + su <## ("Owner: " <> ownerName) + su <## (show count <> " members") + su <## ("Status: " <> status) reapproveGroup :: HasCallStack => Int -> TestCC -> TestCC -> IO () reapproveGroup count superUser bob = do @@ -1046,10 +1228,13 @@ reapproveGroup count superUser bob = do superUser #> "@SimpleX-Directory /approve 1:privacy 1" superUser <# "SimpleX-Directory> > /approve 1:privacy 1" superUser <## " Group approved!" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is approved and listed in directory!" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." bob <## "" - bob <## "Use /filter 1 to configure anti-spam filter and /role 1 to set default member role." + bob <## "Supported commands:" + bob <## "- /filter 1 - to configure anti-spam filter." + bob <## "- /role 1 - to set default member role." + bob <## "- /help commands - other commands." addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO () addCathAsOwner bob cath = do @@ -1121,7 +1306,8 @@ runDirectory cfg opts@DirectoryOpts {directoryLog} action = do where bot st = do env <- newServiceState opts - simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts env + let cfg' = cfg {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}} + simplexChatCore cfg' (mkChatOpts opts) $ directoryService st opts env registerGroup :: TestCC -> TestCC -> String -> String -> IO () registerGroup su u n fn = registerGroupId su u n fn 1 1 @@ -1192,10 +1378,13 @@ approveRegistrationId su u n gId ugId = do su #> ("@SimpleX-Directory " <> approve) su <# ("SimpleX-Directory> > " <> approve) su <## " Group approved!" - u <# ("SimpleX-Directory> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory!") + u <# ("SimpleX-Directory> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory - please moderate it!") u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." u <## "" - u <## ("Use /filter " <> show ugId <> " to configure anti-spam filter and /role " <> show ugId <> " to set default member role.") + u <## "Supported commands:" + u <## ("- /filter " <> show ugId <> " - to configure anti-spam filter.") + u <## ("- /role " <> show ugId <> " - to set default member role.") + u <## "- /help commands - other commands." connectVia :: TestCC -> String -> IO () u `connectVia` dsLink = do diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 03671d1d94..e3bab5a0ec 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -17,6 +17,7 @@ import Control.Concurrent (forkIOWithUnmask, killThread, threadDelay) import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception (bracket, bracket_) +import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.Reader @@ -54,8 +55,8 @@ import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) -import Simplex.Messaging.Server.Env.STM (ServerConfig (..), StartOptions (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) -import Simplex.Messaging.Server.MsgStore.Types (AMSType (..), SMSType (..)) +import Simplex.Messaging.Server.Env.STM (AServerStoreCfg (..), ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) +import Simplex.Messaging.Server.MsgStore.Types (SQSType (..), SMSType (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Server (ServerCredentials (..), defaultTransportServerConfig) import Simplex.Messaging.Version @@ -94,7 +95,6 @@ testOpts :: ChatOpts testOpts = ChatOpts { coreOptions = testCoreOpts, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatCmdLog = CCLNone, @@ -118,7 +118,9 @@ testCoreOpts = { dbConnstr = testDBConnstr, -- dbSchemaPrefix is not used in tests (except bot tests where it's redefined), -- instead different schema prefix is passed per client so that single test database is used - dbSchemaPrefix = "" + dbSchemaPrefix = "", + dbPoolSize = 3, + dbCreateSchema = True #else { dbFilePrefix = "./simplex_v1", -- dbFilePrefix is not used in tests (except bot tests where it's redefined) dbKey = "", -- dbKey = "this is a pass-phrase to encrypt the database", @@ -135,6 +137,7 @@ testCoreOpts = logAgent = Nothing, logFile = Nothing, tbqSize = 16, + deviceName = Nothing, highlyAvailable = False, yesToUpMigrations = False } @@ -184,6 +187,7 @@ testCfg = defaultChatConfig { agentConfig = testAgentCfg, showReceipts = False, + shortLinkPresetServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"], testView = True, tbqSize = 16 } @@ -288,8 +292,8 @@ startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {maintenance} user ct <- newChatTerminal t opts cc <- newChatController db (Just user) cfg opts False void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") `runReaderT` cc - chatAsync <- async . runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts - atomically . unless maintenance $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry + chatAsync <- async $ runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts + unless maintenance $ atomically $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry termQ <- newTQueueIO termAsync <- async $ readTerminalOutput t termQ pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ, printOutput} @@ -391,14 +395,14 @@ withTmpFiles = testChatN :: HasCallStack => ChatConfig -> ChatOpts -> [Profile] -> (HasCallStack => [TestCC] -> IO ()) -> TestParams -> IO () testChatN cfg opts ps test params = - bracket (getTestCCs (zip ps [1 ..]) []) entTests test + bracket (getTestCCs $ zip ps [1 ..]) endTests test where - getTestCCs :: [(Profile, Int)] -> [TestCC] -> IO [TestCC] - getTestCCs [] tcs = pure tcs - getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat params cfg opts (show db) p <*> getTestCCs envs' tcs - entTests tcs = do - concurrentlyN_ $ map ( IO [TestCC] + getTestCCs [] = pure [] + getTestCCs ((p, db) : envs') = (:) <$> createTestChat params cfg opts (show db) p <*> getTestCCs envs' + endTests tcs = do + mapConcurrently_ ( TestCC -> Int -> Expectation ( AServerStoreCfg +persistentServerStoreCfg tmp = ASSCfg SQSMemory SMSMemory $ SSCMemory $ Just StorePaths {storeLogFile = tmp <> "/smp-server-store.log", storeMsgsFile = Just $ tmp <> "/smp-server-messages.log"} + withSmpServer :: IO () -> IO () withSmpServer = withSmpServer' smpServerCfg diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 998835b867..817e16113f 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -270,8 +270,7 @@ testRetryConnecting ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile tes smpServerCfg { transports = [("7003", transport @TLS, False)], msgQueueQuota = 2, - storeLogFile = Just $ tmp <> "/smp-server-store.log", - storeMsgsFile = Just $ tmp <> "/smp-server-messages.log" + serverStoreCfg = persistentServerStoreCfg tmp } fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests cfg' = @@ -329,8 +328,7 @@ testRetryConnectingClientTimeout ps = do smpServerCfg { transports = [("7003", transport @TLS, False)], msgQueueQuota = 2, - storeLogFile = Just $ tmp <> "/smp-server-store.log", - storeMsgsFile = Just $ tmp <> "/smp-server-messages.log" + serverStoreCfg = persistentServerStoreCfg tmp } fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests cfg' = @@ -1115,6 +1113,8 @@ testSendMultiManyBatches = DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]] bobItemsCount `shouldBe` [[300]] + threadDelay 1000000 + testGetSetSMPServers :: HasCallStack => TestParams -> IO () testGetSetSMPServers = testChat aliceProfile $ diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 7ec4033046..431f19c77f 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -79,10 +79,15 @@ chatGroupTests = do it "moderate message of another group member (full delete)" testGroupModerateFullDelete it "moderate message that arrives after the event of moderation" testGroupDelayedModeration it "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete + it "remove member with messages (full deletion is enabled)" testDeleteMemberWithMessages + it "remove member with messages mark deleted" testDeleteMemberMarkMessagesDeleted describe "batch send messages" $ do it "send multiple messages api" testSendMulti it "send multiple timed messages" testSendMultiTimed +#if !defined(dbPostgres) + -- TODO [postgres] this test hangs with PostgreSQL it "send multiple messages (many chat batches)" testSendMultiManyBatches +#endif xit'' "shared message body is reused" testSharedMessageBody xit'' "shared batch body is reused" testSharedBatchBody describe "async group connections" $ do @@ -1801,6 +1806,67 @@ testGroupDelayedModerationFullDelete ps = do -- version before forwarding, so cath doesn't expect alice to forward messages (groupForwardVersion = 4) cfg = testCfg {chatVRange = mkVersionRange (VersionChat 1) (VersionChat 3)} +testDeleteMemberWithMessages :: HasCallStack => TestParams -> IO () +testDeleteMemberWithMessages = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + threadDelay 750000 + alice ##> "/set delete #team on" + alice <## "updated group preferences:" + alice <## "Full deletion: on" + threadDelay 750000 + concurrentlyN_ + [ do + bob <## "alice updated group #team:" + bob <## "updated group preferences:" + bob <## "Full deletion: on", + do + cath <## "alice updated group #team:" + cath <## "updated group preferences:" + cath <## "Full deletion: on" + ] + threadDelay 750000 + bob #> "#team hello" + concurrently_ + (alice <# "#team bob> hello") + (cath <# "#team bob> hello") + alice #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + bob #$> ("/_get chat #1 count=1", chat, [(1, "hello")]) + cath #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + threadDelay 1000000 + alice ##> "/rm #team bob messages=on" + alice <## "#team: you removed bob from the group with all messages" + bob <## "#team: alice removed you from the group with all messages" + bob <## "use /d #team to delete the group" + cath <## "#team: alice removed bob from the group with all messages" + alice #$> ("/_get chat #1 count=2", chat, [(0, "moderated [deleted by you]"), (1, "removed bob (Bob)")]) + bob #$> ("/_get chat #1 count=2", chat, [(1, "moderated [deleted by alice]"), (0, "removed you")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "moderated [deleted by alice]"), (0, "removed bob (Bob)")]) + +testDeleteMemberMarkMessagesDeleted :: HasCallStack => TestParams -> IO () +testDeleteMemberMarkMessagesDeleted = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + threadDelay 1000000 + bob #> "#team hello" + concurrently_ + (alice <# "#team bob> hello") + (cath <# "#team bob> hello") + alice #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + bob #$> ("/_get chat #1 count=1", chat, [(1, "hello")]) + cath #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + threadDelay 1000000 + alice ##> "/rm #team bob messages=on" + alice <## "#team: you removed bob from the group with all messages" + bob <## "#team: alice removed you from the group with all messages" + bob <## "use /d #team to delete the group" + cath <## "#team: alice removed bob from the group with all messages" + alice #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by you]"), (1, "removed bob (Bob)")]) + bob #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted by alice]"), (0, "removed you")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (0, "removed bob (Bob)")]) + testSendMulti :: HasCallStack => TestParams -> IO () testSendMulti = testChat3 aliceProfile bobProfile cathProfile $ @@ -1925,8 +1991,7 @@ testSharedMessageBody ps = serverCfg' = smpServerCfg { transports = [("7003", transport @TLS, False)], - storeLogFile = Just $ tmp <> "/smp-server-store.log", - storeMsgsFile = Just $ tmp <> "/smp-server-messages.log" + serverStoreCfg = persistentServerStoreCfg tmp } opts' = testOpts @@ -1982,8 +2047,7 @@ testSharedBatchBody ps = serverCfg' = smpServerCfg { transports = [("7003", transport @TLS, False)], - storeLogFile = Just $ tmp <> "/smp-server-store.log", - storeMsgsFile = Just $ tmp <> "/smp-server-messages.log" + serverStoreCfg = persistentServerStoreCfg tmp } opts' = testOpts @@ -6435,7 +6499,7 @@ testForwardQuoteMention = bob <## " hello @alice @cath", do cath <# "#team alice!> -> forwarded" - cath <## " hello @alice @cath" + cath <## " hello @alice @cath" ] -- forward mentions alice `send` "@bob <- #team hello" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 5d2b9f5ba8..adff745200 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -101,6 +101,11 @@ chatProfileTests = do it "files & media" testGroupPrefsFilesForRole it "SimpleX links" testGroupPrefsSimplexLinksForRole it "set user, contact and group UI theme" testSetUITheme + describe "short links" $ do + it "should connect via one-time inviation" testShortLinkInvitation + it "should plan and connect via one-time inviation" testPlanShortLinkInvitation + it "should connect via contact address" testShortLinkContactAddress + it "should join group" testShortLinkJoinGroup testUpdateProfile :: HasCallStack => TestParams -> IO () testUpdateProfile = @@ -311,8 +316,7 @@ testRetryAcceptingViaContactLink ps = testChatCfgOpts2 cfg' opts' aliceProfile b smpServerCfg { transports = [("7003", transport @TLS, False)], msgQueueQuota = 2, - storeLogFile = Just $ tmp <> "/smp-server-store.log", - storeMsgsFile = Just $ tmp <> "/smp-server-messages.log" + serverStoreCfg = persistentServerStoreCfg tmp } fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests cfg' = @@ -1823,7 +1827,7 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $ \alice bob -> do -- Create a new invite alice ##> "/connect" - inv <- getInvitation alice + _ <- getInvitation alice -- Create new user and go back to original user alice ##> "/create user alisa" showActiveUser alice "alisa" @@ -1833,12 +1837,18 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $ showActiveUser alice "alice (Alice)" -- Change connection to newly created user alice ##> "/_set conn user :1 2" - alice <## "connection 1 changed from user alice to user alisa" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + _ <- getTermLine alice + alice <## "" alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection back to other user alice ##> "/_set conn user :1 3" - alice <## "connection 1 changed from user alisa to user alisa2" + alice <## "connection 1 changed from user alisa to user alisa2, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" alice ##> "/user alisa2" showActiveUser alice "alisa2" -- Connect @@ -1847,13 +1857,14 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $ concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alisa2: contact is connected") + alice <##> bob testChangePCCUserFromIncognito :: HasCallStack => TestParams -> IO () testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do -- Create a new invite and set as incognito alice ##> "/connect" - inv <- getInvitation alice + _ <- getInvitation alice alice ##> "/_set incognito :1 on" alice <## "connection 1 changed to incognito" -- Create new user and go back to original user @@ -1863,13 +1874,19 @@ testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ showActiveUser alice "alice (Alice)" -- Change connection to newly created user alice ##> "/_set conn user :1 2" - alice <## "connection 1 changed from user alice to user alisa" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + _ <- getTermLine alice + alice <## "" alice `hasContactProfiles` ["alice"] alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection back to initial user alice ##> "/_set conn user :1 1" - alice <## "connection 1 changed from user alisa to user alice" + alice <## "connection 1 changed from user alisa to user alice, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" alice ##> "/user alice" showActiveUser alice "alice (Alice)" -- Connect @@ -1878,13 +1895,14 @@ testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") + alice <##> bob testChangePCCUserAndThenIncognito :: HasCallStack => TestParams -> IO () testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do -- Create a new invite and set as incognito alice ##> "/connect" - inv <- getInvitation alice + _ <- getInvitation alice -- Create new user and go back to original user alice ##> "/create user alisa" showActiveUser alice "alisa" @@ -1892,7 +1910,10 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ showActiveUser alice "alice (Alice)" -- Change connection to newly created user alice ##> "/_set conn user :1 2" - alice <## "connection 1 changed from user alice to user alisa" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection to incognito and make sure it's attached to the newly created user profile @@ -1907,6 +1928,10 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> alisaIncognito) alice <## ("use /i bob to print out this incognito profile again") ] + alice ?#> "@bob hi" + bob <# (alisaIncognito <> "> hi") + bob #> ("@" <> alisaIncognito <> " hey") + alice ?<# "bob> hey" testChangePCCUserDiffSrv :: HasCallStack => TestParams -> IO () testChangePCCUserDiffSrv ps = do @@ -1947,6 +1972,7 @@ testChangePCCUserDiffSrv ps = do concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alisa: contact is connected") + alice <##> bob where serverCfg' = smpServerCfg @@ -2584,3 +2610,162 @@ testSetUITheme = groupInfo a = do a <## "group ID: 1" a <## "current members: 1" + +testShortLinkInvitation :: HasCallStack => TestParams -> IO () +testShortLinkInvitation = + testChat2 aliceProfile bobProfile $ \alice bob -> do + alice ##> "/c short" + inv <- getShortInvitation alice + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + alice #> "@bob hi" + bob <# "alice> hi" + bob #> "@alice hey" + alice <# "bob> hey" + +testPlanShortLinkInvitation :: HasCallStack => TestParams -> IO () +testPlanShortLinkInvitation = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + alice ##> "/c short" + inv <- getShortInvitation alice + alice ##> ("/_connect plan 1 " <> inv) + alice <## "invitation link: own link" + alice ##> ("/_connect plan 1 " <> slSimplexScheme inv) + alice <## "invitation link: own link" + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + -- nobody else can connect + cath ##> ("/_connect plan 1 " <> inv) + cath <##. "error: connection authorization failed" + cath ##> ("/c " <> inv) + cath <##. "error: connection authorization failed" + -- bob can retry "plan" + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + -- with simplex: scheme too + bob ##> ("/_connect plan 1 " <> slSimplexScheme inv) + bob <## "invitation link: ok to connect" + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + alice #> "@bob hi" + bob <# "alice> hi" + bob #> "@alice hey" + alice <# "bob> hey" + bob ##> ("/_connect plan 1 " <> inv) + bob <##. "error: connection authorization failed" + alice ##> ("/_connect plan 1 " <> inv) + alice <##. "error: connection authorization failed" -- short_link_inv and conn_req_inv are removed after connection + +slSimplexScheme :: String -> String +slSimplexScheme sl = T.unpack $ T.replace "https://localhost/" "simplex:/" (T.pack sl) <> "?h=localhost" + +testShortLinkContactAddress :: HasCallStack => TestParams -> IO () +testShortLinkContactAddress = + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do + alice ##> "/ad short" + (shortLink, fullLink) <- getShortContactLink alice True + alice ##> ("/_connect plan 1 " <> shortLink) + alice <## "contact address: own address" + alice ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + alice <## "contact address: own address" + alice ##> ("/_connect plan 1 " <> fullLink) + alice <## "contact address: own address" + (alice, bob) `connectVia` shortLink + bob ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + bob <## "contact address: known contact alice" + bob <## "use @alice to send messages" + (alice, cath) `connectVia` slSimplexScheme shortLink + cath ##> ("/_connect plan 1 " <> shortLink) + cath <## "contact address: known contact alice" + cath <## "use @alice to send messages" + (alice, dan) `connectVia` fullLink + where + (alice, cc) `connectVia` cLink = do + name <- userName cc + sName <- showName cc + cc ##> ("/_connect plan 1 " <> cLink) + cc <## "contact address: ok to connect" + cc ##> ("/c " <> cLink) + alice <#? cc + alice ##> ("/ac " <> name) + alice <## (sName <> ": accepting contact request, you can send messages to contact") + concurrently_ + (cc <## "alice (Alice): contact is connected") + (alice <## (sName <> ": contact is connected")) + cc ##> ("/_connect plan 1 " <> cLink) + cc <## "contact address: known contact alice" + cc <## "use @alice to send messages" + +testShortLinkJoinGroup :: HasCallStack => TestParams -> IO () +testShortLinkJoinGroup = + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do + threadDelay 100000 + alice ##> "/ad short" -- create the address to test that it can co-exist with group link + _ <- getShortContactLink alice True + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/create link #team short" + (shortLink, fullLink) <- getShortGroupLink alice "team" GRMember True + alice ##> ("/_connect plan 1 " <> shortLink) + alice <## "group link: own link for group #team" + alice ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + alice <## "group link: own link for group #team" + alice ##> ("/_connect plan 1 " <> fullLink) + alice <## "group link: own link for group #team" + joinGroup alice bob shortLink + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + bob ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + joinGroup alice cath $ slSimplexScheme shortLink + concurrentlyN_ + [ do + bob <## "#team: alice added cath (Catherine) to the group (connecting...)" + bob <## "#team: new member cath is connected", + cath <## "#team: member bob (Bob) is connected" + ] + cath ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + cath <## "group link: known group #team" + cath <## "use #team to send messages" + cath ##> ("/_connect plan 1 " <> shortLink) + cath <## "group link: known group #team" + cath <## "use #team to send messages" + joinGroup alice dan fullLink + concurrentlyN_ + [ do + bob <## "#team: alice added dan (Daniel) to the group (connecting...)" + bob <## "#team: new member dan is connected", + do + cath <## "#team: alice added dan (Daniel) to the group (connecting...)" + cath <## "#team: new member dan is connected", + do + dan <## "#team: member bob (Bob) is connected" + dan <## "#team: member cath (Catherine) is connected" + ] + dan ##> ("/_connect plan 1 " <> fullLink) + dan <## "group link: known group #team" + dan <## "use #team to send messages" + where + joinGroup alice cc link = do + name <- userName cc + sName <- showName cc + cc ##> ("/_connect plan 1 " <> link) + cc <## "group link: ok to connect" + cc ##> ("/c " <> link) + cc <## "connection request sent!" + alice <## (sName <> ": accepting request to join group #team...") + concurrentlyN_ + [ alice <## ("#team: " <> name <> " joined the group"), + do + cc <## "#team: joining the group..." + cc <## "#team: you joined the group" + ] diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 9a3b560b29..3ae3f61ca7 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -11,7 +11,7 @@ module ChatTests.Utils where import ChatClient import ChatTests.DBUtils import Control.Concurrent (threadDelay) -import Control.Concurrent.Async (concurrently_) +import Control.Concurrent.Async (concurrently_, mapConcurrently_) import Control.Concurrent.STM import Control.Monad (unless, when) import Control.Monad.Except (runExceptT) @@ -424,7 +424,7 @@ getInAnyOrder f cc ls = do cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line (*<#) :: HasCallStack => [TestCC] -> String -> Expectation -ccs *<# line = concurrentlyN_ $ map (<# line) ccs +ccs *<# line = mapConcurrently_ (<# line) ccs (?<#) :: HasCallStack => TestCC -> String -> Expectation cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line @@ -502,14 +502,27 @@ dropPartialReceipt_ msg = case splitAt 2 msg of _ -> Nothing getInvitation :: HasCallStack => TestCC -> IO String -getInvitation cc = do +getInvitation = getInvitation_ False + +getShortInvitation :: HasCallStack => TestCC -> IO String +getShortInvitation = getInvitation_ True + +getInvitation_ :: HasCallStack => Bool -> TestCC -> IO String +getInvitation_ short cc = do cc <## "pass this invitation link to your contact (via another channel):" cc <## "" inv <- getTermLine cc cc <## "" cc <## "and ask them to connect: /c " + when short $ cc <##. "The invitation link for old clients: https://simplex.chat/invitation#" pure inv +getShortContactLink :: HasCallStack => TestCC -> Bool -> IO (String, String) +getShortContactLink cc created = do + shortLink <- getContactLink cc created + fullLink <- dropLinePrefix "The contact link for old clients: " =<< getTermLine cc + pure (shortLink, fullLink) + getContactLink :: HasCallStack => TestCC -> Bool -> IO String getContactLink cc created = do cc <## if created then "Your new chat address is created!" else "Your chat address:" @@ -522,6 +535,17 @@ getContactLink cc created = do cc <## "to delete it: /da (accepted contacts will remain connected)" pure link +dropLinePrefix :: String -> String -> IO String +dropLinePrefix line s + | line `isPrefixOf` s = pure $ drop (length line) s + | otherwise = error $ "expected to start from: " <> line <> ", got: " <> s + +getShortGroupLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO (String, String) +getShortGroupLink cc gName mRole created = do + shortLink <- getGroupLink cc gName mRole created + fullLink <- dropLinePrefix "The group link for old clients: " =<< getTermLine cc + pure (shortLink, fullLink) + getGroupLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO String getGroupLink cc gName mRole created = do cc <## if created then "Group link is created!" else "Group link:" diff --git a/tests/JSONFixtures.hs b/tests/JSONFixtures.hs index 32e90cf754..bd74f44022 100644 --- a/tests/JSONFixtures.hs +++ b/tests/JSONFixtures.hs @@ -5,55 +5,55 @@ module JSONFixtures where import qualified Data.ByteString.Lazy.Char8 as LB noActiveUserSwift :: LB.ByteString -noActiveUserSwift = "{\"resp\":{\"_owsf\":true,\"chatCmdError\":{\"chatError\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"noActiveUser\":{}}}}}}}" +noActiveUserSwift = "{\"error\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"noActiveUser\":{}}}}}" noActiveUserTagged :: LB.ByteString -noActiveUserTagged = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}}" +noActiveUserTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}" activeUserExistsSwift :: LB.ByteString -activeUserExistsSwift = "{\"resp\":{\"_owsf\":true,\"chatCmdError\":{\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"userExists\":{\"contactName\":\"alice\"}}}}}}}" +activeUserExistsSwift = "{\"error\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"userExists\":{\"contactName\":\"alice\"}}}}}" activeUserExistsTagged :: LB.ByteString -activeUserExistsTagged = "{\"resp\":{\"type\":\"chatCmdError\",\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}}" +activeUserExistsTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}" activeUserSwift :: LB.ByteString -activeUserSwift = "{\"resp\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}" +activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}" activeUserTagged :: LB.ByteString -activeUserTagged = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}" +activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}" chatStartedSwift :: LB.ByteString -chatStartedSwift = "{\"resp\":{\"_owsf\":true,\"chatStarted\":{}}}" +chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}" chatStartedTagged :: LB.ByteString -chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}" +chatStartedTagged = "{\"result\":{\"type\":\"chatStarted\"}}" networkStatusesSwift :: LB.ByteString -networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}" +networkStatusesSwift = "{\"result\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}" networkStatusesTagged :: LB.ByteString -networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}" +networkStatusesTagged = "{\"result\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}" userJSON :: LB.ByteString userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}" memberSubSummarySwift :: LB.ByteString -memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}" +memberSubSummarySwift = "{\"result\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}" memberSubSummaryTagged :: LB.ByteString -memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}" +memberSubSummaryTagged = "{\"result\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}" userContactSubSummarySwift :: LB.ByteString -userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}" +userContactSubSummarySwift = "{\"result\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}" userContactSubSummaryTagged :: LB.ByteString -userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}" +userContactSubSummaryTagged = "{\"result\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}" pendingSubSummarySwift :: LB.ByteString -pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}" +pendingSubSummarySwift = "{\"result\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}" pendingSubSummaryTagged :: LB.ByteString -pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}" +pendingSubSummaryTagged = "{\"result\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}" parsedMarkdownSwift :: LB.ByteString parsedMarkdownSwift = "{\"formattedText\":[{\"format\":{\"_owsf\":true,\"bold\":{}},\"text\":\"hello\"}]}" diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index ec4e336fe9..05d5362cf1 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -8,7 +8,9 @@ module MarkdownTests where import Data.List.NonEmpty (NonEmpty) import Data.Text (Text) import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Markdown +import Simplex.Messaging.Encoding.String import System.Console.ANSI.Types import Test.Hspec @@ -169,7 +171,12 @@ uri :: Text -> Markdown uri = Markdown $ Just Uri simplexLink :: SimplexLinkType -> Text -> NonEmpty Text -> Text -> Markdown -simplexLink linkType simplexUri smpHosts = Markdown $ Just SimplexLink {linkType, simplexUri, smpHosts} +simplexLink linkType uriText smpHosts t = Markdown (simplexLinkFormat linkType uriText smpHosts) t + +simplexLinkFormat :: SimplexLinkType -> Text -> NonEmpty Text -> Maybe Format +simplexLinkFormat linkType uriText smpHosts = case strDecode $ encodeUtf8 uriText of + Right simplexUri -> Just SimplexLink {linkType, simplexUri, smpHosts} + Left e -> error e textWithUri :: Spec textWithUri = describe "text with Uri" do @@ -185,10 +192,23 @@ textWithUri = describe "text with Uri" do "https://github.com/simplex-chat/ - SimpleX on GitHub" <==> uri "https://github.com/simplex-chat/" <> " - SimpleX on GitHub" -- "SimpleX on GitHub (https://github.com/simplex-chat/)" <==> "SimpleX on GitHub (" <> uri "https://github.com/simplex-chat/" <> ")" "https://en.m.wikipedia.org/wiki/Servo_(software)" <==> uri "https://en.m.wikipedia.org/wiki/Servo_(software)" + "example.com" <==> uri "example.com" + "example.com." <==> uri "example.com" <> "." + "example.com..." <==> uri "example.com" <> "..." + "www.example.com" <==> uri "www.example.com" + "example.academy" <==> uri "example.academy" + "this is example.com" <==> "this is " <> uri "example.com" + "x.com" <==> uri "x.com" it "ignored as markdown" do "_https://simplex.chat" <==> "_https://simplex.chat" "this is _https://simplex.chat" <==> "this is _https://simplex.chat" "this is https://" <==> "this is https://" + "example.c" <==> "example.c" + "www.www.example.com" <==> "www.www.example.com" + "www.example1.com" <==> "www.example1.com" + "www." <==> "www." + ".com" <==> ".com" + "example.academytoolong" <==> "example.academytoolong" it "SimpleX links" do let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" ("https://simplex.chat" <> inv) <==> simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv) @@ -213,12 +233,14 @@ textWithEmail = describe "text with Email" do "test chat.chat+123@simplex.chat" <==> "test " <> email "chat.chat+123@simplex.chat" "chat@simplex.chat test" <==> email "chat@simplex.chat" <> " test" "test1 chat@simplex.chat test2" <==> "test1 " <> email "chat@simplex.chat" <> " test2" - it "ignored as markdown" do + "test chat@simplex.chat." <==> "test " <> email "chat@simplex.chat" <> "." + "test chat@simplex.chat..." <==> "test " <> email "chat@simplex.chat" <> "..." + it "ignored as email markdown" do "chat @simplex.chat" <==> "chat " <> mention "simplex.chat" "@simplex.chat" "this is chat @simplex.chat" <==> "this is chat " <> mention "simplex.chat" "@simplex.chat" - "this is chat@ simplex.chat" <==> "this is chat@ simplex.chat" - "this is chat @ simplex.chat" <==> "this is chat @ simplex.chat" - "*this* is chat @ simplex.chat" <==> bold "this" <> " is chat @ simplex.chat" + "this is chat@ simplex.chat" <==> "this is chat@ " <> uri "simplex.chat" + "this is chat @ simplex.chat" <==> "this is chat @ " <> uri "simplex.chat" + "*this* is chat @ simplex.chat" <==> bold "this" <> " is chat @ " <> uri "simplex.chat" phone :: Text -> Markdown phone = Markdown $ Just Phone @@ -251,8 +273,13 @@ textWithMentions = describe "text with mentions" do "@alice" <==> mention "alice" "@alice" "hello @alice" <==> "hello " <> mention "alice" "@alice" "hello @alice !" <==> "hello " <> mention "alice" "@alice" <> " !" + "hello @alice!" <==> "hello " <> mention "alice" "@alice" <> "!" + "hello @alice..." <==> "hello " <> mention "alice" "@alice" <> "..." + "hello @alice@example.com" <==> "hello " <> mention "alice@example.com" "@alice@example.com" + "hello @'alice @ example.com'" <==> "hello " <> mention "alice @ example.com" "@'alice @ example.com'" "@'alice jones'" <==> mention "alice jones" "@'alice jones'" "hello @'alice jones'!" <==> "hello " <> mention "alice jones" "@'alice jones'" <> "!" + "hello @'a.j.'!" <==> "hello " <> mention "a.j." "@'a.j.'" <> "!" it "ignored as markdown" $ do "hello @'alice jones!" <==> "hello @'alice jones!" "hello @bob @'alice jones!" <==> "hello " <> mention "bob" "@bob" <> " @'alice jones!" @@ -275,6 +302,6 @@ multilineMarkdownList = describe "multiline markdown" do it "multiline with simplex link" do ("https://simplex.chat" <> inv <> "\ntext") <<==>> - [ FormattedText (Just $ SimplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"]) ("https://simplex.chat" <> inv), + [ FormattedText (simplexLinkFormat XLInvitation ("simplex:" <> inv) ["smp.simplex.im"]) ("https://simplex.chat" <> inv), "\ntext" ] diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 41d7280121..11a89bc62e 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -29,7 +29,7 @@ import Foreign.Storable (peek) import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding) import JSONFixtures import Simplex.Chat.Controller (ChatController (..)) -import Simplex.Chat.Mobile +import Simplex.Chat.Mobile hiding (error) import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.WebRTC diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs index 0a00d7b83c..dbfde6a03d 100644 --- a/tests/OperatorTests.hs +++ b/tests/OperatorTests.hs @@ -19,6 +19,7 @@ import qualified Data.List.NonEmpty as L import Simplex.Chat import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) import Simplex.Chat.Operators +import Simplex.Chat.Operators.Presets import Simplex.Chat.Types import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 83087a89a8..1d37a52459 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -35,7 +35,7 @@ queue = { smpServer = srv, senderId = EntityId "\223\142z\251", dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=", - sndSecure = False + queueMode = Nothing } connReqData :: ConnReqUriData @@ -107,7 +107,7 @@ testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), contactLink = Nothing, preferences = testChatPreferences} testGroupProfile :: GroupProfile -testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, image = Nothing, groupPreferences = testGroupPreferences} +testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, image = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do @@ -201,7 +201,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}" #==# XMsgDeleted it "x.file" $ - "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing} it "x.file without file invitation" $ "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" @@ -210,7 +210,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg" it "x.file.acpt.inv" $ - "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg" it "x.file.acpt.inv" $ "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" @@ -237,10 +237,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" ==# XContact testProfile Nothing it "x.grp.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing} it "x.grp.inv with group link id" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} it "x.grp.acpt without incognito profile" $ "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" @@ -261,16 +261,16 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} (Just MemberRestrictions {restriction = MRSBlocked}) it "x.grp.mem.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.inv w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.fwd" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" @@ -283,7 +283,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XGrpMemConAll (MemberId "\1\2\3\4") it "x.grp.mem.del" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}" - #==# XGrpMemDel (MemberId "\1\2\3\4") + #==# XGrpMemDel (MemberId "\1\2\3\4") False it "x.grp.leave" $ "{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}" ==# XGrpLeave @@ -291,10 +291,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}" ==# XGrpDel it "x.grp.direct.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XGrpDirectInv testConnReq (Just $ MCText "hello") it "x.grp.direct.inv without content" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XGrpDirectInv testConnReq Nothing -- it "x.grp.msg.forward" -- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}" diff --git a/tests/ValidNames.hs b/tests/ValidNames.hs index 03089b1721..22ac4a695d 100644 --- a/tests/ValidNames.hs +++ b/tests/ValidNames.hs @@ -38,3 +38,5 @@ testMkValidName = do mkValidName "alice@example.com" `shouldBe` "alice@example.com" mkValidName "alice <> bob" `shouldBe` "alice <> bob" mkValidName "alice -> bob" `shouldBe` "alice -> bob" + mkValidName "01234567890123456789012345678901234567890123456789extra" `shouldBe` "01234567890123456789012345678901234567890123456789" + mkValidName "0123456789012345678901234567890123456789012345678 extra" `shouldBe` "0123456789012345678901234567890123456789012345678" diff --git a/website/langs/ar.json b/website/langs/ar.json index f257c1c747..0c1fb4515a 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -29,9 +29,9 @@ "simplex-explained-tab-2-text": "2. كيف يعمل", "simplex-explained-tab-1-p-2": "كيف يمكن أن تعمل مع قوائم انتظار أحادية الاتجاه وبدون معرّفات ملف تعريف المستخدم؟", "simplex-explained-tab-2-p-1": "لكل اتصال، تستخدم قائمتي انتظار منفصلتين للمُراسلة لإرسال واستلام الرسائل عبر خوادم مختلفة.", - "simplex-explained-tab-2-p-2": "تقوم الخوادم بتمرير الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمُحادثات المستخدم أو اتصالاته.", + "simplex-explained-tab-2-p-2": "تمرّر الخوادم الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمُحادثات المستخدم أو اتصالاته.", "simplex-explained-tab-3-p-1": "تحتوي الخوادم على بيانات اعتماد مجهولة منفصلة لكل قائمة انتظار، ولا تعرف المستخدمين الذين ينتمون إليهم.", - "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2024", + "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2025", "simplex-chat-protocol": "بروتوكول دردشة SimpleX", "developers": "المطورين", "hero-subheader": "أول نظام مُراسلة
دون معرّفات مُستخدم", @@ -51,13 +51,13 @@ "simplex-private-5-title": "طبقات متعددة من
حشوة المحتوى", "simplex-private-7-title": "التحقق
من سلامة الرسالة", "simplex-private-8-title": "خلط الرسائل
لتقليل من الارتباط", - "simplex-private-10-title": "معرفات زوجية مجهولة مؤقتة", + "simplex-private-10-title": "معرّفات زوجية مجهولة مؤقتة", "simplex-private-card-3-point-1": "يتم استخدام TLS 1.2 / 1.3 مع خوارزميات قوية فقط لاتصالات الخادم والعميل.", "simplex-private-card-3-point-2": "تعمل بصمة الخادم وربط القناة على منع هجمات الوسيط (MITM) وإعادة التشغيل.", "simplex-private-card-3-point-3": "استئناف الاتصال معطل لمنع هجمات الجلسة.", "simplex-private-card-4-point-1": "لحماية عنوان IP الخاص بك، يمكنك الوصول إلى الخوادم عبر تور أو بعض شبكات تراكب النقل الأخرى.", "simplex-private-card-5-point-1": "يستخدم SimpleX حشوة المحتوى لكل طبقة تعمية لإحباط هجمات حجم الرسالة.", - "simplex-private-card-6-point-2": "لمنع ذلك، تقوم تطبيقات SimpleX بتمرير مفاتيح لمرة واحدة خارج النطاق، عند مشاركة عنوان كرابط أو رمز QR.", + "simplex-private-card-6-point-2": "لمنع ذلك، تقوم تطبيقات SimpleX بتمرير مفاتيح لمرة واحدة خارج النطاق، عندما تُشارك عنوان كرابط أو رمز QR.", "simplex-private-card-8-point-1": "تعمل خوادم SimpleX كعقد مختلطة بزمن انتقال منخفض — الرسائل الواردة والصادرة لها ترتيب مختلف.", "simplex-private-card-9-point-1": "كل رسالة انتظار تمرر الرسائل في اتجاه واحد، بعناوين إرسال واستلام مختلفة.", "simplex-private-card-9-point-2": "إنه يقلل من نواقل الهجوم، مقارنة بوسطاء الرسائل التقليديين، والبيانات الوصفية المتاحة.", @@ -74,7 +74,7 @@ "simplex-unique-1-overlay-1-title": "الخصوصية الكاملة لهويتك وملفك الشخصي وجهات الاتصال والبيانات الوصفية", "simplex-unique-2-title": "أنت محمي
من رسائل الإزعاج وإساءة الاستخدام", "simplex-unique-3-title": "أنت تتحكم في بياناتك", - "simplex-unique-3-overlay-1-title": "ملكية ومراقبة وأمان بياناتك", + "simplex-unique-3-overlay-1-title": "الملكية والتحكم وأمن بياناتك", "simplex-unique-4-title": "أنت تمتلك شبكة SimpleX", "simplex-unique-4-overlay-1-title": "لامركزية بالكامل — يمتلك المستخدمون شبكة SimpleX", "hero-overlay-card-1-p-4": "هذا التصميم يمنع تسريب أي البيانات الوصفية للمستخدمين على مستوى التطبيق. لزيادة تحسين الخصوصية وحماية عنوان IP الخاص بك، يمكنك الاتصال بخوادم المراسلة عبر Tor.", @@ -85,23 +85,23 @@ "privacy-matters-overlay-card-1-p-2": "يعرف تجار التجزئة عبر الإنترنت أن الأشخاص ذوي الدخل المنخفض هم أكثر عرضة لإجراء عمليات شراء عاجلة، لذلك قد يفرضون أسعارًا أعلى أو يزيلون الخصومات.", "simplex-private-6-title": "تبديل
خارج النطاق", "simplex-private-9-title": "قوائم انتظار
أحادية الاتجاه", - "privacy-matters-overlay-card-1-p-4": "تحمي منصة SimpleX خصوصية اتصالاتك بشكل أفضل من أي بديل آخر، مما يمنع تمامًا الرسم البياني الاجتماعي الخاص بك من أن يصبح متاحًا لأي شركات أو مؤسسات. حتى عندما يستخدم الأشخاص الخوادم التي توفرها SimpleX Chat، فإننا لا نعرف عدد المستخدمين أو اتصالاتهم.", + "privacy-matters-overlay-card-1-p-4": "تحمي شبكة SimpleX خصوصية اتصالاتك بشكل أفضل من أي بديل آخر، مما يمنع تمامًا الرسم البياني الاجتماعي الخاص بك من أن يصبح متاحًا لأي شركات أو مؤسسات. حتى عندما يستخدم الأشخاص الخوادم التي توفرها SimpleX Chat، فإننا لا نعرف عدد المستخدمين أو اتصالاتهم.", "simplex-private-card-1-point-1": "بروتوكول السقاطة المزدوجة —
رسائل OTR مع السرية المستمرة واستعادة الاختراق.", "simplex-private-card-1-point-2": "NaCL cryptobox في كل قائمة انتظار لمنع ارتباط حركة مرور البيانات بين قوائم انتظار الرسائل في حالة اختراق TLS.", "simplex-private-card-2-point-1": "طبقة إضافية من تعمية الخادم للتسليم إلى المُستلم، لمنع الارتباط بين حركة مرور بيانات الخادم المُستلمة والمُرسلة في حالة اختراق TLS.", "simplex-private-card-4-point-2": "لاستخدام SimpleX عبر تور، يُرجى تثبيت تطبيق Orbot وتمكّين وكيل SOCKS5 (أو VPN على iOS ).", "simplex-private-card-5-point-2": "يجعل الرسائل ذات الأحجام المختلفة تبدو متشابهة للخوادم ومراقبي الشبكة.", - "simplex-private-card-6-point-1": "العديد من منصات الاتصال عرضة لهجمات الوسيط (MITM) من قبل الخوادم أو موفري الشبكات.", + "simplex-private-card-6-point-1": "العديد من شبكات التواصل عرضة لهجمات الوسيط (MITM) من قِبل الخوادم أو موفري الشبكات.", "simplex-private-card-7-point-1": "لضمان سلامة الرسائل يتم ترقيمها بالتسلسل وتضمين تجزئة الرسالة السابقة.", "simplex-private-card-7-point-2": "إذا أُضيفت أي رسالة أو أُزيلت أو تغيّرت، فسيتم تنبيه المُستلم.", "simplex-private-card-10-point-2": "يسمح بتسليم الرسائل بدون معرّفات ملف تعريف المستخدم، مما يوفر خصوصية للبيانات الوصفية أفضل من البدائل.", "privacy-matters-2-overlay-1-linkText": "تمنحك الخصوصية القوة", "simplex-unique-2-overlay-1-title": "أفضل حماية من رسائل الإزعاج وإساءة الاستخدام", - "hero-overlay-card-1-p-3": "أنت تحدد الخادم (الخوادم) المراد استخدامه لاستلام الرسائل وجهات الاتصال الخاصة بك — الخوادم التي تستخدمها لإرسال الرسائل إليهم. من المرجح أن تستخدم كل مُحادثة خادمين مختلفين.", + "hero-overlay-card-1-p-3": "أنت تحدد الخادم (الخوادم) المراد استخدامه لاستلام الرسائل وجهات اتصالك — الخوادم التي تستخدمها لإرسال الرسائل إليهم. من المرجح أن تستخدم كل مُحادثة خادمين مختلفين.", "hero-overlay-card-1-p-1": "سأل العديد من المستخدمين: إذا لم يكن لدى SimpleX معرّفات مستخدم، فكيف يمكنها معرفة مكان تسليم الرسائل؟ ", - "hero-overlay-card-1-p-2": "لتوصيل الرسائل، بدلاً من معرفات المستخدم التي تستخدمها جميع المنصات الأخرى، يستخدم SimpleX معرّفات مزدوجة مؤقتة مجهولة الهوية لقوائم انتظار الرسائل، مختلفة لكل اتصال من اتصالاتك — لا توجد معرفات مستخدم دائمة.", + "hero-overlay-card-1-p-2": "لتسليم الرسائل، بدلاً من معرّفات المُستخدم المُستخدمة من قِبل جميع الشبكات الأخرى، يستخدم SimpleX معرّفات زوجية مجهولة مؤقتة لقوائم الرسائل، منفصلة لكل اتصال من اتصالاتك — ولا توجد معرّفات طويلة الأجل.", "simplex-network-overlay-card-1-p-1": "بروتوكولات المُراسلة والتطبيقات P2P بها مشاكل مختلفة تجعلها أقل موثوقية من SimpleX وأكثر تعقيدًا في التحليل و عرضة لعدة أنواع من الهجمات.", - "hero-overlay-card-2-p-1": "عندما يكون لدى المستخدمين هويات ثابتة، حتى لو كان هذا مجرد رقم عشوائي، مثل معرف الجلسة، فهناك خطر يتمثل في أن الموفر أو المهاجم يمكنه مراقبة كيفية اتصال المستخدمين وعدد الرسائل التي يرسلونها.", + "hero-overlay-card-2-p-1": "عندما يكون لدى المستخدمين هويات ثابتة، حتى لو كان هذا مجرد رقم عشوائي، مثل معرّف الجلسة، فهناك خطر يتمثل في أن الموفر أو المهاجم يمكنه مراقبة كيفية اتصال المستخدمين وعدد الرسائل التي يرسلونها.", "hero-overlay-card-1-p-6": "اقرأ المزيد في SimpleX whitepaper .", "hero-overlay-card-2-p-3": "حتى مع معظم التطبيقات الخاصة التي تستخدم خدمات Tor v3، إذا تحدثت إلى جهتي اتصال مختلفتين عبر نفس الملف الشخصي، فيمكنهما إثبات أنهما متصلان بنفس الشخص.", "hero-overlay-card-2-p-4": "يحمي SimpleX من هذه الهجمات من خلال عدم وجود أي معرّفات مستخدم في تصميمه. وإذا كنت تستخدم وضع التخفي، فسيكون لديك اسم عرض مختلف لكل جهة اتصال، مع تجنب أي بيانات مشتركة بينهما.", @@ -112,29 +112,28 @@ "simplex-network-overlay-card-1-li-5": "قد تكون جميع شبكات P2P المعروفة عرضة لهجوم Sybil، لأن كل عقدة قابلة للاكتشاف، وتعمل الشبكة ككل. تتطلب الإجراءات المعروفة لتخفيفها إما مكونًا مركزيًا أو إثبات عمل مكلف . لا تحتوي شبكة SimpleX على إمكانية اكتشاف الخادم، فهي مجزأة وتعمل كشبكات فرعية متعددة ومعزولة، مما يجعل الهجمات على مستوى الشبكة مستحيلة.", "privacy-matters-overlay-card-3-p-1": "يجب على الجميع الاهتمام بخصوصية وأمان اتصالاتهم — يمكن للمُحادثات غير الضارة أن تعرضك للخطر، حتى لو لم يكن لديك ما تخفيه.", "privacy-matters-overlay-card-3-p-4": "لا يكفي استخدام برنامج مُراسلة مُعمَّاة بين الطرفين، يجب علينا جميعًا استخدام برامج مُراسلة التي تحمي خصوصية شبكاتنا الشخصية — مع من نحن مرتبطون.", - "simplex-unique-overlay-card-1-p-3": "يحمي هذا التصميم خصوصية الأشخاص الذين تتواصل معهم، ويخفيها عن خوادم منصة SimpleX ومن أي مراقبين. لإخفاء عنوان IP الخاص بك من الخوادم، يمكنك الاتصال بخوادم SimpleX عبر تور.", - "simplex-unique-overlay-card-2-p-1": "نظرًا لعدم وجود معرف لديك على نظام SimpleX، لا يمكن لأي شخص الاتصال بك ما لم تشارك عنوان مستخدم لمرة واحدة أو مؤقتًا، كرمز QR أو رابط.", + "simplex-unique-overlay-card-1-p-3": "يحمي هذا التصميم خصوصية الأشخاص الذين تتواصل معهم، ويخفيها عن خوادم شبكة SimpleX ومن أي مراقبين. لإخفاء عنوان IP الخاص بك من الخوادم، يمكنك الاتصال بخوادم SimpleX عبر تور.", + "simplex-unique-overlay-card-2-p-1": "نظرًا لعدم وجود معرّف لديك على شبكة SimpleX، لا يمكن لأي شخص الاتصال بك ما لم تشارك عنوان مستخدم لمرة واحدة أو مؤقتًا، كرمز QR أو رابط.", "simplex-unique-overlay-card-2-p-2": "حتى مع عنوان المستخدم الاختياري، بينما يمكن استخدامه لإرسال طلبات جهات اتصال مزعجة، يمكنك تغييره أو حذفه بالكامل دون فقد أي من اتصالاتك.", "simplex-unique-overlay-card-3-p-2": "يتم الاحتفاظ بالرسائل المُعمَّاة بين الطرفين مؤقتًا على خوادم ترحيل SimpleX حتى يتم استلامها، ثُمَّ تُحذف نهائيًا.", - "simplex-unique-overlay-card-3-p-4": "لا توجد معرفّات أو نص مُعَمَّى مشترك بين حركة مرور بيانات الخادم المُرسلة والمُستلمة — ؛ إذا كان أي شخص يراقب ذلك، فلن يتمكّن بسهولة من تحديد من يتواصل مع من، حتى لو اختُرق TLS.", - "simplex-unique-card-1-p-1": "يحمي SimpleX خصوصية ملف التعريف الخاص بك، جهات الاتصال والبيانات الوصفية، ويخفيه عن خوادم منصة SimpleX وأي مراقبين.", + "simplex-unique-overlay-card-3-p-4": "لا توجد معرفّات أو نص مُعَمَّى مشترك بين حركة مرور بيانات الخادم المُرسلة والمُستلمة — إذا كان أي شخص يراقب ذلك، فلن يتمكّن بسهولة من تحديد من يتواصل مع من، حتى لو اختُرق TLS.", + "simplex-unique-card-1-p-1": "يحمي SimpleX خصوصية ملف تعريفك، جهات اتصالك والبيانات الوصفية، ويخفيه عن خوادم شبكة SimpleX وأي مراقبين.", "privacy-matters-overlay-card-2-p-1": "منذ وقت ليس ببعيد، لاحظنا أن الانتخابات الرئيسية يتم التلاعب بها بواسطة شركة استشارية ذات سمعة طيبة التي استخدمت الرسوم البيانية الاجتماعية لتشويه نظرتنا للعالم الحقيقي والتلاعب بأصواتنا.", - "privacy-matters-overlay-card-2-p-2": "لكي تكون موضوعيًا وتتخذ قرارات مستقلة، يجب أن تكون متحكمًا في مساحة المعلومات الخاصة بك. هذا ممكن فقط إذا كنت تستخدم منصة اتصالات خاصة لا يمكنها الوصول إلى الرسم البياني الاجتماعي الخاص بك.", - "privacy-matters-overlay-card-2-p-3": "SimpleX هو النظام الأساسي الأول الذي لا يحتوي على أي معرّفات مستخدم صمّم ليكون خاصًا، وبهذه الطريقة تحمي مخطط اتصالاتك بشكل أفضل من أي بديل معروف.", + "privacy-matters-overlay-card-2-p-2": "لكي تكون موضوعيًا وتتخذ قرارات مستقلة، عليك التحكم في مساحة معلوماتك. لا يمكن تحقيق ذلك إلا باستخدام شبكة اتصال خاصة لا يمكنها الوصول إلى حسابك على مواقع التواصل الاجتماعي.", + "privacy-matters-overlay-card-2-p-3": "SimpleX هي أول شبكة لا تحتوي على أي معرّفات مستخدم من حيث التصميم، وبهذه الطريقة تحمي مخطط اتصالاتك بشكل أفضل من أي بديل معروف.", "privacy-matters-overlay-card-3-p-2": "واحدة من أكثر القصص إثارة للصدمة هي تجربة محمدو ولد صلاحي الموصوفة في مذكراته والموضحة في فيلم موريتاني. تم وضعه في معتقل غوانتانامو بدون محاكمة، وتعرض للتعذيب هناك لمدة 15 عامًا بعد مكالمة هاتفية مع قريبه في أفغانستان، للاشتباه في تورطه في هجمات 11 سبتمبر، على الرغم من أنه عاش في ألمانيا طوال السنوات العشر الماضية.", "privacy-matters-overlay-card-3-p-3": "يتم القبض على الأشخاص العاديين بسبب ما يشاركونه عبر الإنترنت، حتى عبر حساباتهم \"المجهولة\"، وحتى في البلدان الديمقراطية.", - "simplex-unique-overlay-card-1-p-1": "على عكس أنظمة المُراسلة الأخرى، لا يحتوي SimpleX على معرّفات مخصصة للمستخدمين. لا يعتمد على أرقام الهواتف أو العناوين المستندة إلى النطاقات (مثل البريد الإلكتروني أو XMPP)، أسماء المستخدمين، المفاتيح العامة أو حتى الأرقام العشوائية لتحديد مستخدميها — لا نعرف عدد الأشخاص الذين يستخدمون خوادم SimpleX الخاصة بنا.", - "simplex-unique-overlay-card-1-p-2": "لتسليم الرسائل، يستخدم SimpleX العناوين المزدوجة المجهولة لقوائم انتظار الرسائل أحادية الاتجاه، منفصلة عن الرسائل المُستلمة والمُرسلة، عادةً عبر خوادم مختلفة. إن استخدام SimpleX يشبه امتلاك بريد إلكتروني أو هاتف “مؤقت” مختلف لكل جهة اتصال، ولا توجد متاعب في إدارتها.", + "simplex-unique-overlay-card-1-p-1": "على عكس شبكات المُراسلة الأخرى، لا يحتوي SimpleX على معرّفات مخصصة للمستخدمين. لا يعتمد على أرقام الهواتف أو العناوين المستندة إلى النطاقات (مثل البريد الإلكتروني أو XMPP)، أسماء المستخدمين، المفاتيح العامة أو حتى الأرقام العشوائية لتحديد مستخدميها — مُشغلي خادم SimpleX لا يعرفون عدد الأشخاص الذين يستخدمون خوادمهم.", + "simplex-unique-overlay-card-1-p-2": "لتسليم الرسائل، يستخدم SimpleX العناوين المزدوجة المجهولة لقوائم انتظار الرسائل أحادية الاتجاه، منفصلة عن الرسائل المُستلمة والمُرسلة، عادةً عبر خوادم مختلفة.", "simplex-unique-overlay-card-3-p-1": "يخزن SimpleX Chat جميع بيانات المستخدم على أجهزة العميل فقط باستخدام تنسيق قاعدة بيانات محمولة مُعمَّاة يمكّن تصديرها ونقلها إلى أي جهاز مدعوم.", "simplex-unique-overlay-card-3-p-3": "على عكس خوادم الشبكات الاتحادية (البريد الإلكتروني أو XMPP أو Matrix)، لا تقوم خوادم SimpleX بتخزين حسابات المستخدمين، فهي تقوم فقط بترحيل الرسائل، مما يحمي خصوصية كلا الطرفين.", - "simplex-unique-overlay-card-4-p-1": "يمكنك استخدام SimpleX مع الخوادم الخاصة بك والاستمرار في التواصل مع الأشخاص الذين يستخدمون الخوادم المهيأة مسبقًا التي نقدمها.", - "simplex-unique-overlay-card-4-p-3": "إذا كنت تفكر في التطوير لمنصة SimpleX، على سبيل المثال، بوت الدردشة لمستخدمي تطبيق SimpleX، أو دمج مكتبة SimpleX Chat في تطبيقات الأجهزة المحمولة، من فضلك تواصل معي لأي نصيحة والدعم.", + "simplex-unique-overlay-card-4-p-1": "يمكنك استخدام SimpleX مع خوادمك والاستمرار في التواصل مع الأشخاص الذين يستخدمون الخوادم المُهيأة مسبقًا في التطبيقات.", + "simplex-unique-overlay-card-4-p-3": "إذا كنت تفكر في التطوير شبكة SimpleX، على سبيل المثال، بوت الدردشة لمستخدمي تطبيق SimpleX، أو دمج مكتبة SimpleX Chat في تطبيقات الأجهزة المحمولة، من فضلك تواصل معي لأي نصيحة والدعم.", "donate-here-to-help-us": "تبرّع هنا لمساعدتنا", "sign-up-to-receive-our-updates": "اشترك للحصول على آخر مستجداتنا", "enter-your-email-address": "أدخل عنوان بريدك الإلكتروني", "get-simplex": "احصل على تطبيق سطح المكتب SimpleX", - "why-simplex-is": "لماذا SimpleX", - "unique": "فريد من نوعه", + "why-simplex-is-unique": "لماذا SimpleX فريد من نوعه", "learn-more": "اقرأ أكثر", "more-info": "معلومات أكثر", "hide-info": "معلومات أقل", @@ -153,16 +152,16 @@ "no-decentralized": "لا - لامركزي", "no-federated": "لا - اتِحاديّ", "comparison-section-list-point-2": "العناوين تعتمد على الـDNS", - "comparison-section-list-point-3": "المفتاح العام أو معرف آخر فريد وعام", + "comparison-section-list-point-3": "المفتاح العام أو معرّف آخر فريد وعام", "comparison-section-list-point-7": "شبكات P2P إما لديها سلطة مركزية أو أن الشبكة كلها يمكن عرضة للخطر", "see-here": "اقرأ هنا", "no-secure": "لا - آمن", "comparison-section-list-point-5": "لا يحمي خصوصية البيانات الوصفية للمستخدمين", - "comparison-section-list-point-6": "على الرغم من أن الـP2P موزعة، إلا أنها ليست اتِحاديَّة - يعملون كشبكة واحدة", + "comparison-section-list-point-6": "على الرغم من أن الـP2P موزعة، إلا أنها ليست اتِحاديَّة — يعملون كشبكة واحدة", "comparison-section-list-point-1": "عادة ما يكون مكوناً من رقم الهاتف، أو اسم المستخدم في بعض الأحيان", "comparison-section-list-point-4": "إذا خوادم المشغّل مُخترقة. تحقق من رمز الأمان في Signal وبعض التطبيقات الأخرى للتخفيف منه", "simplex-unique-card-3-p-1": "يخزن SimpleX جميع بيانات المستخدم على الأجهزة العميلة بتنسيق قاعدة بيانات محمولة مُعمَّاة — يمكّن نقلها إلى جهاز آخر.", - "simplex-unique-card-4-p-1": "شبكة SimpleX لا مركزية بالكامل ومستقلة عن أي عملة مُعمَّاة أو أي منصة أخرى، بخلاف الإنترنت.", + "simplex-unique-card-4-p-1": "شبكة SimpleX لا مركزية بالكامل ومستقلة عن أي عملة مُعمَّاة أو أي شبكة أخرى، بخلاف الإنترنت.", "simplex-unique-card-4-p-2": "يمكنك استخدام SimpleX مع خوادمك الخاصة أو مع الخوادم التي نوفرها — ولا يزال الاتصال ممكن بأي مستخدم.", "join": "انضم إلى", "we-invite-you-to-join-the-conversation": "نحن ندعوك للانضمام إلى المُحادثة", @@ -179,7 +178,7 @@ "use-this-command": "استخدم هذا الأمر:", "see-simplex-chat": "انظر SimpleX Chat", "github-repository": "مستودع Github", - "the-instructions--source-code": "التعليمات حول كيفية تنزيله أو تجميعه من التعليمات البرمجية المصدر.", + "the-instructions--source-code": "للتعليمات حول كيفية تنزيله أو تجميعه من التعليمات البرمجية المصدر.", "if-you-already-installed-simplex-chat-for-the-terminal": "إذا قمت بالفعل بتثبيت SimpleX Chat للوحدة الطرفية", "simplex-chat-for-the-terminal": "SimpleX Chat للوحدة الطرفية", "privacy-matters-section-header": "لماذا الخصوصية مهمة", @@ -190,8 +189,8 @@ "simplex-network-3-header": "شبكة SimpleX", "protocol-1-text": "Signal، منصات كبيرة", "protocol-2-text": "XMPP ،Matrix", - "simplex-unique-card-1-p-2": "بخلاف أي نظام مُراسلة آخر، لا يحتوي SimpleX على معرّفات مخصصة للمستخدمين — ولا حتى أرقام عشوائية.", - "simplex-unique-card-2-p-1": "نظرًا لعدم وجود معرف أو عنوان ثابت على منصة SimpleX، لا يمكن لأي شخص الاتصال بك ما لم تشارك عنوان مستخدم لمرة واحدة أو مؤقتًا، كرمز QR أو رابط.", + "simplex-unique-card-1-p-2": "على عكس أي شبكة مُراسلة آخر، لا يحتوي SimpleX على معرّفات مخصّصة للمستخدمين — ولا حتى أرقام عشوائية.", + "simplex-unique-card-2-p-1": "نظرًا لعدم وجود معرّف أو عنوان ثابت على شبكة SimpleX، لا يمكن لأي شخص الاتصال بك ما لم تشارك عنوان مستخدم لمرة واحدة أو مؤقتًا، كرمز QR أو رابط.", "simplex-unique-card-3-p-2": "يتم الاحتفاظ بالرسائل المُعمَّاة بين الطرفين مؤقتًا على خوادم ترحيل SimpleX حتى يتم استلامها، ثُمَّ تُحذف نهائيًا.", "tap-the-connect-button-in-the-app": "اضغط على زر \"اتصال\" في التطبيق", "scan-the-qr-code-with-the-simplex-chat-app": "امسح رمز QR باستخدام تطبيق SimpleX Chat", @@ -209,7 +208,7 @@ "simplex-network-2-desc": "لا تقوم خوادم الترحيل SimpleX بتخزين ملفات تعريف المستخدمين وجهات الاتصال والرسائل التي تم تسليمها، ولا تتصل ببعضها البعض، ولا يوجد دليل خوادم.", "comparison-point-1-text": "يتطلب هوية عالمية", "protocol-3-text": "بروتوكولات P2P", - "simplex-unique-overlay-card-4-p-2": "تستخدم منصة SimpleX بروتوكول مفتوح وتوفر SDK لإنشاء روبوتات دردشة، مما يسمح بتنفيذ الخدمات التي يمكن للمستخدمين التفاعل معها عبر تطبيقات SimpleX Chat — التي تتطلع حقًا لمعرفة خدمات SimpleX التي يمكنك إنشاؤها.", + "simplex-unique-overlay-card-4-p-2": "تستخدم شبكة SimpleX بروتوكول مفتوح وتوفر SDK لإنشاء روبوتات دردشة، مما يسمح بتنفيذ الخدمات التي يمكن للمستخدمين التفاعل معها عبر تطبيقات SimpleX Chat — التي تتطلع حقًا لمعرفة خدمات SimpleX التي ستبنيها.", "guide-dropdown-1": "بداية سريعة", "guide-dropdown-2": "إرسال الرسائل", "guide-dropdown-3": "مجموعات سرية", @@ -228,7 +227,7 @@ "menu": "قائمة", "on-this-page": "على هذه الصفحة", "back-to-top": "عد إلى الأعلى", - "docs-dropdown-1": "منصة SimpleX", + "docs-dropdown-1": "شبكة SimpleX", "guide": "الدليل", "docs-dropdown-2": "الوصول إلى ملفات اندرويد", "docs-dropdown-3": "الوصول إلى قاعدة بيانات الدردشة", @@ -245,7 +244,7 @@ "f-droid-page-f-droid-org-repo-section-text": "مستودعات SimpleX Chat و F-Droid.org مبنية على مفاتيح مختلفة. للتبديل، يُرجى تصدير قاعدة بيانات الدردشة وإعادة تثبيت التطبيق.", "comparison-section-list-point-4a": "مُرحلات SimpleX لا يمكنها أن تتنازل عن تعمية بين الطرفين. تحقق من رمز الأمان للتخفيف من الهجوم على القناة خارج النطاق", "hero-overlay-3-title": "التقييمات الأمنية", - "hero-overlay-card-3-p-2": "قامت Trail of Bits بمراجعة مكونات التشفير والشبكات الخاصة بمنصة SimpleX في نوفمبر 2022. اقرأ المزيد في الإعلان.", + "hero-overlay-card-3-p-2": "قامت Trail of Bits بمراجعة مكونات التشفير والشبكات الخاصة بشبكة SimpleX في نوفمبر 2022. اقرأ المزيد في الإعلان.", "jobs": "انضم للفريق", "hero-overlay-3-textlink": "التقييمات الأمنية", "hero-overlay-card-3-p-1": "Trail of Bits هي شركة رائدة في مجال الاستشارات الأمنية والتكنولوجية، ومن بين عملائها شركات التكنولوجيا الكبرى والوكالات الحكومية ومشاريع blockchain الكبرى.", diff --git a/website/langs/bg.json b/website/langs/bg.json index 963f850e53..21fb93c2cb 100644 --- a/website/langs/bg.json +++ b/website/langs/bg.json @@ -21,7 +21,7 @@ "smp-protocol": "СМП Протокол", "chat-protocol": "Чат протокол", "donate": "Дарете", - "copyright-label": "© 2020-2024 SimpleX | Проект с отворен код", + "copyright-label": "© 2020-2025 SimpleX | Проект с отворен код", "simplex-chat-protocol": "SimpleX Чат протокол", "terminal-cli": "Системна конзола", "terms-and-privacy-policy": "Условия и политика за поверителност", diff --git a/website/langs/cs.json b/website/langs/cs.json index 5d7727ba0a..47ab2c99d0 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -14,7 +14,7 @@ "features": "Funkce", "why-simplex": "Proč SimpleX", "simplex-privacy": "SimpleX soukromí", - "simplex-explained": "Simplex vysvětlení", + "simplex-explained": "SimpleX vysvětlení", "simplex-explained-tab-1-text": "1. Co vidí uživatel", "simplex-explained-tab-2-text": "2. Jak to funguje", "simplex-explained-tab-3-text": "3. Co vidí servery", @@ -25,7 +25,7 @@ "smp-protocol": "SMP protokol", "chat-protocol": "Chat protokol", "donate": "Darovat", - "copyright-label": "© 2020-2024 SimpleX | Projekt s otevřeným zdrojovým kódem", + "copyright-label": "© 2020-2025 SimpleX | Projekt s otevřeným zdrojovým kódem", "simplex-chat-protocol": "SimpleX Chat protokol", "terminal-cli": "Terminálové rozhraní příkazového řádku", "terms-and-privacy-policy": "Ochrana soukromí", @@ -115,8 +115,7 @@ "sign-up-to-receive-our-updates": "Přihlaste se k odběru novinek", "enter-your-email-address": "vložte svou e-mailovou adresu", "get-simplex": "Získat SimpleX desktop app", - "why-simplex-is": "Proč je SimpleX", - "unique": "jedinečný", + "why-simplex-is-unique": "Proč je SimpleX jedinečný", "learn-more": "Další informace", "more-info": "Více informací", "hide-info": "Skrýt informace", @@ -180,7 +179,7 @@ "privacy-matters-overlay-card-1-p-2": "Internetoví prodejci vědí, že lidé s nižšími příjmy častěji provádějí urgentní nákupy, takže mohou účtovat vyšší ceny nebo odebírat slevy.", "privacy-matters-overlay-card-1-p-4": "Platforma SimpleX chrání soukromí vašich připojení lépe než jakákoli jiná alternativa a plně zabraňuje tomu, aby byl váš sociální graf dostupný všem společnostem nebo organizacím. I když lidé používají servery poskytované SimpleX Chat, neznáme počet uživatelů ani jejich připojení.", "privacy-matters-overlay-card-2-p-2": "Chcete-li být objektivní a činit nezávislá rozhodnutí, musíte mít svůj informační prostor pod kontrolou. Je to možné pouze v případě, že používáte soukromou komunikační platformu, která nemá přístup k vašemu sociálnímu grafu.", - "simplex-unique-overlay-card-1-p-2": "K doručování zpráv SimpleX používá párové anonymní adresy jednosměrných front zpráv, oddělených pro přijaté a odeslané zprávy, obvykle přes různé servery. Používání SimpleX je jako mít jinou “ vypalovačku” e-mail nebo telefon pro každý kontakt a žádné potíže s jejich správou.", + "simplex-unique-overlay-card-1-p-2": "K doručování zpráv SimpleX používá párové anonymní adresy jednosměrných front zpráv, oddělených pro přijaté a odeslané zprávy, obvykle přes různé servery.", "privacy-matters-overlay-card-3-p-2": "Jedním z nejvíce šokujících příběhů je zkušenost Mohamedoua Oulda Salahiho popsaná v jeho pamětech a zobrazená v Mauritánském filmu. Byl umístěn do tábora na Guantánamu bez soudu a byl tam 15 let mučen po telefonátu svému příbuznému v Afghánistánu pro podezření z účasti na útocích z 11. září, i když předchozích 10 let žil v Německu.", "simplex-unique-overlay-card-1-p-1": "Na rozdíl od jiných platforem pro zasílání zpráv nemá SimpleX žádné identifikátory přiřazené uživatelům. Nespoléhá se na telefonní čísla, adresy založené na doméně (jako je e-mail nebo XMPP), uživatelská jména, veřejné klíče nebo dokonce náhodná čísla k identifikaci svých uživatelů —. Nevíme', kolik lidí používá naše servery SimpleX.", "invitation-hero-header": "Byl vám zaslán odkaz pro připojení na SimpleX Chat", diff --git a/website/langs/de.json b/website/langs/de.json index 3b1e9d34e8..3e5ef9fbc4 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -15,13 +15,13 @@ "simplex-explained-tab-1-p-1": "Sie können Kontakte und Gruppen erstellen und haben Zwei-Wege-Kommunikation wie in jedem anderen Messenger.", "simplex-explained-tab-1-p-2": "Wie funktioniert das mit den unidirektionalen Warteschlangen und ohne Profilkennungen?", "simplex-explained-tab-2-p-1": "Für jede Verbindung nutzen Sie zwei separate Nachrichten-Warteschlangen, um die Nachrichten über verschiedene Server zu senden und zu empfangen.", - "simplex-explained-tab-2-p-2": "Die Server leiten Nachrichten immer nur in eine Richtung weiter, ohne den vollständigen Verlauf der Nutzer-Unterhaltung oder seiner Verbindungen zu kennen.", + "simplex-explained-tab-2-p-2": "Die Server leiten Nachrichten immer nur in eine Richtung weiter, ohne den vollständigen Verlauf der Nutzer-Unterhaltungen oder seiner Verbindungen zu kennen.", "simplex-explained-tab-3-p-1": "Die Server nutzen für jede Warteschlange separate, anonyme Anmeldeinformationen und wissen nicht welchem Nutzer diese gehören.", "simplex-explained-tab-3-p-2": "Durch die Verwendung von Tor-Zugangsservern können Nutzer ihre Metadaten-Privatsphäre weiter verbessern und Korellationen von IP-Adressen verhindern.", "smp-protocol": "SMP-Protokoll", "chat-bot-example": "Beispiel für einen Chatbot", "donate": "Spenden", - "copyright-label": "© 2020-2024 SimpleX | Open-Source-Projekt", + "copyright-label": "© 2020-2025 SimpleX | Open-Source-Projekt", "chat-protocol": "Chat-Protokoll", "simplex-chat-protocol": "SimpleX-Chat-Protokoll", "terminal-cli": "Terminal-Kommandozeilen-Schnittstelle", @@ -54,7 +54,7 @@ "simplex-private-6-title": "Out-of-Band-
Schlüsselaustausch", "simplex-private-9-title": "Unidirektionale
Nachrichten-Warteschlangen", "simplex-private-10-title": "Temporäre, anonyme paarweise Kennungen", - "simplex-private-card-1-point-1": "Double-Ratchet-Protokoll —
Off-the-Record-Nachrichten mit Perfect Forward Secrecy und Einbruchsresistenz.", + "simplex-private-card-1-point-1": "Double-Ratchet-Protokoll —
Off-the-Record-Nachrichten mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung.", "simplex-private-card-1-point-2": "NaCL-Kryptobox in jeder Warteschlange, um eine Korrelation des Datenverkehrs zwischen Nachrichtenwarteschlangen zu verhindern, falls TLS kompromittiert wurde.", "simplex-private-card-3-point-1": "Für Client-Server-Verbindungen wird nur TLS 1.2/1.3 mit starken Algorithmen verwendet.", "simplex-private-card-2-point-1": "Zusätzliche Server-Verschlüsselungs-Schicht für die Zustellung an den Empfänger, um eine Korrelation zwischen empfangenen und gesendeten Server-Daten zu vermeiden, falls TLS kompromittiert wurde.", @@ -64,7 +64,7 @@ "simplex-private-card-4-point-2": "Um SimpleX per Tor zu nutzen, installieren Sie unter Android bitte die Orbot-App und aktivieren Sie den SOCKS5-Proxy oder unter iOS per VPN.", "simplex-private-card-5-point-1": "SimpleX nutzt Inhalte-Auffüllung für jede Verschlüsselungs-Schicht, um Angriffe auf die Nachrichtengröße zu vereiteln.", "simplex-private-card-5-point-2": "Erzeugt Nachrichten mit unterschiedlichen Größen, die für Server und Netzwerk-Beobachter identisch aussehen.", - "simplex-private-card-6-point-1": "Viele Kommunikations-Plattformen sind für MITM-Angriffe durch Server oder Netzwerk-Anbieter anfällig.", + "simplex-private-card-6-point-1": "Viele Kommunikations-Netzwerke sind für MITM-Angriffe durch Server oder Netzwerk-Anbieter anfällig.", "simplex-private-card-9-point-1": "Jede Nachrichten-Warteschlange leitet Nachrichten mit unterschiedlichen Sende- und Empfängeradressen jeweils nur in einer Richtung weiter.", "simplex-private-card-9-point-2": "Verglichen mit traditionellen Nachrichten-Brokern, werden mögliche Angriffs-Vektoren und vorhandene Metadaten reduziert.", "simplex-private-card-10-point-1": "SimpleX nutzt für jeden Nutzer-Kontakt oder jedes Gruppenmitglied eigene temporäre, anonyme und paarweise Adressen und Berechtigungsnachweise.", @@ -95,29 +95,29 @@ "hero-overlay-card-1-p-6": "Lesen Sie mehr darüber im SimpleX-Whitepaper.", "hero-overlay-card-2-p-2": "Sie können diese Informationen mit bestehenden öffentlichen sozialen Netzwerken korrelieren und damit wahre Identitäten herausfinden.", "hero-overlay-card-2-p-3": "Wenn Sie sich mit zwei unterschiedlichen Kontakten über dasselbe Profil unterhalten, können sie, selbst bei sehr auf Privatsphäre bedachten Apps, die Tor-v3-Dienste nutzen, feststellen, dass diese Kontakte mit derselben Person verbunden sind.", - "hero-overlay-card-1-p-2": "Um Nachrichten auszuliefern, nutzt SimpleX statt Benutzerkennungen wie auf allen anderen Plattformen temporäre, anonyme und paarweise Kennungen für Nachrichten-Warteschlangen, die für jede Ihrer Verbindungen unterschiedlich sind — Es gibt keinerlei Langzeit-Kennungen.", + "hero-overlay-card-1-p-2": "Um Nachrichten auszuliefern, nutzt SimpleX statt Benutzerkennungen wie alle anderen Netzwerke nur temporäre, anonyme und paarweise Kennungen für Nachrichten-Warteschlangen, die für jede Ihrer Verbindungen unterschiedlich sind — es gibt keinerlei Langzeit-Kennungen.", "hero-overlay-card-1-p-4": "Dieses Design verhindert schon auf der Applikations-Ebene Datenlecks für jegliche Benutzer'Metadaten. Sie können sich über Tor mit Nachrichten-Servern verbinden, um Ihre Privatsphäre weiter zu verbessern und die von Ihnen genutzte IP-Adresse zu schützen.", "hero-overlay-card-2-p-1": "Wenn Nutzer dauerhafte Identitäten besitzen, selbst wenn diese eine Zufallsnummer, wie eine Sitzungs-ID, ist, besteht ein Risiko, das Provider oder Angreifer feststellen können, wie Nutzer miteinander verbunden sind und wie viele Nachrichten sie versenden.", "hero-overlay-card-2-p-4": "SimpleX schützt gegen solche Angriffe, weil es vom Design her keinerlei Benutzerkennungen besitzt. Und Sie haben sogar unterschiedliche Anzeigenamen für jeden Kontakt und vermeiden jegliche geteilte Daten zwischen diesen, wenn Sie den Inkognito-Modus nutzen.", "simplex-network-overlay-card-1-p-1": "Peer-to-Peer-Nachrichten-Protokolle und -Applikationen haben verschiedene Probleme, die diese weniger vertrauenswürdig, die Analyse wesentlich komplexer und anfälliger gegen verschiedene Arten von Angriffen, als bei SimpleX machen.", "simplex-network-overlay-card-1-li-2": "Das SimpleX Design hat, im Gegensatz zu den meisten P2P-Netzwerken, keinerlei globalen Benutzerkennungen, auch keine temporären. Es nutzt ausschließlich temporäre paarweise Kennungen, die bessere Anonymität und Metadaten-Schutz bieten.", - "simplex-network-overlay-card-1-li-4": "P2P-Implementierungen können durch Internetanbieter blockiert werden, wie beispielweise BitTorrent). SimpleX ist transportunabhängig – es kann über Standard-Web-Protokolle, wie beispielsweise WebSockets, arbeiten.", + "simplex-network-overlay-card-1-li-4": "——P2P-Implementierungen können durch Internetanbieter blockiert werden, wie beispielweise BitTorrent). SimpleX ist transportunabhängig — es kann über Standard-Web-Protokolle, wie beispielsweise WebSockets, arbeiten.", "simplex-network-overlay-card-1-li-6": "P2P-Netzwerke können anfällig für DRDoS-Angriffe sein, wenn die Clients den Datenverkehr erneut senden und verstärken können, was zu einem netzwerkweiten Denial-of-Service führt. SimpleX-Clients leiten nur Datenverkehr von bekannten Verbindungen weiter und können von einem Angreifer nicht dazu verwendet werden, den Datenverkehr im gesamten Netzwerk zu verstärken.", "privacy-matters-overlay-card-1-p-1": "Viele große Unternehmen nutzen Informationen, mit wem Sie in Verbindung stehen, um Ihr Einkommen zu schätzen, Ihnen Produkte zu verkaufen, die Sie nicht wirklich benötigen und um die Preise zu bestimmen.", "privacy-matters-overlay-card-1-p-2": "Online-Händler wissen, dass Menschen mit geringerem Einkommen eher dringende Einkäufe tätigen, sodass sie möglicherweise höhere Preise verlangen können oder Rabatte streichen.", "privacy-matters-overlay-card-1-p-3": "Einige Finanz- und Versicherungsunternehmen verwenden soziale Graphen, um Zinssätze und Prämien zu ermitteln. Menschen mit niedrigerem Einkommen zahlen so häufig mehr — dies ist als \"Armutsprämie\" bekannt.", - "privacy-matters-overlay-card-2-p-2": "Um objektiv zu sein und unabhängige Entscheidungen treffen zu können, müssen Sie die Kontrolle über Ihren Informationsraum haben. Dies ist nur möglich, wenn Sie eine private Kommunikationsplattform verwenden, die keinen Zugriff auf Ihren sozialen Graphen hat.", - "privacy-matters-overlay-card-2-p-3": "SimpleX ist die erste Plattform, die per Design keinerlei Benutzerkennungen hat und auf diese Weise Ihren Verbindungsgraphen besser schützt als jede andere bekannte Alternative.", + "privacy-matters-overlay-card-2-p-2": "Um objektiv zu sein und unabhängige Entscheidungen treffen zu können, müssen Sie die Kontrolle über Ihren Informationsraum haben. Dies ist nur möglich, wenn Sie ein privates Kommunikations-Netzwerk verwenden, welches keinen Zugriff auf Ihren sozialen Graphen hat.", + "privacy-matters-overlay-card-2-p-3": "SimpleX ist das erste Netzwerk, welches per Design keinerlei Benutzerkennungen hat und auf diese Weise Ihren Verbindungsgraphen besser schützt als jede andere bekannte Alternative.", "privacy-matters-overlay-card-3-p-1": "Jede Person sollte sich um ihre Privatsphäre und die Sicherheit ihrer Kommunikation kümmern — Harmlose Gespräche könnten Sie in Gefahr bringen, selbst wenn Sie nichts zu verbergen haben.", "privacy-matters-overlay-card-3-p-4": "Es reicht nicht aus, einfach einen Ende-zu-Ende-verschlüsselten Messenger zu verwenden. Wir alle sollten den Messenger verwenden, der die Privatsphäre unserer persönlichen Netzwerke schützt, mit welchen wir verbunden sind.", - "simplex-unique-overlay-card-1-p-3": "Dieses Design schützt die Privatsphäre von Ihnen und der Personen, mit denen Sie kommunizieren, und verbirgt die Verbindungen vor den SimpleX-Plattform-Servern und möglichen Beobachtern. Um Ihre IP-Adresse vor den Servern zu verbergen, können Sie sich per Tor mit den SimpleX-Servern verbinden.", - "simplex-unique-overlay-card-2-p-1": "Da Sie auf der SimpleX-Plattform keine Kennungen haben, kann Sie niemand kontaktieren, es sei denn, Sie geben eine einmalige oder vorübergehende Benutzeradresse in Form eines QR-Codes oder eines Links weiter.", + "simplex-unique-overlay-card-1-p-3": "Dieses Design schützt die Privatsphäre von Ihnen und der Nutzer, mit denen Sie kommunizieren, und verbirgt die Verbindungen vor den SimpleX-Netzwerk-Servern und möglichen Beobachtern. Um Ihre IP-Adresse vor den Servern zu verbergen, können Sie sich per Tor mit den SimpleX-Servern verbinden.", + "simplex-unique-overlay-card-2-p-1": "Da Sie im SimpleX-Netzwerk keine Kennungen haben, kann Sie niemand kontaktieren, es sei denn, Sie geben eine einmalige oder vorübergehende Benutzeradresse in Form eines QR-Codes oder eines Links weiter.", "simplex-unique-overlay-card-3-p-2": "Die Ende-zu-Ende-verschlüsselten Nachrichten werden vorübergehend auf SimpleX-Relay-Servern gespeichert, bis sie vom Endgerät empfangen und danach endgültig gelöscht werden.", "simplex-unique-overlay-card-3-p-3": "Im Gegensatz zu föderierten Netzwerkservern (wie z.B. Mail, XMPP oder Matrix) speichern die SimpleX-Server keine Benutzerkonten, sondern leiten Nachrichten nur weiter, so dass die Privatsphäre der beteiligten Parteien geschützt ist.", - "simplex-unique-overlay-card-4-p-1": "Sie können SimpleX mit Ihren eigenen Servern verwenden und trotzdem mit Personen kommunizieren, welche die von uns bereitgestellten und vorkonfigurierten Server verwenden.", - "simplex-unique-card-1-p-1": "SimpleX schützt die Privatsphäre Ihres Profils, Ihrer Kontakte und Metadaten und verbirgt sie vor den SimpleX-Plattform-Servern und allen Beobachtern.", - "simplex-unique-card-1-p-2": "Im Gegensatz zu allen anderen bestehenden Messaging-Plattformen werden den Nutzern von SimpleX keine Kennungen zugewiesen — nicht einmal Zufallszahlen.", - "simplex-unique-card-2-p-1": "Da Sie keine Kennung oder feste Adresse auf der SimpleX-Plattform haben, kann Sie niemand kontaktieren, es sei denn, Sie geben eine einmalige oder vorübergehende Benutzeradresse in Form eines QR-Codes oder Links weiter.", + "simplex-unique-overlay-card-4-p-1": "Sie können SimpleX mit Ihren eigenen Servern verwenden und trotzdem mit Nutzern kommunizieren, welche die vorkonfigurierten Server der App verwenden.", + "simplex-unique-card-1-p-1": "SimpleX schützt die Privatsphäre Ihres Profils, Ihrer Kontakte und Metadaten und verbirgt sie vor den SimpleX-Netzwerk-Servern und weiteren möglichen Beobachtern.", + "simplex-unique-card-1-p-2": "Im Gegensatz zu allen anderen bestehenden Messaging-Netzwerken werden den Nutzern von SimpleX keine Kennungen zugewiesen — nicht einmal Zufallszahlen.", + "simplex-unique-card-2-p-1": "Da Sie keine Kennung oder feste Adresse im SimpleX-Netzwerk haben, kann Sie niemand kontaktieren, es sei denn, Sie geben eine einmalige oder vorübergehende Benutzeradresse in Form eines QR-Codes oder Links weiter.", "simplex-unique-card-3-p-1": "SimpleX speichert Benutzerdaten nur auf den Endgeräten und das in einem portablen, verschlüsselten Datenbankformat — welches auf ein anderes Gerät übertragen werden kann.", "simplex-unique-card-3-p-2": "Die Ende-zu-Ende-verschlüsselten Nachrichten werden vorübergehend auf SimpleX-Relay-Servern gespeichert, bis sie vom Endgerät empfangen und danach endgültig gelöscht werden.", "join": "Nutzen Sie", @@ -142,7 +142,7 @@ "use-this-command": "Benutzen Sie dieses Kommando:", "see-simplex-chat": "Siehe SimpleX-Chat", "github-repository": "GitHub-Repository", - "the-instructions--source-code": "Die Anleitung, wie Sie es herunterladen und aus dem Quellcode kompilieren.", + "the-instructions--source-code": "Für die Anleitungen, wie Sie es herunterladen oder aus dem Quellcode kompilieren.", "if-you-already-installed": "Wenn Sie es schon installiert haben", "simplex-chat-for-the-terminal": "SimpleX Chat für das Terminal", "privacy-matters-section-header": "Warum es auf Privatsphäre ankommt", @@ -171,15 +171,14 @@ "comparison-section-list-point-2": "DNS-basierte Adressen", "comparison-section-list-point-3": "Öffentlicher Schlüssel oder eine andere weltweit eindeutige ID", "comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden. In Signal und weiteren Apps kann der Sicherheitscode überprüft werden, um dies zu entschärfen", - "comparison-section-list-point-6": "P2P sind zwar verteilt, aber nicht föderiert - sie arbeiten als ein einziges Netzwerk", + "comparison-section-list-point-6": "P2P sind zwar verteilt, aber nicht föderiert — sie arbeiten als ein einziges Netzwerk", "comparison-section-list-point-7": "P2P-Netzwerke haben entweder eine zentrale Verwaltung oder das gesamte Netzwerk kann kompromittiert werden", "see-here": "Siehe hier", - "simplex-unique-card-4-p-1": "Das SimpleX-Netzwerk ist vollständig dezentralisiert und unabhängig von Kryptowährungen oder anderen Plattformen außer dem Internet.", - "unique": "einmalig", + "simplex-unique-card-4-p-1": "Das SimpleX-Netzwerk ist vollständig dezentralisiert und unabhängig von Kryptowährungen oder anderen Netzwerken außer dem Internet.", "privacy-matters-overlay-card-2-p-1": "Vor nicht allzu langer Zeit beobachteten wir, wie große Wahlen von einem angesehenen Beratungsunternehmen manipuliert wurden, welches unsere sozialen Graphen nutzte, um unsere Sicht auf die reale Welt zu verzerren und unsere Stimmen zu manipulieren.", "privacy-matters-overlay-card-3-p-2": "Eine der schockierendsten Geschichten ist die Erfahrung von Mohamedou Ould Salahi, welche in seinen Memoiren beschrieben und im Film „The Mauritanian“ gezeigt wird. Er kam nach einem Anruf bei seinen Verwandten in Afghanistan und ohne Gerichtsverfahren in das Guantanamo-Lager und wurde dort einige Jahre lang gefoltert, weil er verdächtigt wurde, an den 9/11-Angriffen beteiligt gewesen zu sein, obwohl er die vorhergehenden 10 Jahre in Deutschland gelebt hatte.", - "simplex-unique-overlay-card-1-p-1": "Im Gegensatz zu anderen Nachrichten-Plattformen weist SimpleX den Benutzern keine Kennungen zu. Es verlässt sich nicht auf Telefonnummern, domänenbasierte Adressen (wie E-Mail oder XMPP), Benutzernamen, öffentliche Schlüssel oder sogar Zufallszahlen, um seine Benutzer zu identifizieren — Wir wissen nicht, wie viele Personen unsere SimpleX-Server verwenden.", - "simplex-unique-overlay-card-1-p-2": "Um Nachrichten auszuliefern nutzt SimpleX paarweise anonyme Adressen aus unidirektionalen Nachrichten-Warteschlangen, die für empfangene und gesendete Nachrichten separiert sind und gewöhnlich über verschiedene Server gesendet werden. Die Nutzung von SimpleX entspricht der Nutzung von unterschiedlichen Mailservern oder Telefonen für jeden einzelnen Kontakt und vermeidet dabei eine mühsame Verwaltung.", + "simplex-unique-overlay-card-1-p-1": "Im Gegensatz zu anderen Nachrichten-Netzwerken weist SimpleX den Benutzern keine Kennungen zu. Es verlässt sich nicht auf Telefonnummern, domänenbasierte Adressen (wie E-Mail oder XMPP), Benutzernamen, öffentliche Schlüssel oder Zufallszahlen, um seine Benutzer zu identifizieren — selbst SimpleX-Server-Betreiber wissen nicht, wie viele Personen deren Server verwenden.", + "simplex-unique-overlay-card-1-p-2": "Um Nachrichten auszuliefern nutzt SimpleX paarweise anonyme Adressen aus unidirektionalen Nachrichten-Warteschlangen, die für empfangene und gesendete Nachrichten separiert sind und gewöhnlich über verschiedene Server gesendet werden.", "simplex-network-overlay-card-1-li-5": "Alle bekannten P2P-Netzwerke können anfällig für Sybil-Angriffe sein, da jeder Knoten ermittelbar ist und das Netzwerk als Ganzes funktioniert. Bekannte Maßnahmen zur Verhinderung erfordern entweder eine zentralisierte Komponente oder einen teuren Ausführungsnachweis. Das SimpleX-Netzwerk bietet keine Ermittlung der Server, ist fragmentiert und arbeitet mit mehreren isolierten Subnetzwerken, wodurch netzwerkweite Angriffe unmöglich werden.", "simplex-network-overlay-card-1-li-3": "P2P löst nicht das Problem des MITM-Angriffs und die meisten bestehenden Implementierungen nutzen für den initialen Schlüsselaustausch keine Out-of-Band-Nachrichten. Im Gegensatz hierzu nutzt SimpleX für den initialen Schlüsselaustausch Out-of-Band-Nachrichten oder zum Teil schon bestehende sichere und vertrauenswürdige Verbindungen.", "tap-the-connect-button-in-the-app": "Drücken Sie die „Verbinden“-Taste in der Applikation", @@ -197,16 +196,16 @@ "comparison-section-list-point-5": "Die Privatsphäre-Metadaten des Nutzers werden nicht geschützt", "simplex-network-overlay-card-1-li-1": "P2P-Netzwerke vertrauen auf Varianten von DHT, um Nachrichten zu routen. DHT-Designs müssen zwischen Zustellungsgarantie und Latenz ausgleichen. Verglichen mit P2P bietet SimpleX sowohl eine bessere Zustellungsgarantie als auch eine niedrigere Latenz, weil eine Nachricht redundant und parallel über mehrere Server gesendet werden kann, wobei die durch den Empfänger ausgewählten Server genutzt werden. In P2P-Netzwerken werden Nachrichten sequentiell über O(log N)-Knoten gesendet, wobei die Knoten durch einen Algorithmus ausgewählt werden.", "simplex-unique-overlay-card-3-p-4": "Zwischen dem gesendeten und empfangenen Serververkehr gibt es keine gemeinsamen Kennungen oder Chiffriertexte — sodass ein Beobachter nicht ohne weiteres feststellen kann, wer mit wem kommuniziert, selbst wenn TLS kompromittiert wurde.", - "simplex-unique-overlay-card-4-p-3": "Wenn Sie darüber nachdenken, für die SimpleX-Plattform entwickeln zu wollen, z.B. einen Chatbot für SimpleX-App-Nutzer oder die Integration der SimpleX-Chat-Bibliothek in Ihre mobilen Apps, kontaktieren Sie uns bitte für eine weitere Beratung und Unterstützung.", - "privacy-matters-overlay-card-1-p-4": "Die SimpleX-Plattform schützt die Privatsphäre Ihrer Verbindungen besser als jede Alternative und verhindert vollständig, dass Ihr sozialer Graph für Unternehmen oder Organisationen verfügbar wird. Selbst wenn die Anwender SimpleX-Chat-Server verwenden, kennen wir die Anzahl der Benutzer oder ihre Verbindungen nicht.", + "simplex-unique-overlay-card-4-p-3": "Falls Sie Interesse daran haben, aktiv bei der Entwicklung des SimpleX-Netzwerks mitzuhelfen, z.B. einen Chatbot für SimpleX-App-Nutzer zu entwickeln oder die Integration der SimpleX-Chat-Bibliothek in mobile Apps voranzutreiben, kontaktieren Sie uns bitte für eine weitere Beratung und Unterstützung.", + "privacy-matters-overlay-card-1-p-4": "Das SimpleX-Netzwerk schützt die Privatsphäre Ihrer Verbindungen besser als jede Alternative und verhindert vollständig, dass Ihr sozialer Graph für Unternehmen oder Organisationen verfügbar wird. Selbst wenn Anwender die in der SimpleX-Chat-App vorkonfigurierten Server verwenden, kennen die Server-Betreiber die Anzahl der Benutzer oder deren Verbindungen nicht.", "contact-hero-header": "Sie haben eine Adresse zur Verbindung mit SimpleX Chat erhalten", "invitation-hero-header": "Sie haben einen Einmal-Link zur Verbindung mit SimpleX Chat erhalten", "privacy-matters-overlay-card-3-p-3": "Normale Menschen werden für das, was sie online teilen, sogar unter Nutzung ihrer „anonymen“ Konten, selbst in demokratischen Ländern verhaftet.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat speichert alle Benutzerdaten ausschließlich auf den Endgeräten in einem portablen und verschlüsselten Datenbankformat, welches exportiert und auf jedes unterstützte Gerät übertragen werden kann.", "simplex-unique-overlay-card-2-p-2": "Auch wenn die optionale Benutzeradresse zum Versenden von Spam-Kontaktanfragen verwendet werden kann, können Sie sie ändern oder ganz löschen, ohne dass Ihre Verbindungen verloren gehen.", - "simplex-unique-overlay-card-4-p-2": "Die SimpleX-Plattform verwendet ein offenes Protokoll und bietet ein SDK zur Erstellung von Chatbots an, welches die Erstellung von Diensten ermöglicht, mit denen Nutzer über SimpleX-Chat interagieren können — Wir sind gespannt, welche SimpleX-Dienste Sie erstellen können.", + "simplex-unique-overlay-card-4-p-2": "Das SimpleX-Netzwerk verwendet ein offenes Protokoll und bietet ein SDK zur Erstellung von Chatbots an. Dies ermöglicht die Erstellung von Diensten, mit denen Nutzer über SimpleX-Chat-Apps interagieren können — wir sind gespannt, welche SimpleX-Dienste Sie entwickeln werden.", "simplex-unique-card-4-p-2": "Sie können SimpleX mit Ihren eigenen Servern oder mit den von uns zur Verfügung gestellten Servern verwenden — und sich trotzdem mit jedem Benutzer verbinden.", - "why-simplex-is": "Warum ist SimpleX", + "why-simplex-is-unique": "Warum ist SimpleX einmalig", "contact-hero-p-1": "Die öffentlichen Schlüssel und die Adresse der Nachrichtenwarteschlange in diesem Link werden NICHT über das Netzwerk gesendet, wenn Sie diese Seite aufrufen — sie sind in dem Hash-Fragment der Link-URL enthalten.", "if-you-already-installed-simplex-chat-for-the-terminal": "Wenn Sie SimpleX schon für das Terminal installiert haben", "simplex-network-3-desc": "Die Server stellen unidirektionale Warteschlangen zur Verfügung, um die Benutzer miteinander zu verbinden. Sie haben aber keinen Einblick in den Verbindungs-Graphen des Netzwerks — Diesen haben nur die Benutzer selbst.", @@ -219,7 +218,7 @@ "guide-dropdown-8": "App-Einstellungen", "guide-dropdown-9": "Verbindungen herstellen", "guide": "Leitfaden", - "docs-dropdown-1": "SimpleX-Plattform", + "docs-dropdown-1": "SimpleX-Netzwerk", "docs-dropdown-2": "Zugriff auf Android-Dateien", "docs-dropdown-3": "Zugriff auf die Chat-Datenbank", "docs-dropdown-4": "Den SMP-Server hosten", diff --git a/website/langs/en.json b/website/langs/en.json index e57b3375de..f9691e2594 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -7,21 +7,21 @@ "why-simplex": "Why SimpleX", "simplex-privacy": "SimpleX privacy", "simplex-network": "SimpleX network", - "simplex-explained": "Simplex explained", + "simplex-explained": "SimpleX explained", "simplex-explained-tab-1-text": "1. What users experience", "simplex-explained-tab-2-text": "2. How does it work", "simplex-explained-tab-3-text": "3. What servers see", "simplex-explained-tab-1-p-1": "You can create contacts and groups, and have two-way conversations, as in any other messenger.", "simplex-explained-tab-1-p-2": "How can it work with unidirectional queues and without user profile identifiers?", "simplex-explained-tab-2-p-1": "For each connection you use two separate messaging queues to send and receive messages via different servers.", - "simplex-explained-tab-2-p-2": "Servers only pass messages one way, without having the full picture of user's conversation or connections.", + "simplex-explained-tab-2-p-2": "Servers only pass messages one way, without having the full picture of user's conversations or connections.", "simplex-explained-tab-3-p-1": "The servers have separate anonymous credentials for each queue, and do not know which users they belong to.", "simplex-explained-tab-3-p-2": "Users can further improve metadata privacy by using Tor to access servers, preventing corellation by IP address.", "chat-bot-example": "Chat bot example", "smp-protocol": "SMP protocol", "chat-protocol": "Chat protocol", "donate": "Donate", - "copyright-label": "© 2020-2024 SimpleX | Open-Source Project", + "copyright-label": "© 2020-2025 SimpleX | Open-Source Project", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Privacy Policy", @@ -65,7 +65,7 @@ "simplex-private-card-4-point-2": "To use SimpleX via Tor please install Orbot app and enable SOCKS5 proxy (or VPN on iOS).", "simplex-private-card-5-point-1": "SimpleX uses content padding for each encryption layer to frustrate message size attacks.", "simplex-private-card-5-point-2": "It makes messages of different sizes look the same to the servers and network observers.", - "simplex-private-card-6-point-1": "Many communication platforms are vulnerable to MITM attacks by servers or network providers.", + "simplex-private-card-6-point-1": "Many communication networks are vulnerable to MITM attacks by servers or network providers.", "simplex-private-card-6-point-2": "To prevent it SimpleX apps pass one-time keys out-of-band, when you share an address as a link or a QR code.", "simplex-private-card-7-point-1": "To guarantee integrity the messages are sequentially numbered and include the hash of the previous message.", "simplex-private-card-7-point-2": "If any message is added, removed or changed the recipient will be alerted.", @@ -92,7 +92,7 @@ "simplex-unique-4-title": "You own SimpleX network", "simplex-unique-4-overlay-1-title": "Fully decentralised — users own the SimpleX network", "hero-overlay-card-1-p-1": "Many users asked: if SimpleX has no user identifiers, how can it know where to deliver messages?", - "hero-overlay-card-1-p-2": "To deliver messages, instead of user IDs used by all other platforms, SimpleX uses temporary anonymous pairwise identifiers of message queues, separate for each of your connections — there are no long term identifiers.", + "hero-overlay-card-1-p-2": "To deliver messages, instead of user IDs used by all other networks, SimpleX uses temporary anonymous pairwise identifiers of message queues, separate for each of your connections — there are no long term identifiers.", "hero-overlay-card-1-p-3": "You define which server(s) to use to receive the messages, your contacts — the servers you use to send the messages to them. Every conversation is likely to use two different servers.", "hero-overlay-card-1-p-4": "This design prevents leaking any users' metadata on the application level. To further improve privacy and protect your IP address you can connect to messaging servers via Tor.", "hero-overlay-card-1-p-5": "Only client devices store user profiles, contacts and groups; the messages are sent with 2-layer end-to-end encryption.", @@ -108,38 +108,38 @@ "simplex-network-overlay-card-1-li-1": "P2P networks rely on some variant of DHT to route messages. DHT designs have to balance delivery guarantee and latency. SimpleX has both better delivery guarantee and lower latency than P2P, because the message can be redundantly passed via several servers in parallel, using the servers chosen by the recipient. In P2P networks the message is passed through O(log N) nodes sequentially, using nodes chosen by the algorithm.", "simplex-network-overlay-card-1-li-2": "SimpleX design, unlike most P2P networks, has no global user identifiers of any kind, even temporary, and only uses temporary pairwise identifiers, providing better anonymity and metadata protection.", "simplex-network-overlay-card-1-li-3": "P2P does not solve MITM attack problem, and most existing implementations do not use out-of-band messages for the initial key exchange. SimpleX uses out-of-band messages or, in some cases, pre-existing secure and trusted connections for the initial key exchange.", - "simplex-network-overlay-card-1-li-4": "P2P implementations can be blocked by some Internet providers (like BitTorrent). SimpleX is transport agnostic - it can work over standard web protocols, e.g. WebSockets.", + "simplex-network-overlay-card-1-li-4": "P2P implementations can be blocked by some Internet providers (like BitTorrent). SimpleX is transport agnostic — it can work over standard web protocols, e.g. WebSockets.", "simplex-network-overlay-card-1-li-5": "All known P2P networks may be vulnerable to Sybil attack, because each node is discoverable, and the network operates as a whole. Known measures to mitigate it require either a centralized component or expensive proof of work. SimpleX network has no server discoverability, it is fragmented and operates as multiple isolated sub-networks, making network-wide attacks impossible.", "simplex-network-overlay-card-1-li-6": "P2P networks may be vulnerable to DRDoS attack, when the clients can rebroadcast and amplify traffic, resulting in network-wide denial of service. SimpleX clients only relay traffic from known connection and cannot be used by an attacker to amplify the traffic in the whole network.", "privacy-matters-overlay-card-1-p-1": "Many large companies use information about who you are connected with to estimate your income, sell you the products you don't really need, and to determine the prices.", "privacy-matters-overlay-card-1-p-2": "Online retailers know that people with lower incomes are more likely to make urgent purchases, so they may charge higher prices or remove discounts.", "privacy-matters-overlay-card-1-p-3": "Some financial and insurance companies use social graphs to determine interest rates and premiums. It often makes people with lower incomes pay more — it is known as 'poverty premium'.", - "privacy-matters-overlay-card-1-p-4": "SimpleX platform protects the privacy of your connections better than any alternative, fully preventing your social graph becoming available to any companies or organizations. Even when people use servers provided by SimpleX Chat, we do not know the number of users or their connections.", + "privacy-matters-overlay-card-1-p-4": "SimpleX network protects the privacy of your connections better than any alternative, fully preventing your social graph becoming available to any companies or organizations. Even when people use servers preconfigured in SimpleX Chat apps, server operators do not know the number of users or their connections.", "privacy-matters-overlay-card-2-p-1": "Not so long ago we observed the major elections being manipulated by a reputable consulting company that used our social graphs to distort our view of the real world and manipulate our votes.", - "privacy-matters-overlay-card-2-p-2": "To be objective and to make independent decisions you need to be in control of your information space. It is only possible if you use private communication platform that does not have access to your social graph.", - "privacy-matters-overlay-card-2-p-3": "SimpleX is the first platform that doesn't have any user identifiers by design, in this way protecting your connections graph better than any known alternative.", + "privacy-matters-overlay-card-2-p-2": "To be objective and to make independent decisions you need to be in control of your information space. It is only possible if you use private communication network that does not have access to your social graph.", + "privacy-matters-overlay-card-2-p-3": "SimpleX is the first network that doesn't have any user identifiers by design, in this way protecting your connections graph better than any known alternative.", "privacy-matters-overlay-card-3-p-1": "Everyone should care about privacy and security of their communications — harmless conversations can put you in danger, even if you have nothing to hide.", "privacy-matters-overlay-card-3-p-2": "One of the most shocking stories is the experience of Mohamedou Ould Salahi described in his memoir and shown in The Mauritanian movie. He was put into Guantanamo camp, without trial, and was tortured there for 15 years after a phone call to his relative in Afghanistan, under suspicion of being involved in 9/11 attacks, even though he lived in Germany for the previous 10 years.", "privacy-matters-overlay-card-3-p-3": "Ordinary people get arrested for what they share online, even via their 'anonymous' accounts, even in democratic countries.", "privacy-matters-overlay-card-3-p-4": "It is not enough to use an end-to-end encrypted messenger, we all should use the messengers that protect the privacy of our personal networks — who we are connected with.", - "simplex-unique-overlay-card-1-p-1": "Unlike other messaging platforms, SimpleX has no identifiers assigned to the users. It does not rely on phone numbers, domain-based addresses (like email or XMPP), usernames, public keys or even random numbers to identify its users — we don't know how many people use our SimpleX servers.", - "simplex-unique-overlay-card-1-p-2": "To deliver messages SimpleX uses pairwise anonymous addresses of unidirectional message queues, separate for received and sent messages, usually via different servers. Using SimpleX is like having a different “burner” email or phone for each contact, and no hassle to manage them.", - "simplex-unique-overlay-card-1-p-3": "This design protects the privacy of who you are communicating with, hiding it from SimpleX platform servers and from any observers. To hide your IP address from the servers, you can connect to SimpleX servers via Tor.", - "simplex-unique-overlay-card-2-p-1": "Because you have no identifier on the SimpleX platform, nobody can contact you unless you share a one-time or temporary user address, as a QR code or a link.", + "simplex-unique-overlay-card-1-p-1": "Unlike other messaging networks, SimpleX has no identifiers assigned to the users. It does not rely on phone numbers, domain-based addresses (like email or XMPP), usernames, public keys or even random numbers to identify its users — SimpleX server operators don't know how many people use their servers.", + "simplex-unique-overlay-card-1-p-2": "To deliver messages SimpleX uses pairwise anonymous addresses of unidirectional message queues, separate for received and sent messages, usually via different servers.", + "simplex-unique-overlay-card-1-p-3": "This design protects the privacy of who you are communicating with, hiding it from SimpleX network servers and from any observers. To hide your IP address from the servers, you can connect to SimpleX servers via Tor.", + "simplex-unique-overlay-card-2-p-1": "Because you have no identifier on the SimpleX network, nobody can contact you unless you share a one-time or temporary user address, as a QR code or a link.", "simplex-unique-overlay-card-2-p-2": "Even with the optional user address, while it can be used to send spam contact requests, you can change or completely delete it without losing any of your connections.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat stores all user data only on client devices using a portable encrypted database format that can be exported and transferred to any supported device.", "simplex-unique-overlay-card-3-p-2": "The end-to-end encrypted messages are held temporarily on SimpleX relay servers until received, then they are permanently deleted.", "simplex-unique-overlay-card-3-p-3": "Unlike federated networks servers (email, XMPP or Matrix), SimpleX servers don't store user accounts, they only relay messages, protecting the privacy of both parties.", "simplex-unique-overlay-card-3-p-4": "There are no identifiers or ciphertext in common between sent and received server traffic — if anybody is observing it, they cannot easily determine who communicates with whom, even if TLS is compromised.", - "simplex-unique-overlay-card-4-p-1": "You can use SimpleX with your own servers and still communicate with people who use the pre-configured servers provided by us.", - "simplex-unique-overlay-card-4-p-2": "SimpleX platform uses an open protocol and provides SDK to create chat bots, allowing implementation of services that users can interact with via SimpleX Chat apps — we're really looking forward to see what SimpleX services you can build.", - "simplex-unique-overlay-card-4-p-3": "If you are considering developing for the SimpleX platform, for example, the chat bot for SimpleX app users, or the integration of the SimpleX Chat library into your mobile apps, please get in touch for any advice and support.", - "simplex-unique-card-1-p-1": "SimpleX protects the privacy of your profile, contacts and metadata, hiding it from SimpleX platform servers and any observers.", - "simplex-unique-card-1-p-2": "Unlike any other existing messaging platform, SimpleX has no identifiers assigned to the users — not even random numbers.", - "simplex-unique-card-2-p-1": "Because you have no identifier or fixed address on the SimpleX platform, nobody can contact you unless you share a one-time or temporary user address, as a QR code or a link.", + "simplex-unique-overlay-card-4-p-1": "You can use SimpleX with your own servers and still communicate with people who use the servers preconfigured in the apps.", + "simplex-unique-overlay-card-4-p-2": "SimpleX network uses an open protocol and provides SDK to create chat bots, allowing implementation of services that users can interact with via SimpleX Chat apps — we're really looking forward to see what SimpleX services you will build.", + "simplex-unique-overlay-card-4-p-3": "If you are considering developing for the SimpleX network, for example, the chat bot for SimpleX app users, or the integration of the SimpleX Chat library into your mobile apps, please get in touch for any advice and support.", + "simplex-unique-card-1-p-1": "SimpleX protects the privacy of your profile, contacts and metadata, hiding it from SimpleX network servers and any observers.", + "simplex-unique-card-1-p-2": "Unlike any other existing messaging network, SimpleX has no identifiers assigned to the users — not even random numbers.", + "simplex-unique-card-2-p-1": "Because you have no identifier or fixed address on the SimpleX network, nobody can contact you unless you share a one-time or temporary user address, as a QR code or a link.", "simplex-unique-card-3-p-1": "SimpleX stores all user data on client devices in a portable encrypted database format — it can be transferred to another device.", "simplex-unique-card-3-p-2": "The end-to-end encrypted messages are held temporarily on SimpleX relay servers until received, then they are permanently deleted.", - "simplex-unique-card-4-p-1": "The SimpleX network is fully decentralised and independent of any crypto-currency or any other platform, other than the Internet.", + "simplex-unique-card-4-p-1": "The SimpleX network is fully decentralised and independent of any crypto-currency or any other network, other than the Internet.", "simplex-unique-card-4-p-2": "You can use SimpleX with your own servers or with the servers provided by us — and still connect to any user.", "join": "Join", "we-invite-you-to-join-the-conversation": "We invite you to join the conversation", @@ -149,8 +149,7 @@ "sign-up-to-receive-our-updates": "Sign up to receive our updates", "enter-your-email-address": "Enter your email address", "get-simplex": "Get SimpleX desktop app", - "why-simplex-is": "Why SimpleX is", - "unique": "unique", + "why-simplex-is-unique": "Why SimpleX is unique", "learn-more": "Learn more", "more-info": "More info", "hide-info": "Hide info", @@ -172,7 +171,7 @@ "use-this-command": "Use this command:", "see-simplex-chat": "See SimpleX Chat", "github-repository": "GitHub repository", - "the-instructions--source-code": "the instructions how to download or compile it from the source code.", + "the-instructions--source-code": "for the instructions how to download or compile it from the source code.", "if-you-already-installed-simplex-chat-for-the-terminal": "If you already installed SimpleX Chat for the terminal", "if-you-already-installed": "If you already installed", "simplex-chat-for-the-terminal": "SimpleX Chat for the terminal", @@ -213,7 +212,7 @@ "comparison-section-list-point-4a": "SimpleX relays cannot compromise e2e encryption. Verify security code to mitigate attack on out-of-band channel", "comparison-section-list-point-4": "If operator’s servers are compromised. Verify security code in Signal and some other apps to mitigate it", "comparison-section-list-point-5": "Does not protect users' metadata privacy", - "comparison-section-list-point-6": "While P2P are distributed, they are not federated - they operate as a single network", + "comparison-section-list-point-6": "While P2P are distributed, they are not federated — they operate as a single network", "comparison-section-list-point-7": "P2P networks either have a central authority or the whole network can be compromised", "see-here": "see here", "guide-dropdown-1": "Quick start", @@ -226,7 +225,7 @@ "guide-dropdown-8": "App settings", "guide-dropdown-9": "Making connections", "guide": "Guide", - "docs-dropdown-1": "SimpleX platform", + "docs-dropdown-1": "SimpleX network", "docs-dropdown-2": "Accessing Android files", "docs-dropdown-3": "Accessing chat database", "docs-dropdown-4": "Host SMP Server", diff --git a/website/langs/es.json b/website/langs/es.json index b88a592ac4..b3cfc83969 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -10,7 +10,7 @@ "simplex-explained-tab-3-p-2": "El usuario puede mejorar aún más la privacidad de sus metadatos haciendo uso de la red Tor para acceder a los servidores, evitando así la correlación por dirección IP.", "smp-protocol": "Protocolo SMP", "donate": "Donación", - "copyright-label": "© 2020-2024 SimpleX | Proyecto de Código Abierto", + "copyright-label": "© 2020-2025 SimpleX | Proyecto de Código Abierto", "simplex-chat-protocol": "Protocolo de SimpleX Chat", "terms-and-privacy-policy": "Política de Privacidad", "hero-header": "Privacidad redefinida", @@ -40,7 +40,7 @@ "simplex-private-card-7-point-2": "Si se añade, elimina o modifica algún mensaje, el destinatario es avisado.", "simplex-private-card-8-point-1": "Los servidores SimpleX actúan como nodos de mezcla de baja latencia — los mensajes entrantes y salientes siguen un orden diferente.", "simplex-private-card-9-point-1": "Cada cola de mensajes transmite los mensajes en un solo sentido, con las distintas direcciones de envío y recepción.", - "simplex-explained": "Simplex explicado", + "simplex-explained": "SimpleX explicado", "simplex-explained-tab-1-p-1": "Puedes crear contactos y grupos, y mantener conversaciones bidireccionales como en cualquier aplicación de mensajería.", "simplex-explained-tab-1-p-2": "¿Cómo puede funcionar con colas unidireccionales y sin identificadores de usuario?", "simplex-explained-tab-2-p-1": "Por cada conexión se usan dos colas de mensajes separadas para que el envío y recepción se hagan a través de servidores diferentes.", @@ -122,7 +122,7 @@ "privacy-matters-overlay-card-3-p-2": "Una de las historias más estremecedoras es la experiencia de Mohamedou Ould Salahi descrita en sus memorias y plasmada en la película The Mauritanian. Fue internado en la prisión militar de Guantánamo sin juicio previo, donde fue torturado durante 15 años tras una llamada telefónica a su pariente en Afganistán, bajo sospecha de estar implicado en los atentados del 11-S a pesar de que vivía en Alemania desde hacía 10 años.", "privacy-matters-overlay-card-3-p-4": "No basta con usar mensajería cifrada de extremo a extremo, todos deberíamos utilizar las aplicaciones de mensajería que protegen la privacidad de nuestras redes personales y con quién estamos conectados.", "privacy-matters-overlay-card-3-p-3": "Personas corrientes son detenidas por lo que comparten en Internet, incluso a través de sus cuentas \"anónimas\" e incluso en países democráticos.", - "simplex-unique-overlay-card-1-p-2": "Para enviar mensajes, SimpleX utiliza direcciones anónimas por pares de colas de mensajes unidireccionales, separadas para mensajes recibidos y enviados, y normalmente a través de servidores diferentes. Utilizar SimpleX es como tener un correo electrónico “desechable” o un teléfono diferente para cada contacto, pero sin las complicaciones que implicaría gestionarlos.", + "simplex-unique-overlay-card-1-p-2": "Para enviar mensajes, SimpleX utiliza direcciones anónimas por pares de colas de mensajes unidireccionales, separadas para mensajes recibidos y enviados, y normalmente a través de servidores diferentes.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat almacena todos los datos de usuario únicamente en los dispositivos cliente usando un formato cifrado y portable de la base de datos, la cual puede ser exportada y transferida a cualquier dispositivo compatible.", "simplex-unique-overlay-card-3-p-3": "A diferencia de los servidores de redes federadas (correo electrónico, XMPP o Matrix), los servidores SimpleX no almacenan cuentas de usuario, sólo retransmiten mensajes, protegiendo así la privacidad de ambas partes.", "simplex-unique-overlay-card-4-p-1": "Puede usar SimpleX con sus propios servidores y aún así comunicarte con personas conectadas a los servidores preconfigurados y proporcionados por nosotros.", @@ -138,8 +138,7 @@ "donate-here-to-help-us": "Para ayudarnos haga una donación aquí", "enter-your-email-address": "Escriba su dirección de correo electrónico", "get-simplex": "Obtenga SimpleX desktop app", - "why-simplex-is": "Por qué SimpleX es", - "unique": "único", + "why-simplex-is-unique": "Por qué SimpleX es único", "learn-more": "Descubra más", "more-info": "Más información", "hide-info": "Ocultar información", @@ -244,15 +243,17 @@ "signing-key-fingerprint": "Huella digital de la clave de firma (SHA-256)", "releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen varios días más tarde en este repositorio", "comparison-section-list-point-4a": "Los servidores de retransmisión no pueden comprometer la encriptación e2e. Para evitar posibles ataques, verifique el código de seguridad mediante un canal alternativo", - "hero-overlay-3-title": "Evaluación de la seguridad", + "hero-overlay-3-title": "Evaluaciones de seguridad", "hero-overlay-card-3-p-2": "Trail of Bits revisó la criptografía y los componentes de red de la plataforma SimpleX en noviembre de 2022. Más información.", "jobs": "Únete al equipo", - "hero-overlay-3-textlink": "Evaluación de la seguridad", + "hero-overlay-3-textlink": "Evaluaciones de seguridad", "hero-overlay-card-3-p-1": "Trail of Bits es una consultora de seguridad y tecnología líder cuyos clientes incluyen grandes tecnológicas, agencias gubernamentales e importantes proyectos de blockchain.", "docs-dropdown-9": "Descargas", "please-enable-javascript": "Habilita JavaScript para ver el código QR.", "please-use-link-in-mobile-app": "Usa el enlace en la apliación móvil", "docs-dropdown-10": "Transparencia", "docs-dropdown-11": "FAQ", - "docs-dropdown-12": "Seguridad" + "docs-dropdown-12": "Seguridad", + "docs-dropdown-14": "SimpleX para empresas", + "hero-overlay-card-3-p-3": "Trail of Bits revisó el diseño criptográfico de los protocolos de red SimpleX en julio de 2024." } diff --git a/website/langs/fi.json b/website/langs/fi.json index 68c0d4f1b4..460aa7989b 100644 --- a/website/langs/fi.json +++ b/website/langs/fi.json @@ -112,7 +112,7 @@ "simplex-explained-tab-1-p-1": "Voit luoda yhteyshenkilöitä ja ryhmiä sekä käydä kaksisuuntaisia keskusteluja kuten missä tahansa muussa viestisovelluksessa.", "simplex-explained-tab-3-p-1": "Palvelimilla on erilliset anonyymit tunnistetiedot kullekin jonolle, eivätkä ne tiedä, mille käyttäjille ne kuuluvat.", "donate": "Lahjoita", - "copyright-label": "© 2020-2023 SimpleX | Avoin projekti", + "copyright-label": "© 2020-2025 SimpleX | Avoin projekti", "hero-p-1": "Muissa sovelluksissa on käyttäjätunnuksia: Signal, Matrix, Session, Briar, Jami, Cwtch, jne.
SimpleX ei käytä niitä, ei edes satunnaisia numeroita.
Tämä parantaa yksityisyyttäsi radikaalisti.", "simplex-private-1-title": "2 kerrosta päästä päähän salattua viestintää", "simplex-private-2-title": "Lisäkerros palvelimen salaukselle", @@ -168,7 +168,7 @@ "privacy-matters-overlay-card-3-p-3": "Tavalliset ihmiset pidätetään siitä, mitä he jakavat verkossa, jopa 'anonyymien' tiliensä kautta, jopa demokraattisissa maissa.", "privacy-matters-overlay-card-3-p-4": "Ei riitä, että käytät päästä päähän salattua viestintäsovellusta, meidän kaikkien pitäisi käyttää viestintäsovelluksia, jotka suojelevat henkilökohtaisten verkostojemme yksityisyyttä — keiden kanssa olemme yhteydessä.", "simplex-unique-overlay-card-1-p-1": "Toisin kuin muut viestintäalustat, SimpleX:llä ei ole mitään tunnisteita käyttäjille. Se ei luota puhelinnumeroihin, verkkotunnuksiin perustuviin osoitteisiin (kuten sähköposti tai XMPP), käyttäjänimiin, julkisiin avaimiin tai edes satunnaisiin numeroihin tunnistaakseen käyttäjänsä — emme tiedä kuinka monta ihmistä käyttää SimpleX-palvelimiamme.", - "simplex-unique-overlay-card-1-p-2": "Viestien toimittamiseksi SimpleX käyttää parittaisia nimettömiä osoitteita kaksisuuntaisille viestijonoille, jotka ovat erilliset vastaanotetuille ja lähetetyille viesteille, yleensä eri palvelimien kautta. SimpleX:n käyttö on kuin eri “kertakäyttöinen” sähköposti tai puhelin jokaiselle yhteydelle, eikä sinun tarvitse vaivautua niiden hallitsemiseen.", + "simplex-unique-overlay-card-1-p-2": "Viestien toimittamiseksi SimpleX käyttää parittaisia nimettömiä osoitteita kaksisuuntaisille viestijonoille, jotka ovat erilliset vastaanotetuille ja lähetetyille viesteille, yleensä eri palvelimien kautta.", "simplex-unique-overlay-card-1-p-3": "Tämä suunnittelu suojaa sitä, kenen kanssa kommunikoit, piilottamalla sen SimpleX-alustan palvelimilta ja kaikilta havainnoijilta. Piilottaaksesi IP-osoitteesi palvelimilta, voit yhdistää SimpleX-palvelimiin Tor-verkon kautta.", "simplex-unique-overlay-card-2-p-1": "Koska sinulla ei ole tunnistetta SimpleX-alustalla, kukaan ei voi ottaa sinuun yhteyttä, ellei jaa kertakäyttöistä tai väliaikaista käyttäjäosoitetta, kuten QR-koodia tai linkkiä.", "simplex-unique-overlay-card-2-p-2": "Jopa valinnaisen käyttäjäosoitteen kanssa, vaikka sitä voitaisiin käyttää roskapostiyhteyspyyntöjen lähettämiseen, voit vaihtaa sen tai poistaa sen kokonaan menettämättä mitään yhteyksiäsi.", @@ -187,8 +187,7 @@ "sign-up-to-receive-our-updates": "Tilaa päivityksemme", "enter-your-email-address": "Syötä sähköpostiosoitteesi", "get-simplex": "Hanki SimpleX desktop app", - "why-simplex-is": "Miksi SimpleX on", - "unique": "ainutlaatuinen", + "why-simplex-is-unique": "Miksi SimpleX on ainutlaatuinen", "learn-more": "Lue lisää", "more-info": "Lisätietoja", "contact-hero-subheader": "Skannaa QR-koodi SimpleX Chat -sovelluksella puhelimessasi tai tabletissasi.", diff --git a/website/langs/fr.json b/website/langs/fr.json index 61be2c8621..efc8d6c9c6 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -7,7 +7,7 @@ "why-simplex": "Pourquoi SimpleX", "simplex-privacy": "Confidentialité SimpleX", "simplex-network": "Réseau SimpleX", - "simplex-explained": "Simplex expliqué", + "simplex-explained": "SimpleX expliqué", "simplex-explained-tab-1-text": "1. L'expérience des utilisateurs", "simplex-explained-tab-2-text": "2. Comment ça marche", "simplex-explained-tab-3-text": "3. Ce que voient les serveurs", @@ -21,7 +21,7 @@ "smp-protocol": "Protocole SMP", "chat-protocol": "Protocole de chat", "donate": "Faire un don", - "copyright-label": "© 2020-2024 SimpleX | Projet Open-Source", + "copyright-label": "© 2020-2025 SimpleX | Projet Open-Source", "simplex-chat-protocol": "Protocole SimpleX Chat", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Politique de confidentialité", @@ -118,7 +118,7 @@ "privacy-matters-overlay-card-3-p-3": "Des personnes lambda se font arrêter pour ce qu'elles partagent en ligne, même via leurs comptes \"anonymes\", même dans les pays démocratiques.", "privacy-matters-overlay-card-3-p-4": "Il ne suffit pas d'utiliser une messagerie chiffrée de bout en bout, nous devrions tous utiliser des messageries qui protègent la vie privée de nos réseaux personnels — les personnes avec lesquelles nous sommes connectés.", "simplex-unique-overlay-card-1-p-1": "Contrairement aux autres plateformes de messagerie, SimpleX n'a pas d'identifiant attribué aux utilisateurs. Il ne s'appuie pas sur des numéros de téléphone, des adresses basées sur des domaines (comme l'e-mail ou XMPP), des noms d'utilisateur, des clés publiques ou même des nombres aléatoires pour identifier ses utilisateurs — nous ne savons pas combien de personnes utilisent nos serveurs SimpleX.", - "simplex-unique-overlay-card-1-p-2": "Pour envoyer des messages, SimpleX utilise des adresses anonymes par paires de files d'attente de messages unidirectionnelles, séparées pour les messages reçus et envoyés, généralement via des serveurs différents. Utiliser SimpleX, c'est comme avoir un e-mail ou un téléphone jetable différent pour chaque contact, sans aucune difficulté pour les gérer.", + "simplex-unique-overlay-card-1-p-2": "Pour envoyer des messages, SimpleX utilise des adresses anonymes par paires de files d'attente de messages unidirectionnelles, séparées pour les messages reçus et envoyés, généralement via des serveurs différents.", "simplex-unique-overlay-card-1-p-3": "Cette approche protège la confidentialité des personnes avec lesquelles vous communiquez, en la cachant des serveurs de la plateforme SimpleX et de tout observateur. Pour cacher votre adresse IP aux serveurs, vous pouvez vous connecter aux serveurs SimpleX via Tor.", "simplex-unique-overlay-card-2-p-1": "Parce que vous n'avez pas d'identifiant sur la plateforme SimpleX, personne ne peut vous contacter sauf si vous partagez une adresse d'utilisateur unique ou temporaire, sous forme de code QR ou de lien .", "simplex-unique-overlay-card-2-p-2": "Même avec l'adresse utilisateur facultative, bien qu'elle puisse être utilisée pour envoyer des demandes de contact de spam, vous pouvez la modifier ou la supprimer complètement sans perdre aucune de vos connexions.", @@ -144,8 +144,7 @@ "sign-up-to-receive-our-updates": "Inscrivez-vous pour recevoir nos mises à jour", "enter-your-email-address": "Entrez votre adresse e-mail", "get-simplex": "Obtenir SimpleX desktop app", - "why-simplex-is": "Pourquoi SimpleX est", - "unique": "unique", + "why-simplex-is-unique": "Pourquoi SimpleX est unique", "learn-more": "En savoir plus", "more-info": "Plus d'infos", "hide-info": "Masquer les infos", @@ -245,15 +244,17 @@ "f-droid-org-repo": "Dépot F-Droid.org", "stable-versions-built-by-f-droid-org": "Versions stables créées par F-Droid.org", "comparison-section-list-point-4a": "Les relais SimpleX ne peuvent pas compromettre le chiffrement e2e. Vérifier le code de sécurité pour limiter les attaques sur le canal hors bande", - "hero-overlay-3-title": "Évaluation de sécurité", + "hero-overlay-3-title": "Évaluation de la sécurité", "hero-overlay-card-3-p-2": "Trail of Bits a examiné les composants cryptographiques et réseau de la plateforme SimpleX en novembre 2022. En savoir plus.", "jobs": "Rejoignez notre équipe", - "hero-overlay-3-textlink": "Évaluation de sécurité", + "hero-overlay-3-textlink": "Évaluations de la sécurité", "hero-overlay-card-3-p-1": "Trail of Bits est un cabinet leader dans le secteur de la sécurité et des technologies qui compte parmi ses clients des grandes entreprises de la tech, des agences gouvernementales et d'importants projets de blockchain.", "docs-dropdown-9": "Téléchargements", "please-enable-javascript": "Veuillez activer JavaScript pour voir le code QR.", "please-use-link-in-mobile-app": "Veuillez utiliser le lien dans l'application mobile", "docs-dropdown-10": "Transparence", "docs-dropdown-12": "Sécurité", - "docs-dropdown-11": "FAQ" + "docs-dropdown-11": "FAQ", + "hero-overlay-card-3-p-3": "Trail of Bits a examiné la conception cryptographique des protocoles réseau SimpleX en juillet 2024.", + "docs-dropdown-14": "SimpleX pour les entreprises" } diff --git a/website/langs/he.json b/website/langs/he.json index 4fd966f05d..4a61999896 100644 --- a/website/langs/he.json +++ b/website/langs/he.json @@ -53,7 +53,7 @@ "smp-protocol": "פרוטוקול SMP", "chat-protocol": "פרוטוקול צ'אט", "donate": "תרומה", - "copyright-label": "© 2020-2024 SimpleX | פרויקט קוד פתוח", + "copyright-label": "© 2020-2025 SimpleX | פרויקט קוד פתוח", "hero-p-1": "לאפליקציות אחרות יש מזהי משתמש: Signal, Matrix, Session, Briar, Jami, Cwtch וכו'.
ל-SimpleX אין, אפילו לא מספרים אקראיים.
זה משפר באופן קיצוני את הפרטיות שלך.", "hero-overlay-2-title": "מדוע מזהי משתמש מזיקים לפרטיות?", "feature-6-title": "שיחות שמע ווידאו
מוצפנות מקצה לקצה", @@ -75,12 +75,12 @@ "privacy-matters-2-overlay-1-linkText": "פרטיות מעניקה לכם עוצמה", "privacy-matters-3-overlay-1-linkText": "פרטיות מגנה על החופש שלכם", "simplex-explained-tab-1-p-1": "אתם יכולים ליצור אנשי קשר וקבוצות, ולנהל שיחות דו-כיווניות, כמו בכל תוכנה אחרת לשליחת הודעות.", - "hero-overlay-3-title": "הערכת אבטחה", + "hero-overlay-3-title": "הערכות אבטחה", "simplex-unique-1-overlay-1-title": "פרטיות מלאה של הזהות, הפרופיל, אנשי הקשר והמטא נתונים שלך", "simplex-unique-3-overlay-1-title": "בעלות, שליטה ואבטחה של הנתונים שלך", "simplex-unique-2-overlay-1-title": "ההגנה הטובה ביותר מפני ספאם וניצול לרעה", "simplex-unique-3-title": "אתה שולט בנתונים שלך", - "hero-overlay-3-textlink": "הערכת אבטחה", + "hero-overlay-3-textlink": "הערכות אבטחה", "simplex-unique-4-overlay-1-title": "מבוזר לחלוטין — המשתמשים הם הבעלים של רשת SimpleX", "simplex-unique-2-title": "אתה מוגן
מפני ספאם וניצול לרעה", "simplex-unique-4-title": "רשת SimpleX בבעלותך", @@ -163,10 +163,9 @@ "simplex-unique-overlay-card-3-p-2": "ההודעות המוצפנות מקצה לקצה מוחזקות באופן זמני בשרתי ממסר של SimpleX עד שמתקבלות, ואז הן נמחקות לצמיתות.", "simplex-unique-overlay-card-4-p-2": "פלטפורמת SimpleX משתמשת בפרוטוקול פתוח ומספקת SDK ליצירת צ'אט בוטים, המאפשר הטמעה של שירותים שמשתמשים יכולים לתקשר איתם באמצעות אפליקציות SimpleX Chat — אנחנו ממש מצפים לראות אילו שירותי SimpleX אתם יכולים לבנות.", "contact-hero-p-2": "עדיין לא הורדתם את ה-SimpleX Chat?", - "why-simplex-is": "מדוע SimpleX הוא", + "why-simplex-is-unique": "מדוע SimpleX הוא ייחודי", "simplex-network-section-header": "רשת SimpleX", "tap-the-connect-button-in-the-app": "הקישו על הלחצן 'התחבר' באפליקציה", - "unique": "ייחודי", "simplex-network-1-overlay-linktext": "בעיות של רשתות P2P", "protocol-2-text": "XMPP, Matrix", "simplex-network-overlay-card-1-li-4": "יישומי P2P יכולים להיחסם על ידי ספקי אינטרנט מסוימים (כמו BitTorrent). SimpleX הוא אגנוסטי לתעבורה - הוא יכול לעבוד על פרוטוקולי אינטרנט סטנדרטיים, למשל WebSockets.", @@ -254,5 +253,7 @@ "please-use-link-in-mobile-app": "אנא השתמש בקישור באפליקציה במכשיר נייד", "docs-dropdown-10": "שקיפות", "docs-dropdown-11": "שאלות ותשובות", - "docs-dropdown-12": "אבטחה" + "docs-dropdown-12": "אבטחה", + "hero-overlay-card-3-p-3": "Trail of bits סקר את הקוד הקריפטוגרפי של פרוטוקולי רשת SimpleX ביולי 2024. קרא עוד.", + "docs-dropdown-14": "SimpleX לעסקים" } diff --git a/website/langs/hu.json b/website/langs/hu.json index ae9ee1d12b..9dfd3d9f65 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -1,32 +1,32 @@ { "home": "Kezdőoldal", "developers": "Fejlesztők", - "reference": "Referencia", + "reference": "Hivatkozás", "blog": "Blog", "features": "Funkciók", "why-simplex": "Miért válassza a SimpleXet", "simplex-privacy": "A SimpleX adatvédelme", "simplex-network": "A SimpleX-hálózat", - "simplex-explained": "A Simplex bemutatása", + "simplex-explained": "A SimpleX bemutatása", "simplex-explained-tab-1-text": "1. Felhasználói élmény", "simplex-explained-tab-2-text": "2. Hogyan működik", "simplex-explained-tab-3-text": "3. Mit látnak a kiszolgálók", - "simplex-explained-tab-1-p-1": "Létrehozhat kapcsolatokat és csoportokat, valamint kétirányú beszélgetéseket folytathat, ugyanúgy mint bármely más üzenetváltón.", + "simplex-explained-tab-1-p-1": "Létrehozhat kapcsolatokat és csoportokat, valamint kétirányú beszélgetéseket folytathat, ugyanúgy mint bármely más üzenetváltó-alkalmazásban.", "simplex-explained-tab-1-p-2": "Hogyan működhet egyirányú üzenet-sorballítással és felhasználói profilazonosítók nélkül?", "simplex-explained-tab-2-p-1": "Minden kapcsolathoz két különböző üzenetküldési sorbaállítást használ a különböző kiszolgálókon keresztül történő üzenetküldéshez és -fogadáshoz.", - "simplex-explained-tab-2-p-2": "A kiszolgálók csak egyirányú üzeneteket továbbítanak, anélkül, hogy teljes képet kapnának a felhasználók beszélgetéseiről vagy kapcsolatairól.", + "simplex-explained-tab-2-p-2": "A kiszolgálók csak egyetlen irányba továbbítják az üzeneteket, anélkül, hogy teljes képet kapnának a felhasználók beszélgetéseiről vagy kapcsolatairól.", "simplex-explained-tab-3-p-1": "A kiszolgálók minden egyes üzenetsorbaállításhoz külön névtelen hitelesítő-adatokkal rendelkeznek, és nem tudják, hogy melyik felhasználóhoz tartoznak.", "simplex-explained-tab-3-p-2": "A felhasználók tovább fokozhatják a metaadatok adatvédelmét, ha a Tor segítségével férnek hozzá a kiszolgálókhoz, így megakadályozva az IP-cím szerinti korrelációt.", "smp-protocol": "SMP-protokoll", - "chat-protocol": "Csevegésprotokoll", - "donate": "Támogatás", - "copyright-label": "© 2020-2024 SimpleX | Nyílt forráskódú projekt", + "chat-protocol": "Csevegési protokoll", + "donate": "Adományozás", + "copyright-label": "© 2020-2025 SimpleX | Nyílt forráskódú projekt", "simplex-chat-protocol": "A SimpleX Chat-protokoll", "terminal-cli": "Terminál CLI", "terms-and-privacy-policy": "Adatvédelmi irányelvek", - "hero-header": "Újradefiniált adatvédelem", - "hero-subheader": "Az első üzenetváltó
felhasználói azonosítók nélkül", - "hero-p-1": "Más alkalmazások felhasználói azonosítókkal rendelkeznek: Signal, Matrix, Session, Briar, Jami, Cwtch, stb.
A SimpleX nem, még véletlenszerű számokkal sem.
Ez radikálisan javítja az adatvédelmet.", + "hero-header": "Újraértelmezett adatvédelem", + "hero-subheader": "Az első üzenetváltó-alkalmazás
felhasználói azonosítók nélkül", + "hero-p-1": "Más alkalmazások felhasználói azonosítókkal rendelkeznek: Signal, Matrix, Session, Briar, Jami, Cwtch, stb.
A SimpleX azonban nem, még véletlenszerű számokkal sem.
Ez radikálisan javítja az adatvédelmet.", "hero-overlay-1-textlink": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", "hero-overlay-2-textlink": "Hogyan működik a SimpleX?", "hero-overlay-3-textlink": "Biztonsági felmérések", @@ -43,7 +43,7 @@ "feature-6-title": "E2E-titkosított
hang- és videohívások", "feature-7-title": "Hordozható titkosított alkalmazás-adattárolás — profil átköltöztetése egy másik eszközre", "feature-8-title": "Az inkognitómód —
egyedülálló a SimpleX Chatben", - "simplex-network-overlay-1-title": "Összehasonlítás más P2P üzenetküldő protokollokkal", + "simplex-network-overlay-1-title": "Összehasonlítás más P2P-üzenetküldő protokollokkal", "simplex-private-1-title": "2 rétegű végpontok közötti titkosítás", "simplex-private-2-title": "További rétege a
kiszolgáló-titkosítás", "simplex-private-4-title": "Nem kötelező
hozzáférés Tor-on keresztül", @@ -59,8 +59,8 @@ "simplex-private-card-3-point-1": "A kliens és a kiszolgálók közötti kapcsolatokhoz csak az erős algoritmusokkal rendelkező TLS 1.2/1.3 protokollt használja.", "simplex-private-card-3-point-2": "A kiszolgáló ujjlenyomata és a csatornakötés megakadályozza a MITM- és a visszajátszási támadásokat.", "simplex-private-card-3-point-3": "Az újrakapcsolódás le van tiltva a munkamenet elleni támadások megelőzése érdekében.", - "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a TORon vagy más átvitel-átfedő-hálózaton keresztül is elérheti.", - "simplex-private-card-6-point-1": "Számos kommunikációs platform sebezhető a kiszolgálók vagy a hálózat-szolgáltatók MITM-támadásaival szemben.", + "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a Tor-on vagy más átvitel-átfedő-hálózaton keresztül is elérheti.", + "simplex-private-card-6-point-1": "Számos kommunikációs hálózat sebezhető a kiszolgálók vagy a hálózat-szolgáltatók MITM-támadásaival szemben.", "simplex-private-card-6-point-2": "Ennek megakadályozása érdekében a SimpleX-alkalmazások egyszeri kulcsokat adnak át sávon kívül, amikor egy címet hivatkozásként vagy QR-kódként oszt meg.", "simplex-private-card-7-point-1": "Az integritás garantálása érdekében az üzenetek sorszámozással vannak ellátva, és tartalmazzák az előző üzenet hasítóértékét.", "simplex-private-card-7-point-2": "Ha bármilyen üzenetet hozzáadnak, eltávolítanak vagy módosítanak, a címzett értesítést kap róla.", @@ -79,20 +79,20 @@ "privacy-matters-3-overlay-1-linkText": "Az adatvédelem szabaddá tesz", "simplex-unique-1-title": "Teljes magánéletet élvezhet", "simplex-unique-1-overlay-1-title": "Személyazonosságának, profiljának, kapcsolatainak és metaadatainak teljes körű védelme", - "simplex-unique-2-title": "Véd
a kéretlen üzenetektől és a visszaélésektől", - "simplex-unique-2-overlay-1-title": "A legjobb védelem a kéretlen üzenetek és a visszaélések ellen", + "simplex-unique-2-title": "Véd
a kéretlen tartalmaktól és a visszaélésektől", + "simplex-unique-2-overlay-1-title": "A legjobb védelem a kéretlen tartalmak és a visszaélések ellen", "simplex-unique-3-title": "Ön kezeli az adatait", "simplex-unique-3-overlay-1-title": "Az adatok biztonsága és kezelése az Ön kezében van", "simplex-unique-4-title": "Öné a SimpleX-hálózat", "simplex-unique-4-overlay-1-title": "Teljesen decentralizált — a SimpleX-hálózat a felhasználóké", "hero-overlay-card-1-p-1": "Sok felhasználó kérdezte: ha a SimpleXnek nincsenek felhasználói azonosítói, honnan tudja, hogy hová kell eljuttatni az üzeneteket?", - "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi platform által használt felhasználói azonosítók helyett a SimpleX az üzenetek sorbaállításához ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", - "hero-overlay-card-1-p-4": "Ez a kialakítás megakadályozza a felhasználók metaadatainak kiszivárgását az alkalmazás szintjén. Az adatvédelem további javítása és az IP-cím védelme érdekében az üzenetküldő kiszolgálókhoz TOR hálózaton keresztül is kapcsolódhat.", + "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi hálózat által használt felhasználói azonosítók helyett a SimpleX az üzenetek sorbaállításához ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", + "hero-overlay-card-1-p-4": "Ez a kialakítás megakadályozza a felhasználók metaadatainak kiszivárgását az alkalmazás szintjén. Az adatvédelem további javítása és az IP-cím védelme érdekében az üzenetküldő kiszolgálókhoz Tor hálózaton keresztül is kapcsolódhat.", "hero-overlay-card-1-p-5": "Csak a kliensek tárolják a felhasználói profilokat, kapcsolatokat és csoportokat; az üzenetek küldése 2 rétegű végpontok közötti titkosítással történik.", "hero-overlay-card-1-p-6": "További leírást a SimpleX ismertetőben olvashat.", "hero-overlay-card-2-p-1": "Ha a felhasználók állandó azonosítóval rendelkeznek, még akkor is, ha ez csak egy véletlenszerű szám, például egy munkamenet-azonosító, fennáll annak a veszélye, hogy a szolgáltató vagy egy támadó megfigyelheti, azt hogy hogyan kapcsolódnak a felhasználók egymáshoz, és hány üzenetet küldenek egymásnak.", "hero-overlay-card-2-p-2": "Ezt az információt aztán összefüggésbe hozhatják a meglévő nyilvános közösségi hálózatokkal, és meghatározhatnak néhány valódi személyazonosságot.", - "hero-overlay-card-2-p-3": "Még a TOR v3 szolgáltatásokat használó, legprivátabb alkalmazások esetében is, ha két különböző kapcsolattartóval beszél ugyanazon a profilon keresztül, bizonyítani tudják, hogy ugyanahhoz a személyhez kapcsolódnak.", + "hero-overlay-card-2-p-3": "Még a Tor v3 szolgáltatásokat használó, legprivátabb alkalmazások esetében is, ha két különböző kapcsolattartóval beszél ugyanazon a profilon keresztül, bizonyítani tudják, hogy ugyanahhoz a személyhez kapcsolódnak.", "hero-overlay-card-2-p-4": "A SimpleX úgy védekezik ezen támadások ellen, hogy nem tartalmaz felhasználói azonosítókat. Ha pedig használja az inkognitómódot, akkor minden egyes létrejött kapcsolatban más-más felhasználó név jelenik meg, így elkerülhető a közöttük lévő összefüggések teljes bizonyítása.", "hero-overlay-card-3-p-1": "Trail of Bits egy vezető biztonsági és technológiai tanácsadó cég, amelynek az ügyfelei közé tartoznak nagy technológiai cégek, kormányzati ügynökségek és jelentős blokklánc projektek.", "hero-overlay-card-3-p-2": "A Trail of Bits 2022 novemberében áttekintette a SimpleX-platform kriptográfiai és hálózati komponenseit. További információk.", @@ -104,43 +104,42 @@ "privacy-matters-overlay-card-1-p-1": "Sok nagyvállalat arra használja fel az önnel kapcsolatban álló személyek adatait, hogy megbecsülje az ön jövedelmét, hogy olyan termékeket adjon el önnek, amelyekre valójában nincs is szüksége, és hogy meghatározza az árakat.", "privacy-matters-overlay-card-1-p-2": "Az online kiskereskedők tudják, hogy az alacsonyabb jövedelműek nagyobb valószínűséggel vásárolnak azonnal, ezért magasabb árakat számíthatnak fel, vagy eltörölhetik a kedvezményeket.", "privacy-matters-overlay-card-1-p-3": "Egyes pénzügyi és biztosítótársaságok szociális grafikonokat használnak a kamatlábak és a díjak meghatározásához. Ez gyakran arra készteti az alacsonyabb jövedelmű embereket, hogy többet fizessenek — ez az úgynevezett „szegénységi prémium”.", - "privacy-matters-overlay-card-1-p-4": "A SimpleX-platform minden alternatívánál jobban védi a kapcsolatainak adatait, teljes mértékben megakadályozva, hogy a ismeretségi-hálója bármilyen vállalat vagy szervezet számára elérhetővé váljon. Még ha az emberek a SimpleX Chat által biztosított kiszolgálókat is használják, sem a felhasználók számát, sem a kapcsolataikat nem ismerjük.", + "privacy-matters-overlay-card-1-p-4": "A SimpleX-hálózat minden alternatívánál jobban védi a kapcsolatainak adatait, teljes mértékben megakadályozva, hogy az ismeretségi-hálója bármilyen vállalat vagy szervezet számára elérhetővé váljon. Még ha az emberek a SimpleX Chat által előre beállított kiszolgálókat is használják, sem az alkalmazások, sem a kiszolgálók üzemeltetői nem ismerik, sem felhasználók számát, sem a kapcsolataikat.", "privacy-matters-overlay-card-2-p-1": "Nem is olyan régen megfigyelhettük, hogy a nagy választásokat manipulálta egy neves tanácsadó cég, amely az ismeretségi-háló segítségével eltorzította a valós világról alkotott képünket, és manipulálta a szavazatainkat.", - "privacy-matters-overlay-card-2-p-2": "Ahhoz, hogy objektív legyen és független döntéseket tudjon hozni, az információs terét is kézben kell tartania. Ez csak akkor lehetséges, ha privát kommunikációs platformot használ, amely nem fér hozzá az ismeretségi-hálójához.", - "privacy-matters-overlay-card-2-p-3": "A SimpleX az első olyan platform, amely eleve nem rendelkezik felhasználói azonosítókkal, így jobban védi az ismeretségi-hálóját, mint bármely ismert alternatíva.", + "privacy-matters-overlay-card-2-p-2": "Ahhoz, hogy objektív legyen és független döntéseket tudjon hozni, az információs terét is kézben kell tartania. Ez csak akkor lehetséges, ha privát kommunikációs hálózatot használ, amely nem fér hozzá az ismeretségi-hálójához.", + "privacy-matters-overlay-card-2-p-3": "A SimpleX az első olyan hálózat, amely eleve nem rendelkezik felhasználói azonosítókkal, így jobban védi az ismeretségi-hálóját, mint bármely ismert alternatíva.", "privacy-matters-overlay-card-3-p-1": "Mindenkinek törődnie kell a magánélet és a kommunikáció biztonságával — az ártalmatlan beszélgetések veszélybe sodorhatják, még akkor is, ha nincs semmi rejtegetnivalója.", "privacy-matters-overlay-card-3-p-2": "Az egyik legmegdöbbentőbb a Mohamedou Ould Salahi memoárjában leírt és az „A mauritániai” c. filmben bemutatott történet. Őt bírósági tárgyalás nélkül a guantánamói táborba zárták, és ott kínozták 15 éven át, miután egy afganisztáni rokonát telefonon felhívta, akit azzal gyanúsítottak a hatóságok, hogy köze van a 9/11-es merényletekhez, holott Salahi az előző 10 évben Németországban élt.", "privacy-matters-overlay-card-3-p-3": "Átlagos embereket letartóztatnak azért, amit online megosztanak, még „névtelen” fiókjaikon keresztül is, még demokratikus országokban is.", - "privacy-matters-overlay-card-3-p-4": "Nem elég csak egy végpontok között titkosított üzenetváltó alkalmazást használnunk, mindannyiunknak olyan üzenetváltó alkalmazásokat kell használnunk, amelyek védik a személyes partnereink magánéletét — akikkel kapcsolatban állunk.", - "simplex-unique-overlay-card-1-p-1": "Más üzenetküldő platformoktól eltérően a SimpleX nem rendel azonosítókat a felhasználókhoz. Nem támaszkodik telefonszámokra, tartomány-alapú címekre (mint az e-mail, XMPP vagy a Matrix), felhasználónevekre, nyilvános kulcsokra vagy akár véletlenszerű számokra a felhasználók azonosításához — nem tudjuk, hogy hányan használják a SimpleX-kiszolgálóinkat.", - "simplex-unique-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez a SimpleX az egyirányú üzenet várakoztatást használ páronkénti névtelen címekkel, külön a fogadott és külön az elküldött üzenetek számára, általában különböző kiszolgálókon keresztül. A SimpleX használata olyan, mintha minden egyes kapcsolatnak más-más “eldobható” e-mail-címe vagy telefonja lenne és nem kell ezeket gondosan kezelni.", - "simplex-unique-overlay-card-1-p-3": "Ez a kialakítás megvédi annak titkosságát, hogy kivel kommunikál, elrejtve azt a SimpleX platform kiszolgálói és a megfigyelők elől. IP-címének a kiszolgálók elől való elrejtéséhez azt teheti meg, hogy Toron keresztül kapcsolódik a SimpleX-kiszolgálókhoz.", - "simplex-unique-overlay-card-2-p-1": "Mivel ön nem rendelkezik azonosítóval a SimpleX-platformon, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", - "simplex-unique-overlay-card-2-p-2": "Még a nem kötelező felhasználói cím esetében is, bár spam kapcsolatfelvételi kérések küldésére használható, megváltoztathatja vagy teljesen törölheti azt anélkül, hogy elveszítené a meglévő kapcsolatait.", + "privacy-matters-overlay-card-3-p-4": "Nem elég csak egy végpontok között titkosított üzenetváltó-alkalmazást használnunk, mindannyiunknak olyan üzenetváltó-alkalmazásokat kell használnunk, amelyek védik a személyes partnereink magánéletét — akikkel kapcsolatban állunk.", + "simplex-unique-overlay-card-1-p-1": "Más üzenetküldő hálózatoktól eltérően a SimpleX nem rendel azonosítókat a felhasználókhoz. Nem támaszkodik telefonszámokra, tartomány-alapú címekre (mint az e-mail, XMPP vagy a Matrix), felhasználónevekre, nyilvános kulcsokra vagy akár véletlenszerű számokra a felhasználók azonosításához — a SimpleX-kiszolgálók üzemeltetői nem tudják, hogy hányan használják a kiszolgálóikat.", + "simplex-unique-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez a SimpleX az egyirányú üzenet várakoztatást használ páronkénti névtelen címekkel, külön a fogadott és külön az elküldött üzenetek számára, általában különböző kiszolgálókon keresztül.", + "simplex-unique-overlay-card-1-p-3": "Ez a kialakítás megvédi annak titkosságát, hogy kivel kommunikál, elrejtve azt a SimpleX-hálózat kiszolgálói és a megfigyelők elől. IP-címének a kiszolgálók elől való elrejtéséhez azt teheti meg, hogy Toron keresztül kapcsolódik a SimpleX-kiszolgálókhoz.", + "simplex-unique-overlay-card-2-p-1": "Mivel ön nem rendelkezik azonosítóval a SimpleX-hálózaton, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", + "simplex-unique-overlay-card-2-p-2": "Még a felhasználói cím használata esetén is, aminek használata nem kötelező – ugyanakkor ez a kéretlen kapcsolatkérelmek küldésére is használható – módosíthatja vagy teljesen törölheti anélkül, hogy elveszítené a meglévő kapcsolatait.", "simplex-unique-overlay-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban, amely exportálható és átvihető bármely más támogatott eszközre.", "simplex-unique-overlay-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX továbbítókiszolgálóin tartózkodnak, amíg be nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", "simplex-unique-overlay-card-3-p-3": "A föderált hálózatok kiszolgálóitól (e-mail, XMPP vagy Matrix) eltérően a SimpleX-kiszolgálók nem tárolják a felhasználói fiókokat, csak továbbítják az üzeneteket, így védve mindkét fél magánéletét.", "simplex-unique-overlay-card-3-p-4": "A küldött és a fogadott kiszolgálóforgalom között nincsenek közös azonosítók vagy titkosított szövegek — ha bárki megfigyeli, nem tudja könnyen megállapítani, hogy ki kivel kommunikál, még akkor sem, ha a TLS-t kompromittálják.", - "simplex-unique-overlay-card-4-p-1": "Használhatja a SimpleXet a saját kiszolgálóival, és továbbra is kommunikálhat azokkal, akik az általunk biztosított, előre konfigurált kiszolgálókat használják.", - "simplex-unique-overlay-card-4-p-2": "A SimpleX platform nyitott protokollt használ és SDK-t biztosít a chatbotok létrehozásához, lehetővé téve olyan szolgáltatások megvalósítását, amelyekkel a felhasználók a SimpleX Chat alkalmazásokon keresztül léphetnek kapcsolatba — mi már nagyon várjuk, hogy milyen SimpleX szolgáltatásokat készítenek a lelkes közreműködők.", - "simplex-unique-overlay-card-4-p-3": "Ha a SimpleX-platformra való fejlesztést fontolgatja, például a SimpleX-alkalmazások felhasználóinak szánt chatbotot, vagy a SimpleX Chat könyvtárbotjának integrálását más mobilalkalmazásba, lépjen velünk kapcsolatba, ha bármilyen tanácsot vagy támogatást szeretne kapni.", - "simplex-unique-card-1-p-1": "A SimpleX védi az ön profiljához tartozó kapcsolatait és metaadatait, elrejtve azokat a SimpleX platform kiszolgálói és a megfigyelők elől.", - "simplex-unique-card-1-p-2": "Minden más létező üzenetküldő platformtól eltérően a SimpleX nem rendelkezik a felhasználókhoz rendelt azonosítókkal — még véletlenszerű számokkal sem.", - "simplex-unique-card-2-p-1": "Mivel a SimpleX platformon nincs azonosítója vagy állandó címe, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", + "simplex-unique-overlay-card-4-p-1": "Használhatja a SimpleXet a saját kiszolgálóival, és továbbra is kommunikálhat azokkal, akik az előre beállított kiszolgálókat használják az alkalmazásban.", + "simplex-unique-overlay-card-4-p-2": "A SimpleX-hálózat nyitott protokollt használ és SDK-t biztosít a chatbotok létrehozásához, lehetővé téve olyan szolgáltatások megvalósítását, amelyekkel a felhasználók a SimpleX Chat alkalmazásokon keresztül léphetnek kapcsolatba — mi már nagyon várjuk, hogy milyen SimpleX szolgáltatásokat készítenek a lelkes közreműködők.", + "simplex-unique-overlay-card-4-p-3": "Ha a SimpleX-hálózatra való fejlesztést fontolgatja, például a SimpleX-alkalmazások felhasználóinak szánt chatbotot, vagy a SimpleX Chat könyvtárbotjának integrálását más mobilalkalmazásba, lépjen velünk kapcsolatba, ha bármilyen tanácsot vagy támogatást szeretne kapni.", + "simplex-unique-card-1-p-1": "A SimpleX megvédi a profilhoz tartozó kapcsolatokat és metaadatokat, elrejtve azokat a SimpleX-hálózat kiszolgálói és a megfigyelők elől.", + "simplex-unique-card-1-p-2": "Minden más létező üzenetküldő hálózattól eltérően a SimpleX nem rendelkezik a felhasználókhoz rendelt azonosítókkal — még véletlenszerű számokkal sem.", + "simplex-unique-card-2-p-1": "Mivel a SimpleX-hálózaton nincs azonosítója vagy állandó címe, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", "simplex-unique-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban —, amely exportálható és átvihető bármely más támogatott eszközre.", "simplex-unique-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX továbbítókiszolgálóin tartózkodnak, amíg be nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", - "simplex-unique-card-4-p-1": "A SimpleX hálózat teljesen decentralizált és független bármely kriptopénztől vagy bármely más platformtól, kivéve az internetet.", + "simplex-unique-card-4-p-1": "A SimpleX-hálózat teljesen decentralizált és független bármely kriptopénztől vagy bármely más hálózattól, kivéve az internetet.", "simplex-unique-card-4-p-2": "Használhatja a SimpleXet a saját kiszolgálóival vagy az általunk biztosított kiszolgálókkal, és továbbra is kapcsolódhat bármely felhasználóhoz.", - "join": "Csatlakozás", - "we-invite-you-to-join-the-conversation": "Meghívjuk Önt, hogy csatlakozzon a beszélgetéshez", + "join": "Csatlakozzon a közösségeinkhez", + "we-invite-you-to-join-the-conversation": "Meghívjuk Önt, hogy csatlakozzon a beszélgetésekhez", "join-the-REDDIT-community": "Csatlakozzon a REDDIT közösséghez", "join-us-on-GitHub": "Csatlakozzon hozzánk a GitHubon", "donate-here-to-help-us": "Adományozzon és segítsen nekünk", - "sign-up-to-receive-our-updates": "Regisztráljon az oldalra, hogy megkapja frissítéseinket", + "sign-up-to-receive-our-updates": "Regisztráljon a hírleveleinkre, hogy ne maradjon le semmiről", "enter-your-email-address": "Adja meg az e-mail-címét", - "get-simplex": "A SimpleX számítógép-alkalmazásának letöltése", - "why-simplex-is": "A SimpleX mitől", - "unique": "egyedülálló", + "get-simplex": "A SimpleX számítógépes alkalmazásának letöltése", + "why-simplex-is-unique": "Mitől egyedülálló a SimpleX", "learn-more": "Tudjon meg többet", "more-info": "További információ", "hide-info": "Információ elrejtése", @@ -166,19 +165,19 @@ "copy-the-command-below-text": "másolja be az alábbi parancsot, és használja a csevegésben:", "privacy-matters-section-header": "Miért számít az adatvédelem", "privacy-matters-section-subheader": "A metaadatok védelmének megőrzése — kivel beszélget — megvédi a következőktől:", - "privacy-matters-section-label": "Győződjön meg arról, hogy az üzenetváltó amit használ nem fér hozzá az adataidhoz!", + "privacy-matters-section-label": "Győződjön meg arról, hogy az üzenetváltó-alkalmazás amit használ nem fér hozzá az adataihoz!", "simplex-private-section-header": "Mitől lesz a SimpleX privát", - "simplex-network-section-header": "SimpleX hálózat", - "simplex-network-section-desc": "A Simplex Chat a P2P és a föderált hálózatok előnyeinek kombinálásával biztosítja a legjobb adatvédelmet.", + "simplex-network-section-header": "SimpleX-hálózat", + "simplex-network-section-desc": "A Simplex Chat a P2P- és a föderált hálózatok előnyeinek kombinálásával biztosítja a legjobb adatvédelmet.", "simplex-network-1-desc": "Minden üzenet a kiszolgálókon keresztül kerül elküldésre, ami jobb metaadat-védelmet és megbízható aszinkron üzenetkézbesítést biztosít, miközben elkerülhető a sok", "simplex-network-2-header": "A föderált hálózatokkal ellentétben", "simplex-network-2-desc": "A SimpleX továbbítókiszolgálói NEM tárolnak felhasználói profilokat, kapcsolatokat és kézbesített üzeneteket, NEM kapcsolódnak egymáshoz, és NINCS kiszolgálókönyvtár.", - "simplex-network-3-header": "SimpleX hálózat", + "simplex-network-3-header": "SimpleX-hálózat", "simplex-network-3-desc": "a kiszolgálók egyirányú üzenet várakoztatásokat biztosítanak a felhasználók összekapcsolásához, de nem látják a hálózati kapcsolati gráfot; azt csak a felhasználók látják.", "comparison-section-header": "Összehasonlítás más protokollokkal", "protocol-1-text": "Signal, nagy platformok", "protocol-2-text": "XMPP, Matrix", - "protocol-3-text": "P2P protokollok", + "protocol-3-text": "P2P-protokollok", "comparison-point-1-text": "Globális személyazonosságot igényel", "comparison-point-2-text": "MITM lehetősége", "comparison-point-4-text": "Egyetlen vagy központosított hálózat", @@ -195,9 +194,9 @@ "comparison-section-list-point-4a": "A SimpleX továbbítókiszolgálói nem veszélyeztethetik az e2e titkosítást. Hitelesítse a biztonsági kódot a sávon kívüli csatorna elleni támadások veszélyeinek csökkentésére", "comparison-section-list-point-4": "Ha az üzemeltetett kiszolgálók veszélybe kerülnek. Hitelesítse a biztonsági kódot a Signal vagy más biztonságos üzenetküldő alkalmazás segítségével a támadások veszélyeinek csökkentésére", "comparison-section-list-point-5": "Nem védi a felhasználók metaadatait", - "comparison-section-list-point-6": "Bár a P2P elosztott, de nem föderált - egyetlen hálózatként működnek", + "comparison-section-list-point-6": "Bár a P2P elosztott, de nem föderált — egyetlen hálózatként működnek", "comparison-section-list-point-7": "A P2P-hálózatoknak vagy van egy központi hitelesítője, vagy az egész hálózat kompromittálódhat", - "see-here": "lásd itt", + "see-here": "tekintse meg itt", "guide-dropdown-1": "Gyors indítás", "guide-dropdown-2": "Üzenetek küldése", "guide-dropdown-3": "Titkos csoportok", @@ -207,7 +206,7 @@ "guide-dropdown-7": "Adatvédelem és biztonság", "guide-dropdown-8": "Alkalmazás beállításai", "guide": "Útmutató", - "docs-dropdown-1": "SimpleX platform", + "docs-dropdown-1": "SimpleX-hálózat", "docs-dropdown-2": "Android fájlok elérése", "docs-dropdown-3": "Hozzáférés a csevegési adatbázishoz", "docs-dropdown-8": "SimpleX jegyzékszolgáltatás", @@ -223,19 +222,19 @@ "please-use-link-in-mobile-app": "Használja a mobilalkalmazásban található hivatkozást", "contact-hero-header": "Kapott egy meghívót a SimpleX Chaten való beszélgetéshez", "invitation-hero-header": "Kapott egy egyszer használható meghívót a SimpleX Chaten való beszélgetéshez", - "simplex-network-overlay-card-1-li-4": "A P2P-megvalósításokat egyes internetszolgáltatók blokkolhatják (mint például a BitTorrent). A SimpleX átvitel-független - a szabványos webes protokollokon, pl. WebSocketsen keresztül is működik.", - "simplex-private-card-4-point-2": "A SimpleX TORon keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxyt (vagy a VPN-t az iOS-ban).", + "simplex-network-overlay-card-1-li-4": "A P2P-megvalósításokat egyes internetszolgáltatók blokkolhatják (mint például a BitTorrent). A SimpleX átvitel-független — a szabványos webes protokollokon, például WebSocketsen keresztül is működik.", + "simplex-private-card-4-point-2": "A SimpleX Tor-on keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxyt (vagy a VPN-t az iOS-ban).", "simplex-private-card-5-point-1": "A SimpleX minden titkosítási réteghez tartalomkitöltést használ, hogy meghiúsítsa az üzenetméret ellen irányuló támadásokat.", "simplex-private-card-5-point-2": "A kiszolgálók és a hálózatot megfigyelők számára a különböző méretű üzenetek egyformának tűnnek.", "privacy-matters-1-title": "Hirdetés és árdiszkrimináció", "hero-overlay-card-1-p-3": "Ön határozza meg, hogy melyik kiszolgáló(ka)t használja az üzenetek fogadására, a kapcsolatokhoz — azokat a kiszolgálókat, amelyeket az üzenetek küldésére használ. Minden beszélgetés két különböző kiszolgálót használ.", "simplex-network-overlay-card-1-p-1": "A P2P üzenetküldő protokollok és alkalmazások számos problémával küzdenek, amelyek miatt kevésbé megbízhatóak, mint a SimpleX, bonyolultabb az elemzésük és többféle támadással szemben sebezhetőek.", - "chat-bot-example": "Chat bot példa", + "chat-bot-example": "Példa csevegési botra", "simplex-private-3-title": "Biztonságos, hitelesített
TLS adatátvitel", "github-repository": "GitHub tárolójában", "tap-to-close": "Koppintson a bezáráshoz", - "simplex-network-1-header": "A P2P hálózatokkal ellentétben", - "simplex-network-1-overlay-linktext": "a P2P hálózat problémái", + "simplex-network-1-header": "A P2P-hálózatokkal ellentétben", + "simplex-network-1-overlay-linktext": "a P2P-hálózatok problémái", "comparison-point-3-text": "Függés a DNS-től", "yes": "Igen", "guide-dropdown-9": "Kapcsolatok létrehozása", diff --git a/website/langs/it.json b/website/langs/it.json index 502ab6d886..18a47bfe9f 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -10,7 +10,7 @@ "simplex-explained-tab-3-p-1": "I server hanno credenziali anonime separate per ogni coda e non sanno a quali utenti appartengano.", "chat-protocol": "Protocollo di chat", "donate": "Dona", - "copyright-label": "© 2020-2024 SimpleX | Progetto Open-Source", + "copyright-label": "© 2020-2025 SimpleX | Progetto Open-Source", "simplex-chat-protocol": "Protocollo di SimpleX Chat", "terminal-cli": "Terminale CLI", "terms-and-privacy-policy": "Informativa sulla privacy", @@ -60,30 +60,29 @@ "hero-overlay-card-2-p-2": "Potrebbero quindi correlare queste informazioni con i social network pubblici esistenti e determinare alcune identità reali.", "hero-overlay-card-2-p-4": "SimpleX protegge da questi attacchi non avendo alcun ID utente per design. Inoltre, se usi la modalità di modalità in incognito, il tuo nome mostrato sarà diverso per ogni contatto, evitando dati condivisi tra di loro.", "simplex-network-overlay-card-1-li-2": "Il design di SimpleX, a differenza della maggior parte delle reti P2P, non ha identificatori utente globali di alcun tipo, nemmeno temporanei, e usa solo identificatori temporanei a coppie, garantendo una maggiore protezione dell'anonimato e dei metadati.", - "simplex-network-overlay-card-1-li-4": "Le implementazioni P2P possono essere bloccate da alcuni fornitori di internet (come BitTorrent). SimpleX è indipendente dal trasporto - può funzionare su protocolli web standard, es. WebSocket.", + "simplex-network-overlay-card-1-li-4": "Le implementazioni P2P possono essere bloccate da alcuni fornitori di internet (come BitTorrent). SimpleX è indipendente dal trasporto — può funzionare su protocolli web standard, es. WebSocket.", "hero-overlay-card-2-p-1": "Quando gli utenti hanno identità permanenti, anche se si tratta solo di un numero casuale, come un Session ID, c'è il rischio che il fornitore o un malintenzionato possano osservare come gli utenti sono connessi e quanti messaggi inviano.", "simplex-network-overlay-card-1-li-6": "Le reti P2P possono essere vulnerabili all'attacco DRDoS, quando i client possono ritrasmettere e amplificare il traffico, con conseguente \"denial of service\" a livello di rete. I client SimpleX si limitano a inoltrare il traffico da una connessione nota e non possono essere usati da un aggressore per amplificare il traffico nell'intera rete.", - "privacy-matters-overlay-card-1-p-4": "La piattaforma SimpleX protegge la privacy delle tue connessioni meglio di qualsiasi alternativa, impedendo completamente che il tuo grafico sociale sia disponibile a qualsiasi azienda o organizzazione. Anche quando le persone usano i server forniti da SimpleX Chat, non conosciamo il numero di utenti o le loro connessioni.", - "privacy-matters-overlay-card-2-p-3": "SimpleX è la prima piattaforma che non ha alcun identificatore utente per design, proteggendo così il tuo grafico delle connessioni meglio di qualsiasi alternativa conosciuta.", + "privacy-matters-overlay-card-1-p-4": "La rete di SimpleX protegge la privacy delle tue connessioni meglio di qualsiasi alternativa, impedendo completamente che il tuo grafico sociale sia disponibile a qualsiasi azienda o organizzazione. Anche quando le persone usano i server preconfigurati in SimpleX Chat, gli operatori dei server non conoscono il numero di utenti o le loro connessioni.", + "privacy-matters-overlay-card-2-p-3": "SimpleX è la prima rete che non ha alcun identificatore utente per design, proteggendo così il tuo grafico delle connessioni meglio di qualsiasi alternativa conosciuta.", "privacy-matters-overlay-card-3-p-1": "Tutti dovrebbero preoccuparsi della privacy e della sicurezza delle proprie comunicazioni — conversazioni innocue possono metterti in pericolo, anche se non hai nulla da nascondere.", "privacy-matters-overlay-card-3-p-3": "Le persone comuni vengono arrestate per ciò che condividono online, anche tramite i loro account \"anonimi\", anche nei Paesi democratici.", - "simplex-unique-overlay-card-1-p-3": "Questo design protegge la privacy di chi stai comunicando, nascondendola ai server della piattaforma SimpleX e a qualsiasi osservatore. Per nascondere il tuo indirizzo IP ai server, puoi connetterti ai server SimpleX tramite Tor.", - "simplex-unique-overlay-card-2-p-1": "Poiché non hai alcun identificatore sulla piattaforma SimpleX, nessuno può contattarti a meno che tu non condivida un indirizzo utente una tantum o temporaneo, come un codice QR o un link.", + "simplex-unique-overlay-card-1-p-3": "Questo design protegge la privacy di chi stai comunicando, nascondendola ai server della rete SimpleX e a qualsiasi osservatore. Per nascondere il tuo indirizzo IP ai server, puoi connetterti ai server SimpleX tramite Tor.", + "simplex-unique-overlay-card-2-p-1": "Poiché non hai alcun identificatore sulla rete SimpleX, nessuno può contattarti a meno che tu non condivida un indirizzo utente una tantum o temporaneo, come un codice QR o un link.", "simplex-unique-overlay-card-2-p-2": "Anche l'indirizzo utente opzionale, che può essere usato per inviare richieste di contatto spam, è possibile modificarlo o eliminarlo completamente senza perdere alcuna connessione.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat conserva tutti i dati utente solo sui dispositivi client usando un formato trasferibile di database crittografato che può essere esportato e trasferito su qualsiasi dispositivo supportato.", "simplex-unique-overlay-card-3-p-2": "I messaggi crittografati end-to-end vengono conservati temporaneamente sui server di inoltro SimpleX fino alla ricezione, quindi vengono eliminati definitivamente.", - "simplex-unique-overlay-card-4-p-1": "Puoi usare SimpleX con i tuoi server personali e continuare a comunicare con le persone che usano i server preconfigurati forniti da noi.", - "simplex-unique-overlay-card-4-p-3": "Se stai pensando di sviluppare per la piattaforma SimpleX, ad esempio, il chat bot per gli utenti dell'app SimpleX o l'integrazione della libreria SimpleX Chat nelle tue app mobili, contattaci per qualsiasi consiglio e supporto.", - "simplex-unique-card-1-p-1": "SimpleX protegge la privacy del tuo profilo, contatti e metadati, nascondendoli ai server della piattaforma SimpleX e ad eventuali osservatori.", - "simplex-unique-card-1-p-2": "A differenza di qualsiasi altra piattaforma di messaggistica esistente, SimpleX non ha identificatori assegnati agli utenti — nemmeno numeri casuali.", - "simplex-unique-card-2-p-1": "Poiché non hai un identificatore o un indirizzo fisso sulla piattaforma SimpleX, nessuno può contattarti a meno che tu non condivida un indirizzo utente una tantum o temporaneo, come codice un QR o un link.", + "simplex-unique-overlay-card-4-p-1": "Puoi usare SimpleX con i tuoi server personali e continuare a comunicare con le persone che usano i server preconfigurati nelle app.", + "simplex-unique-overlay-card-4-p-3": "Se stai pensando di sviluppare per la rete SimpleX, ad esempio, il chat bot per gli utenti dell'app SimpleX o l'integrazione della libreria SimpleX Chat nelle tue app mobili, contattaci per qualsiasi consiglio e supporto.", + "simplex-unique-card-1-p-1": "SimpleX protegge la privacy del tuo profilo, contatti e metadati, nascondendoli ai server della rete SimpleX e ad eventuali osservatori.", + "simplex-unique-card-1-p-2": "A differenza di qualsiasi altra rete di messaggistica esistente, SimpleX non ha identificatori assegnati agli utenti — nemmeno numeri casuali.", + "simplex-unique-card-2-p-1": "Poiché non hai un identificatore o un indirizzo fisso sulla rete SimpleX, nessuno può contattarti a meno che tu non condivida un indirizzo utente una tantum o temporaneo, come codice un QR o un link.", "simplex-unique-card-3-p-1": "SimpleX conserva tutti i dati utente sui dispositivi client in un formato trasferibile di database crittografato — può essere trasferito su un altro dispositivo.", "join": "Unisciti a", "we-invite-you-to-join-the-conversation": "Ti invitiamo a unirti alla conversazione", "enter-your-email-address": "Inserisci il tuo indirizzo email", "get-simplex": "Ottieni SimpleX desktop app", - "why-simplex-is": "Perché SimpleX è", - "unique": "unico", + "why-simplex-is-unique": "Perché SimpleX è unico", "learn-more": "Maggiori informazioni", "more-info": "Mostra info", "hide-info": "Nascondi info", @@ -103,7 +102,7 @@ "use-this-command": "Usa questo comando:", "see-simplex-chat": "Vedi SimpleX Chat", "github-repository": "Repository GitHub", - "the-instructions--source-code": "le istruzioni su come scaricarlo o compilarlo dal codice sorgente.", + "the-instructions--source-code": "per le istruzioni su come scaricarlo o compilarlo dal codice sorgente.", "if-you-already-installed-simplex-chat-for-the-terminal": "Se hai già installato SimpleX Chat per il terminale", "if-you-already-installed": "Se hai già installato", "privacy-matters-section-subheader": "Preservare la privacy dei tuoi metadati — con chi parli — ti protegge da:", @@ -131,13 +130,13 @@ "no-federated": "No - federato", "comparison-section-list-point-1": "Solitamente si basa su un numero di telefono, in alcuni casi su nomi utente", "comparison-section-list-point-2": "Indirizzi basati su DNS", - "comparison-section-list-point-6": "Sebbene i P2P siano distribuiti, non sono federati - operano come un'unica rete", + "comparison-section-list-point-6": "Sebbene i P2P siano distribuiti, non sono federati — operano come un'unica rete", "comparison-section-list-point-7": "Le reti P2P hanno un'autorità centrale o l'intera rete può essere compromessa", "see-here": "vedi qui", "simplex-network-3-header": "Nella rete di SimpleX", "simplex-private-card-7-point-2": "Se un messaggio viene aggiunto, rimosso o modificato, il destinatario verrà avvisato.", - "hero-overlay-card-1-p-2": "Per consegnare i messaggi, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX usa identificatori temporanei anonimi a coppie delle code di messaggi, separati per ciascuna delle tue connessioni — non ci sono identificatori a lungo termine.", - "simplex-private-card-6-point-1": "Molte piattaforme di comunicazione sono vulnerabili agli attacchi MITM da parte di server o fornitori di rete.", + "hero-overlay-card-1-p-2": "Per consegnare i messaggi, invece degli ID utente utilizzati da tutte le altre reti, SimpleX usa identificatori temporanei anonimi a coppie delle code di messaggi, separati per ciascuna delle tue connessioni — non ci sono identificatori a lungo termine.", + "simplex-private-card-6-point-1": "Molte reti di comunicazione sono vulnerabili agli attacchi MITM da parte di server o fornitori di rete.", "simplex-private-card-7-point-1": "Per garantire l'integrità, i messaggi sono numerati in sequenza e includono l'hash del messaggio precedente.", "simplex-private-card-9-point-2": "Riduce i vettori di attacco, rispetto ai broker di messaggi tradizionali, e i metadati disponibili.", "simplex-private-card-10-point-2": "Ciò consente di recapitare messaggi senza identificatori del profilo utente, garantendo una migliore privacy dei metadati rispetto alle alternative.", @@ -162,7 +161,7 @@ "feature-3-title": "Gruppi decentralizzati crittografati E2E — solo gli utenti sanno che esistono", "blog": "Blog", "simplex-explained-tab-2-p-1": "Per ogni connessione usi due code di messaggi distinte per inviare e ricevere i messaggi attraverso server diversi.", - "simplex-explained-tab-2-p-2": "I server passano i messaggi solo in una direzione, senza avere il quadro completo della conversazione dell'utente o delle connessioni.", + "simplex-explained-tab-2-p-2": "I server passano i messaggi solo in una direzione, senza avere il quadro completo delle conversazioni dell'utente o delle connessioni.", "simplex-explained-tab-3-p-2": "Gli utenti possono aumentare ulteriormente la privacy dei metadati usando Tor per accedere ai server, evitando correlazioni per indirizzo IP.", "smp-protocol": "Protocollo SMP", "feature-1-title": "Messaggi crittografati E2E con markdown e modifica", @@ -182,21 +181,21 @@ "privacy-matters-overlay-card-1-p-2": "I rivenditori online sanno che le persone con redditi più bassi sono più propense a fare acquisti urgenti, quindi possono applicare prezzi più alti o rimuovere sconti.", "privacy-matters-overlay-card-1-p-3": "Alcune società finanziarie e assicurative usano grafici sociali per determinare i tassi di interesse e i premi. Spesso ciò fa pagare di più le persone con redditi più bassi — è noto come \"premio di povertà\" .", "privacy-matters-overlay-card-2-p-1": "Non molto tempo fa abbiamo assistito alla manipolazione delle principali elezioni da una rispettabile società di consulenza che ha usato i nostri grafici sociali per distorcere la nostra visione del mondo reale e manipolare i nostri voti.", - "privacy-matters-overlay-card-2-p-2": "Per essere obiettivi e prendere decisioni indipendenti devi avere il controllo del tuo spazio informativo. È possibile solo se utilizzi una piattaforma di comunicazione privata che non ha accesso al tuo grafico sociale.", + "privacy-matters-overlay-card-2-p-2": "Per essere obiettivi e prendere decisioni indipendenti devi avere il controllo del tuo spazio informativo. È possibile solo se utilizzi una rete di comunicazione privata che non ha accesso al tuo grafico sociale.", "privacy-matters-overlay-card-3-p-2": "Una delle storie più scioccanti è l'esperienza di Mohamedou Ould Salahi descritta nel suo libro di memorie e mostrata nel film The Mauritanian. È stato rinchiuso nel campo di Guantánamo, senza processo, e lì è stato torturato per 15 anni dopo una telefonata a un suo parente in Afghanistan, sospettato di essere coinvolto negli attacchi dell'11/9, nonostante avesse vissuto in Germania per i precedenti 10 anni.", "join-us-on-GitHub": "Unisciti a noi su GitHub", "simplex-chat-for-the-terminal": "SimpleX Chat per il terminale", "privacy-matters-overlay-card-3-p-4": "Non è sufficiente usare un messenger crittografato end-to-end, tutti dovremmo usare i messenger che proteggono la privacy delle nostre reti personali — con chi siamo connessi.", - "simplex-unique-overlay-card-1-p-2": "Per recapitare i messaggi, SimpleX usa indirizzi anonimi a coppie di code di messaggi unidirezionali, separate per i messaggi ricevuti e inviati, di solito tramite server diversi. Usare SimpleX è come avere un'email o telefono “temporanei” per ogni contatto, e nessuna seccatura per gestirli.", - "simplex-unique-overlay-card-1-p-1": "A differenza di altre piattaforme di messaggistica, SimpleX non ha alcun identificatore assegnato agli utenti. Non si basa su numeri di telefono, indirizzi basati su domini (come email o XMPP), nomi utente, chiavi pubbliche o persino numeri casuali per identificare i suoi utenti — non sappiamo quante persone usano i nostri server SimpleX.", + "simplex-unique-overlay-card-1-p-2": "Per recapitare i messaggi, SimpleX usa indirizzi anonimi a coppie di code di messaggi unidirezionali, separate per i messaggi ricevuti e inviati, di solito tramite server diversi.", + "simplex-unique-overlay-card-1-p-1": "A differenza di altre reti di messaggistica, SimpleX non ha alcun identificatore assegnato agli utenti. Non si basa su numeri di telefono, indirizzi basati su domini (come email o XMPP), nomi utente, chiavi pubbliche o persino numeri casuali per identificare i suoi utenti — gli operatori dei server non sanno quante persone usano i loro server.", "simplex-unique-overlay-card-3-p-3": "A differenza dei server di reti federate (email, XMPP o Matrix), i server SimpleX non conservano gli account utente, ma trasmettono solo i messaggi, proteggendo la privacy di entrambe le parti.", "simplex-unique-card-3-p-2": "I messaggi crittografati end-to-end vengono conservati temporaneamente sui server di inoltro SimpleX fino alla ricezione, quindi vengono eliminati definitivamente.", "simplex-unique-card-4-p-2": "Puoi usare SimpleX con i tuoi server personali o con i server forniti da noi — e connetterti comunque a qualsiasi utente.", "join-the-REDDIT-community": "Unisciti alla comunità di REDDIT", "sign-up-to-receive-our-updates": "Iscriviti per ricevere i nostri aggiornamenti", "simplex-unique-overlay-card-3-p-4": "Non ci sono identificatori o testi cifrati in comune tra il traffico del server inviato e quello ricevuto — se qualcuno lo osserva, non può determinare facilmente chi comunica con chi, anche se il TLS è compromesso.", - "simplex-unique-overlay-card-4-p-2": "La piattaforma SimpleX usa un protocollo aperto e fornisce un SDK per creare chat bot, consentendo l'implementazione di servizi con cui gli utenti possono interagire tramite le app SimpleX Chat — siamo impazienti di vedere quali servizi SimpleX puoi creare.", - "simplex-unique-card-4-p-1": "La rete SimpleX è completamente decentralizzata e indipendente da qualsiasi criptovaluta o altra piattaforma, ad eccezione di internet.", + "simplex-unique-overlay-card-4-p-2": "La rete di SimpleX usa un protocollo aperto e fornisce un SDK per creare chat bot, consentendo l'implementazione di servizi con cui gli utenti possono interagire tramite le app SimpleX Chat — siamo impazienti di vedere quali servizi SimpleX creerai.", + "simplex-unique-card-4-p-1": "La rete SimpleX è completamente decentralizzata e indipendente da qualsiasi criptovaluta o altra rete, ad eccezione di internet.", "donate-here-to-help-us": "Dona qui per aiutarci", "to-make-a-connection": "Per stabilire una connessione:", "scan-the-qr-code-with-the-simplex-chat-app-description": "Le chiavi pubbliche e l'indirizzo della coda di messaggi in questo link NON vengono inviati in rete quando vedi questa pagina —
sono contenuti nel frammento hash dell'URL del link.", @@ -219,7 +218,7 @@ "guide-dropdown-7": "Privacy e sicurezza", "guide-dropdown-9": "Creare connessioni", "guide": "Guida", - "docs-dropdown-1": "Piattaforma SimpleX", + "docs-dropdown-1": "Rete di SimpleX", "docs-dropdown-2": "Accesso ai file Android", "docs-dropdown-3": "Accesso al database di chat", "docs-dropdown-4": "Ospita un server SMP", diff --git a/website/langs/ja.json b/website/langs/ja.json index 6f994b59da..05365e2271 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -52,7 +52,7 @@ "chat-protocol": "チャットプロトコル", "chat-bot-example": "チャットボットの例", "donate": "寄付", - "copyright-label": "© 2020-2024 SimpleX | Open-Source Project", + "copyright-label": "© 2020-2025 SimpleX | Open-Source Project", "hero-p-1": "他のアプリにはユーザー ID があります: Signal、Matrix、Session、Briar、Jami、Cwtch など。
SimpleX にはありません。乱数さえもありません
これにより、プライバシーが大幅に向上します。", "copy-the-command-below-text": "以下のコマンドをコピーしてチャットで使用します:", "simplex-private-card-9-point-1": "各メッセージ キューは、異なる送信アドレスと受信アドレスを使用してメッセージを一方向に渡します。", @@ -193,13 +193,12 @@ "simplex-unique-overlay-card-3-p-2": "エンドツーエンドで暗号化されたメッセージは、SimpleXのリレーサーバーで受信するまで一時的に保持され、その後永久に削除されます。", "simplex-private-card-7-point-1": "整合性を保証するために、メッセージには連続した番号が付けられ、前のメッセージのハッシュが含まれます。", "contact-hero-p-2": "SimpleX Chat をまだダウンロードしていませんか?", - "why-simplex-is": "なぜSimpleXなのか", + "why-simplex-is-unique": "なぜSimpleXなのか唯一", "simplex-network-section-header": "SimpleX ネットワーク", "simplex-private-10-title": "一時的な匿名のペア識別子", "privacy-matters-1-overlay-1-linkText": "プライバシーの保護はコストを削減します", "tap-the-connect-button-in-the-app": "アプリの 「接続」 ボタンをタップします", "comparison-section-list-point-4a": "SimpleX リレーは e2e 暗号化を侵害できません。 セキュリティ コードを検証して帯域外チャネルへの攻撃を軽減します", - "unique": "唯一", "simplex-network-1-overlay-linktext": "P2Pネットワークの問題点", "no-private": "いいえ - プライベート", "simplex-unique-1-title": "プライバシーが完全に守られます", @@ -246,13 +245,15 @@ "simplex-private-5-title": "何レイヤーもの
コンテンツパディング", "hero-overlay-card-3-p-1": "Trail of Bitsは、大手ハイテク企業、政府機関、主要なブロックチェーン・プロジェクトなどを顧客に持つ、セキュリティとテクノロジーの大手コンサルタント会社です。", "jobs": "チームに参加する", - "hero-overlay-3-textlink": "セキュリティ監査", - "hero-overlay-3-title": "セキュリティ監査", + "hero-overlay-3-textlink": "セキュリティ評価", + "hero-overlay-3-title": "セキュリティ評価", "hero-overlay-card-3-p-2": "Trail of Bitsは2022年11月にSimpleXプラットフォームの暗号とネットワークのコンポーネントを検証しました。詳しくは お知らせをご覧ください。", "docs-dropdown-9": "ダウンロード", "please-enable-javascript": "QRコードを表示するためにJavaScriptを有効にしてください。", "please-use-link-in-mobile-app": "このリンクをモバイルアプリで使用してください", "docs-dropdown-10": "透明性", "docs-dropdown-11": "よくある質問", - "docs-dropdown-12": "セキュリティ" + "docs-dropdown-12": "セキュリティ", + "docs-dropdown-14": "ビジネス向けSimpleX", + "hero-overlay-card-3-p-3": "Trail of Bits は 2024 年 7 月に SimpleX ネットワーク プロトコルの暗号設計をレビューしました。" } diff --git a/website/langs/nl.json b/website/langs/nl.json index 4db6126c2c..30e751c9f9 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -7,7 +7,7 @@ "why-simplex": "Waarom SimpleX", "simplex-privacy": "SimpleX privacy", "simplex-network": "SimpleX netwerk", - "simplex-explained": "Simplex uitgelegd", + "simplex-explained": "SimpleX uitgelegd", "simplex-explained-tab-1-text": "1. Wat gebruikers ervaren", "simplex-explained-tab-3-text": "3. Wat servers zien", "simplex-explained-tab-1-p-1": "U kunt contacten en groepen maken en tweerichtings gesprekken voeren, zoals in elke andere messenger.", @@ -17,7 +17,7 @@ "chat-bot-example": "Chatbot voorbeeld", "smp-protocol": "SMP protocol", "donate": "Doneer", - "copyright-label": "© 2020-2024 SimpleX | Open-sourceproject", + "copyright-label": "© 2020-2025 SimpleX | Open-sourceproject", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Privacybeleid", @@ -116,8 +116,7 @@ "join-us-on-GitHub": "Volg ons op GitHub", "donate-here-to-help-us": "Doneer hier om ons te helpen", "sign-up-to-receive-our-updates": "Schrijf u in om onze updates te ontvangen", - "unique": "uniek", - "why-simplex-is": "Waarom is SimpleX", + "why-simplex-is-unique": "Waarom is SimpleX uniek", "learn-more": "Kom meer te weten", "more-info": "Meer informatie", "hide-info": "Info verbergen", @@ -183,7 +182,7 @@ "privacy-matters-overlay-card-3-p-2": "Een van de meest schokkende verhalen is de ervaring van Mohamedou Ould Salahi beschreven in zijn memoires en getoond in The Mauritanian movie. Hij werd zonder proces in het Guantanamo kamp geplaatst en werd daar 15 jaar lang gemarteld na een telefoontje naar zijn familielid in Afghanistan, op verdenking van betrokkenheid bij aanslagen van 9/11, ook al woonde hij de afgelopen 10 jaar in Duitsland.", "privacy-matters-overlay-card-3-p-3": "Gewone mensen worden gearresteerd voor wat ze online delen, zelfs via hun 'anonieme' accounts, zelfs in democratische landen.", "privacy-matters-overlay-card-3-p-4": "Het is niet genoeg om een end-to-end versleutelde messenger te gebruiken, we zouden allemaal de messengers moeten gebruiken die de privacy van onze persoonlijke netwerken beschermen — met wie we verbonden zijn.", - "simplex-unique-overlay-card-1-p-2": "Om berichten af te leveren gebruikt SimpleX paarsgewijze anonieme adressen van unidirectionele berichten wachtrijen, gescheiden voor ontvangen en verzonden berichten, meestal via verschillende servers. Het gebruik van SimpleX is als het hebben van een andere “brander” e-mail of telefoon voor elk contact, en geen gedoe om ze te beheren.", + "simplex-unique-overlay-card-1-p-2": "Om berichten af te leveren gebruikt SimpleX paarsgewijze anonieme adressen van unidirectionele berichten wachtrijen, gescheiden voor ontvangen en verzonden berichten, meestal via verschillende servers.", "simplex-unique-overlay-card-1-p-3": "Dit ontwerp beschermt de privacy van met wie u communiceert en verbergt deze voor SimpleX platform servers en voor waarnemers. Om uw IP-adres voor de servers te verbergen, kunt u verbinding maken met SimpleX servers via Tor .", "simplex-unique-overlay-card-2-p-1": "Omdat je geen identificatie hebt op het SimpleX platform, kan niemand contact met je opnemen, tenzij je een eenmalig of tijdelijk gebruikers adres deelt, als een QR-code of een link.", "simplex-unique-overlay-card-2-p-2": "Zelfs met het optionele gebruikers adres, hoewel het kan worden gebruikt om spam-contact verzoeken te verzenden, kunt u het wijzigen of volledig verwijderen zonder uw verbindingen te verliezen.", diff --git a/website/langs/pl.json b/website/langs/pl.json index a25155e018..d45207a709 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -15,7 +15,7 @@ "smp-protocol": "Protokół SMP", "chat-protocol": "Protokół czatu", "donate": "Darowizna", - "copyright-label": "© 2020-2024 SimpleX | Projekt Open-Source", + "copyright-label": "© 2020-2025 SimpleX | Projekt Open-Source", "simplex-chat-protocol": "Protokół SimpleX Chat", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Polityka prywatności", @@ -140,7 +140,7 @@ "simplex-network-overlay-card-1-li-6": "Sieci P2P mogą być podatne na atak DRDoS, kiedy klienci mogą rozgłaszać i wzmacniać ruch, co powoduje odmowę usługi w całej sieci. Klienci SimpleX przekazują jedynie ruch ze znanego połączenia i nie mogą być wykorzystani przez atakującego do wzmocnienia ruchu w całej sieci.", "privacy-matters-overlay-card-1-p-1": "Wiele dużych firm wykorzystuje informacje o tym, z kim jesteś połączony, aby oszacować Twoje dochody, sprzedać Ci produkty, których tak naprawdę nie potrzebujesz, oraz ustalić ceny.", "simplex-unique-overlay-card-2-p-2": "Nawet w przypadku opcjonalnego adresu użytkownika, podczas gdy może on być używany do wysyłania spamowych zapytań o kontakt, możesz go zmienić lub całkowicie usunąć bez utraty jakichkolwiek połączeń.", - "simplex-unique-overlay-card-1-p-2": "Do dostarczania wiadomości SimpleX używa parami anonimowych adresów jednokierunkowych kolejek z wiadomościami, oddzielnych dla wiadomości odbieranych i wysyłanych, zwykle przez różne serwery. Korzystanie z SimpleX jest jak posiadanie wielu “jednorazowych” emailów lub osobnego telefonu dla każdego kontaktu i braku problemów z zarządzaniem nimi.", + "simplex-unique-overlay-card-1-p-2": "Do dostarczania wiadomości SimpleX używa parami anonimowych adresów jednokierunkowych kolejek z wiadomościami, oddzielnych dla wiadomości odbieranych i wysyłanych, zwykle przez różne serwery.", "simplex-unique-overlay-card-1-p-3": "Taka konstrukcja chroni prywatność tego, z kim się komunikujesz, ukrywając ją przed serwerami platformy SimpleX i przed wszelkimi obserwatorami. Aby ukryć swój adres IP przed serwerami, możesz połączyć się z serwerami SimpleX za pośrednictwem sieci Tor.", "simplex-unique-overlay-card-3-p-4": "Nie ma żadnych identyfikatorów ani szyfrogramów wspólnych między wysyłanym i odbieranym ruchem serwera — jeśli ktokolwiek to obserwuje, nie może łatwo określić, kto komunikuje się z kim, nawet jeśli bezpieczeństwo protokołu TLS zostało zagrożone.", "simplex-unique-overlay-card-2-p-1": "Ponieważ nie masz identyfikatora na platformie SimpleX, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku.", @@ -154,13 +154,12 @@ "simplex-unique-card-1-p-2": "W przeciwieństwie do każdej innej istniejącej platformy komunikacyjnej, SimpleX nie ma żadnych identyfikatorów przypisanych do użytkowników — nawet losowych liczb.", "tap-the-connect-button-in-the-app": "Stuknij przycisk 'połącz się' w aplikacji", "join-the-REDDIT-community": "Dołącz do społeczności REDDIT", - "unique": "unikalny", "hide-info": "Ukryj informacje", "simplex-unique-card-4-p-2": "Możesz używać SimpleX z własnymi serwerami lub z serwerami dostarczonymi przez nas — i nadal łączyć się z dowolnym użytkownikiem.", "we-invite-you-to-join-the-conversation": "Zapraszamy do udziału w rozmowie", "enter-your-email-address": "Wpisz swój adres e-mail", "get-simplex": "Pobierz SimpleX desktop app", - "why-simplex-is": "Dlaczego SimpleX jest", + "why-simplex-is-unique": "Dlaczego SimpleX jest unikalny", "join": "Dołącz do", "join-us-on-GitHub": "Dołącz do nas na GitHubie", "sign-up-to-receive-our-updates": "Zapisz się, aby otrzymywać nasze aktualizacje", diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index 73095c6db2..77854f5be1 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -11,7 +11,7 @@ "simplex-explained-tab-1-p-1": "Você pode criar contatos e grupos e ter conversas bidirecionais, como em qualquer outro mensageiro.", "simplex-explained-tab-2-p-1": "Para cada conexão, são usadas duas filas de mensagens separadas para enviar e receber mensagens por meio de servidores diferentes.", "hero-p-1": "Outros aplicativos possuem IDs de usuário: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.
O SimpleX não tem, nem mesmo números aleatórios.
Isso melhora radicalmente a sua privacidade.", - "terms-and-privacy-policy": "Termos e Política de Privacidade", + "terms-and-privacy-policy": "Termos de serviço & Política de Privacidade", "hero-2-header-desc": "O vídeo mostra como você se conecta com seu amigo através do QR code de uso único dele, pessoalmente ou via link de vídeo. Você também pode se conectar compartilhando um link de convite.", "feature-7-title": "Armazenamento do aplicativo criptografado portátil — mova o perfil para outro dispositivo", "simplex-explained": "Explicação do SimpleX", @@ -25,7 +25,7 @@ "smp-protocol": "Protocolo SMP", "chat-protocol": "Protocolo de bate-papo", "donate": "Doar", - "copyright-label": "© 2020-2024 SimpleX | Projeto de Código Livre", + "copyright-label": "© 2020-2025 SimpleX | Projeto de Código Livre", "simplex-chat-protocol": "Protocolo Chat SimpleX", "terminal-cli": "CLI Terminal", "hero-header": "Privacidade redefinida", @@ -127,7 +127,7 @@ "privacy-matters-overlay-card-2-p-3": "O SimpleX é a primeira plataforma que não tem nenhum identificador de usuário por design, protegendo assim seu gráfico de conexões melhor do que qualquer alternativa conhecida.", "privacy-matters-overlay-card-3-p-3": "Pessoas comuns são presas pelo que compartilham online, mesmo por meio de suas contas \"anônimas\", mesmo em países democráticos.", "privacy-matters-overlay-card-3-p-4": "Não basta usar um mensageiro criptografado de ponta-a-ponta, todos nós devemos usar os mensageiros que protegem a privacidade de nossas redes pessoais — com quem estamos conectados.", - "simplex-unique-overlay-card-1-p-2": "Para entregar mensagens, o SimpleX usa endereços anônimos em pares de filas de mensagens unidirecionais, separadas para mensagens recebidas e enviadas, geralmente por meio de servidores diferentes. Usar o SimpleX é como ter um email ou telefone de “gravação” diferente para cada contato, sem a necessidade de gerenciá-los.", + "simplex-unique-overlay-card-1-p-2": "Para entregar mensagens, o SimpleX usa endereços anônimos em pares de filas de mensagens unidirecionais, separadas para mensagens recebidas e enviadas, geralmente por meio de servidores diferentes.", "simplex-unique-overlay-card-1-p-3": "Esse design protege a privacidade de quem está se comunicando com você, ocultando-a dos servidores da plataforma SimpleX e de quaisquer observadores. Para ocultar seu endereço IP dos servidores, você pode se conectar aos servidores do SimpleX via Tor.", "simplex-unique-overlay-card-2-p-2": "Mesmo com o endereço de usuário opcional, embora ele possa ser usado para enviar solicitações de contato de spam, você pode alterá-lo ou excluí-lo completamente sem perder nenhuma das suas conexões.", "simplex-unique-overlay-card-2-p-1": "Como você não tem um identificador na plataforma SimpleX, ninguém pode entrar em contato com você, a menos que compartilhe um endereço de usuário único ou temporário, como um QR code ou um link.", @@ -147,8 +147,7 @@ "sign-up-to-receive-our-updates": "Inscreva-se para receber nossas atualizações", "enter-your-email-address": "Digite seu endereço de email", "get-simplex": "Obtenha o SimpleX aplicativo desktop", - "why-simplex-is": "Por que o SimpleX é", - "unique": "único", + "why-simplex-is-unique": "Por que o SimpleX é único", "learn-more": "Saiba mais", "more-info": "Mais informações", "contact-hero-header": "Você recebeu um endereço para se conectar no SimpleX Chat", @@ -242,9 +241,9 @@ "simplex-chat-repo": "Repositório Simplex Chat", "f-droid-org-repo": "Repositório F-Droid.org", "stable-versions-built-by-f-droid-org": "Versões estáveis criadas por F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "Os lançamentos para este repositório são feitos 1-2 dias depois", - "hero-overlay-3-textlink": "Avaliação Segura", - "hero-overlay-3-title": "Avaliação Segura", + "releases-to-this-repo-are-done-1-2-days-later": "Os lançamentos para este repositório são feitos 1 ou 2 dias depois", + "hero-overlay-3-textlink": "Avaliações de segurança", + "hero-overlay-3-title": "Avaliações de segurança", "hero-overlay-card-3-p-1": "Trail of Bits é uma consultoria líder em segurança e tecnologia cujos clientes incluem grandes empresas de tecnologia, agências governamentais e grandes projetos de blockchain.", "hero-overlay-card-3-p-2": "Trail of Bits analisou a criptografia da plataforma SimpleX e os componentes de rede em novembro de 2022. Leia mais em o anúncio.", "f-droid-page-f-droid-org-repo-section-text": "Os repositórios SimpleX Chat e F-Droid.org assinam compilações com chaves diferentes. Para mudar, exporte o banco de dados de bate-papo e reinstale o aplicativo.", @@ -254,5 +253,7 @@ "docs-dropdown-9": "Baixar", "docs-dropdown-11": "FAQ", "docs-dropdown-10": "Transparência", - "docs-dropdown-12": "Segurança" + "docs-dropdown-12": "Segurança", + "hero-overlay-card-3-p-3": "Trail of Bits revisou o design criptografico das redes utilizadas pelo SimpleX em julho de 2024", + "docs-dropdown-14": "SimpleX para negócios" } diff --git a/website/langs/ru.json b/website/langs/ru.json index 5d59ec2c76..335b19ab11 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -1,259 +1,259 @@ { "copy-the-command-below-text": "скопируйте приведенную ниже команду и используйте ее в чате:", - "copyright-label": "© 2020-2024 SimpleX | Проект с открытым исходным кодом", + "copyright-label": "© 2020-2025 SimpleX | Проект с открытым исходным кодом", "chat-bot-example": "Пример Чат бота", "simplex-private-card-9-point-1": "Каждая очередь сообщений передает сообщения в одном направлении с разными адресами отправки и получения.", - "simplex-private-card-1-point-2": "Криптобокс NaCL в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометации TLS.", - "contact-hero-p-1": "Открытые ключи и адрес очереди сообщений по этой ссылке НЕ отправляются по сети при просмотре этой страницы — они содержатся в хэш-фрагменте URL-адреса ссылки.", + "simplex-private-card-1-point-2": "NaCL cryptobox в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометированного TLS.", + "contact-hero-p-1": "Публичные ключи и адрес очереди сообщений в этой ссылке НЕ отправляются по сети при просмотре этой страницы — они содержатся в хэш-фрагменте URL-адреса ссылки.", "guide-dropdown-5": "Управление данными", "scan-the-qr-code-with-the-simplex-chat-app": "Отсканируйте QR-код с помощью приложения SimpleX Chat", - "simplex-private-card-9-point-2": "Это уменьшает векторы атак и доступные метаданные, по сравнению с традиционными посредниками для доставки сообщений.", + "simplex-private-card-9-point-2": "Это уменьшает векторы атак и доступные метаданные, по сравнению с традиционными серверами доставки сообщений.", "simplex-unique-card-3-p-2": "Сквозные зашифрованные сообщения временно хранятся на серверах SimpleX до получения, после чего они удаляются безвозвратно.", - "feature-7-title": "Портативное, зашифрованное хранилище в приложении — можно перенести профиль на другое устройство", - "no-federated": "Нет - федеративный", - "hero-2-header": "Как начать общаться приватно", + "feature-7-title": "Зашифрованное база данных — Вы можете перенести профиль на другое устройство", + "no-federated": "Нет - федеративные", + "hero-2-header": "Установите конфиденциальное соединение", "simplex-unique-overlay-card-3-p-3": "В отличие от серверов федеративных сетей (электронной почты, XMPP или Matrix), серверы SimpleX не хранят учетные записи пользователей, они только ретранслируют сообщения, защищая конфиденциальность обеих сторон.", - "hero-subheader": "Первый мессенджер
не нуждающийся в идентификаторах
пользователя", - "privacy-matters-overlay-card-3-p-2": "Одна из самых шокирующих историй - это опыт Слахи, Мохаммеда Ульда, описанный в его мемуарах и показанный в фильме Мавританец. Он был помещен в лагерь Гуантанамо без суда и следствия и подвергался там пыткам в течение 15 лет после телефонного звонка своему родственнику в Афганистан под подозревается в причастности к терактам 11 сентября, хотя предыдущие 10 лет он жил в Германии.", - "signing-key-fingerprint": "Отпечаток ключа подписи (SHA-256)", + "hero-subheader": "Первый мессенджер
без идентификаторов пользователей", + "privacy-matters-overlay-card-3-p-2": "Одна из самых шокирующих историй — это опыт Слахи, Мохаммеда Ульда, описанный в его мемуарах и показанный в фильме Мавританец. Он был помещен в лагерь Гуантанамо без суда и следствия и подвергался там пыткам в течение 15 лет после телефонного звонка своему родственнику в Афганистане, из за подозрения в причастности к терактам 11 сентября, хотя предыдущие 10 лет он жил в Германии.", + "signing-key-fingerprint": "Идентификатор ключа подписи (SHA-256)", "simplex-network-2-desc": "Серверные узлы SimpleX НЕ хранят профили пользователей, контакты и доставленные сообщения, НЕ подключаются друг к другу, и НЕ имеют каталога серверов.", "simplex-privacy": "Конфиденциальность SimpleX", "docs-dropdown-5": "Свой XFTP Сервер", - "simplex-private-card-3-point-2": "Отпечаток сервера и привязка канала предотвращают MITM атаки и Атаки повторного воспроизведения.", + "simplex-private-card-3-point-2": "Идентификатор сервера и привязка к TLS сессии предотвращают атаки перехвата (MITM) и повторного использования.", "docs-dropdown-3": "Доступ к в базе данных чата", "installing-simplex-chat-to-terminal": "Установка SimpleX Chat в терминале", "use-this-command": "Используйте эту команду:", - "simplex-explained": "Простое объяснение вкратце", + "simplex-explained": "Как SimpleX работает", "to-make-a-connection": "Чтобы установить соединение:", - "comparison-section-list-point-6": "Хотя P2P распределены, они не являются федеративными, то есть P2P - работают как единая сеть", - "hero-overlay-2-textlink": "Как работает SimpleX?", + "comparison-section-list-point-6": "Хотя P2P распределены, они не являются федеративными, то есть P2P — работают как единая сеть", + "hero-overlay-2-textlink": "Как SimpleX работает?", "simplex-chat-via-f-droid": "SimpleX Chat в F-Droid", - "privacy-matters-overlay-card-1-p-1": "Многие крупные компании используют информацию о том, с кем вы связаны, чтобы оценить ваш доход, продавать вам больше товаров, которые вам на самом деле не нужны, и определять из этой информации, выгодные для них цены.", - "privacy-matters-1-overlay-1-title": "Конфиденциальность экономит ваши деньги", - "simplex-private-card-5-point-1": "SimpleX использует заполнение содержимого для каждого уровня шифрования, чтобы предотвратить атаки на размер сообщения.", - "privacy-matters-2-overlay-1-linkText": "Конфиденциальность дает вам власть", - "hero-overlay-3-title": "Оценка безопасности", - "enter-your-email-address": "Email адрес", - "simplex-explained-tab-1-text": "1. Как это видят пользователи", + "privacy-matters-overlay-card-1-p-1": "Многие крупные компании используют информацию о том, с кем Вы связаны, чтобы оценить Ваш доход, продавать Вам больше товаров, которые Вам на самом деле не нужны, и определять из этой информации, выгодные для них цены.", + "privacy-matters-1-overlay-1-title": "Конфиденциальность экономит Ваши деньги", + "simplex-private-card-5-point-1": "SimpleX использует дополнение содержания для каждого уровня шифрования, чтобы скрыть размер сообщений.", + "privacy-matters-2-overlay-1-linkText": "Конфиденциальность дает Вам власть", + "hero-overlay-3-title": "Аудит безопасности", + "enter-your-email-address": "Введите Ваш адрес Email", + "simplex-explained-tab-1-text": "1. Что видят пользователи", "tap-to-close": "Нажмите, чтобы закрыть", "simplex-unique-card-4-p-2": "Вы можете использовать SimpleX со своими собственными серверами или с серверами, предоставленными нами — и при этом подключаться к любому пользователю SimpleX.", - "hero-overlay-card-3-p-2": "В ноябре 2022 года Trail of Bits провела обзор криптографии и сетевых компонентов SimpleX. Дополнительная информация.", - "hero-overlay-card-3-p-3": "В июле 2024 года Trail of Bits провела обзор криптографического дизайна протоколов SimpleX. Дополнительная информация.", - "feature-1-title": "Сообщения зашифрованные E2E-шифрованием
с поддержкой markdown и редактированием", + "hero-overlay-card-3-p-2": "В ноябре 2022 года Trail of Bits провела аудит криптографии и сетевых компонентов SimpleX. Дополнительная информация.", + "hero-overlay-card-3-p-3": "В июле 2024 года Trail of Bits провела аудит криптографического дизайна протоколов SimpleX. Дополнительная информация.", + "feature-1-title": "Сообщения с E2E-шифрованием
с форматированием и редактированием", "comparison-point-4-text": "Единая или Централизованная сеть", "guide-dropdown-9": "Установление соединений", - "simplex-unique-1-overlay-1-title": "Полная конфиденциальность вашей личности, профиля, контактов и других метаданных", - "hero-overlay-card-2-p-4": "SimpleX защищает от этих атак, поскольку в его конструкции нет никаких идентификаторов пользователей. И, если вы используете режим инкогнито, у вас будет другое отображаемое имя для каждого контакта, что позволит избежать какого-либо пересечения между ними.", - "privacy-matters-overlay-card-2-p-2": "Чтобы быть объективным и принимать независимые решения, вам необходимо контролировать свое информационное пространство. Это возможно только в том случае, если вы используете приватную, коммуникационную платформу, которая не имеет доступа к вашему социальному графу.", - "hero-overlay-card-2-p-1": "Когда у пользователей есть постоянные идентификаторы, даже если это просто случайное число, например идентификатор сеанса, существует риск того, что провайдер или злоумышленник могут наблюдайте за тем, как подключены пользователи и сколько сообщений они отправляют.", - "feature-3-title": "Децентрализованные группы — только
их участники знают, что они существуют", + "simplex-unique-1-overlay-1-title": "Полная конфиденциальность Вашей личности, профиля, контактов и метаданных", + "hero-overlay-card-2-p-4": "SimpleX защищает от этих атак, поскольку он не использует никакие идентификаторы профилей пользователей. И, если Вы используете режим инкогнито, у Вас будет другое отображаемое имя для каждого контакта, что позволит избежать какого-либо пересечения между ними.", + "privacy-matters-overlay-card-2-p-2": "Чтобы быть объективным и принимать независимые решения, необходимо контролировать свое информационное пространство. Это возможно только, если Вы используете конфиденциальную коммуникационную сеть, которая не имеет доступа к контактам Вашей социальной сети.", + "hero-overlay-card-2-p-1": "Когда у пользователя есть постоянный идентификатор, даже если это просто случайное число, например Session ID, существует риск того, что провайдер или злоумышленник могут наблюдать за тем, как пользователи соединены и сколько сообщений они отправляют.", + "feature-3-title": "Децентрализованные группы — известные только участникам", "glossary": "Глоссарий", "simplex-network-overlay-1-title": "Сравнение с протоколами обмена сообщениями P2P", - "comparison-section-list-point-7": "Сети P2P либо имеют центральный орган управления, либо вся сеть может быть скомпрометирована", - "simplex-unique-overlay-card-4-p-1": "Вы можете использовать SimpleX со своими собственными серверами или предоставленными нами серверами, при этом имея возможность общаться с любым пользователем.", - "simplex-explained-tab-3-p-1": "Серверы имеют отдельные, Анонимные учётные данные для каждой очереди и не знают, к каким пользователям они принадлежат.", - "docs-dropdown-1": "Платформа SimpleX", - "hero-overlay-card-1-p-5": "Только клиентские устройства хранят профили пользователей, контакты и группы; сообщения отправляются с двухуровневым, Сквозным шифрованием.", + "comparison-section-list-point-7": "Сети P2P либо имеют центральный компонент, либо вся сеть может быть атакована", + "simplex-unique-overlay-card-4-p-1": "Вы можете использовать SimpleX со своими собственными серверами или с серверами предустановленными в приложении, при этом имея возможность общаться с любым пользователем.", + "simplex-explained-tab-3-p-1": "Серверы имеют отдельные, анонимные учётные данные для каждой очереди и не знают, каким пользователям они принадлежат.", + "docs-dropdown-1": "Сеть SimpleX", + "hero-overlay-card-1-p-5": "Только клиентские устройства хранят профили пользователей, контакты и группы; сообщения отправляются с двухуровневым сквозным шифрованием.", "simplex-chat-for-the-terminal": "SimpleX Chat для терминала", - "simplex-network-overlay-card-1-li-3": "P2P не решает проблему MITM-атаки (Атака посредника), и большинство существующих реализаций не используют внеполосные сообщения для первоначального обмена ключами. SimpleX использует внеполосные сообщения или, в некоторых случаях, ранее существовавшие защищенные и доверенные соединения для первоначального обмена ключами.", + "simplex-network-overlay-card-1-li-3": "P2P не решает проблему MITM-атаки (Атака посредника), и многие сети не используют сообщения вне протокола для первоначального обмена ключами. SimpleX использует сообщения вне протокола или, в некоторых случаях, ранее существовавшие защищенные и доверенные соединения для первоначального обмена ключами.", "the-instructions--source-code": "SimpleX Chat.", "simplex-network-section-desc": "SimpleX Chat обеспечивает наилучшую конфиденциальность, сочетая преимущества P2P и федеративных сетей.", - "privacy-matters-section-subheader": "Сохранение конфиденциальности ваших метаданных — с кем вы общаетесь — защищает вас от:", - "if-you-already-installed": "Если вы уже установили", - "simplex-explained-tab-3-p-2": "Пользователи могут еще больше повысить свою конфиденциальность скрыв свой IP-адрес, например используя сеть Tor для доступа к серверам.", - "join": "Присоединяйся к", - "privacy-matters-section-header": "Почему приватность важна", - "hero-overlay-1-textlink": "Почему идентификаторы пользователя - вредны для приватности?", + "privacy-matters-section-subheader": "Сохранение конфиденциальности Ваших метаданных — с кем Вы общаетесь — защищает Вас от:", + "if-you-already-installed": "Если Вы уже установили", + "simplex-explained-tab-3-p-2": "Пользователи могут повысить свою конфиденциальность используя сеть Tor для доступа к серверам.", + "join": "Присоединяйтесь к", + "privacy-matters-section-header": "Почему конфиденциальность важна", + "hero-overlay-1-textlink": "Почему идентификаторы пользователя уменьшают конфиденциальность?", "on-this-page": "На этой странице", "privacy-matters-overlay-card-1-p-2": "Интернет-магазины знают, что люди с более низкими доходами с большей вероятностью совершают срочные покупки, поэтому они могут устанавливать более высокие цены или отменять скидки.", - "simplex-unique-3-overlay-1-title": "Контроль и безопасность ваших данных", + "simplex-unique-3-overlay-1-title": "Контроль и безопасность Ваших данных", "protocol-3-text": "Протоколы P2P", - "simplex-private-card-6-point-2": "Чтобы предотвратить это, приложения SimpleX передают одноразовые ключи внеполосно, когда вы делитесь адресом в виде ссылки или QR-кода.", + "simplex-private-card-6-point-2": "Чтобы предотвратить это, приложения SimpleX передают одноразовые ключи вне протокола, когда Вы делитесь адресом в виде ссылки или QR-кода.", "no": "Нет", - "contact-hero-header": "Вы получили адрес для подключения в SimpleX Chat", + "contact-hero-header": "Вы получили адрес контакта для соединения в SimpleX Chat", "feature-8-title": "Режим инкогнито —
уникальный для SimpleX Chat", - "why-simplex": "Что делает SimpleX уникальным", + "why-simplex": "Почему SimpleX", "simplex-private-card-4-point-2": "Чтобы использовать SimpleX через сеть Tor, пожалуйста, установите приложение Orbot и включите прокси (или режим VPN на iOS).", - "contact-hero-subheader": "Отсканируйте QR-код с помощью приложения SimpleX Chat на вашем телефоне или планшете.", + "contact-hero-subheader": "Отсканируйте QR-код с помощью приложения SimpleX Chat на Вашем телефоне или планшете.", "simplex-unique-2-overlay-1-title": "Лучшая защита от спама и злоупотреблений", - "simplex-private-6-title": "Внеполосный
Обмен ключами", - "join-us-on-GitHub": "Присоединяйтесь к нам на GitHub", + "simplex-private-6-title": "Обмен ключами
вне протокола", + "join-us-on-GitHub": "Присоединяйтесь на GitHub", "comparison-section-header": "Сравнение с другими протоколами", - "invitation-hero-header": "Вы получили одноразовую ссылку для подключения в SimpleX Chat", - "no-secure": "Нет - безопасно", - "hero-overlay-card-1-p-2": "Для доставки сообщений вместо идентификаторов пользователей, используемых всеми другими платформами, SimpleX использует временные, анонимные, попарные идентификаторы очередей сообщений, отдельно для каждого из ваших контактов — нет долгосрочных идентификаторов.", + "invitation-hero-header": "Вы получили одноразовую ссылку для соединения в SimpleX Chat", + "no-secure": "Нет - безопасный", + "hero-overlay-card-1-p-2": "Для доставки сообщений вместо идентификаторов пользователей, используемых другими сетями, SimpleX использует временные, анонимные, парные идентификаторы очередей сообщений, отдельные для каждого Вашего контакта — без долгосрочных идентификаторов.", "simplex-explained-tab-1-p-2": "Как это может работать с однонаправленными очередями и без идентификаторов профиля пользователя?", "simplex-network-1-header": "В отличие от P2P-сетей", "jobs": "Присоединиться к команде", "simplex-private-card-7-point-2": "Если какое-либо сообщение будет добавлено, удалено или изменено, получатель будет предупрежден об этом.", - "simplex-unique-3-title": "Только вы контролируете
свои данные", + "simplex-unique-3-title": "Только Вы контролируете
Ваши данные", "guide-dropdown-3": "Секретные группы", "no-resilient": "Нет - устойчив", - "hide-info": "Спрятать информацию", + "hide-info": "Скрыть информацию", "privacy-matters-overlay-card-3-p-4": "Недостаточно просто использовать мессенджер со сквозным шифрованием, мы все должны использовать мессенджеры, которые защищают конфиденциальность наших личных сетей — с какими людьми мы связаны.", - "releases-to-this-repo-are-done-1-2-days-later": "Выпуск новых версий в этом репозитории выходит с задержкой в несколько дней", + "releases-to-this-repo-are-done-1-2-days-later": "Новые версии в этом репозитории публикуются с задержкой в несколько дней", "comparison-point-1-text": "Требуется глобальный идентификатор", "comparison-section-list-point-5": "Не защищает конфиденциальность пользовательских метаданных", - "hero-overlay-card-2-p-2": "Затем они могли бы сопоставить эту информацию с существующими общедоступными социальными сетями и определить некоторые реальные личности.", - "privacy-matters-overlay-card-1-p-3": "Некоторые финансовые и страховые компании используют социальные графики для определения процентных ставок и премий. Это часто заставляет людей с более низкими доходами платить больше — это известно как \"премия за бедность\".", + "hero-overlay-card-2-p-2": "Эта информация может быть сопоставлена с социальными сетями, чтобы определить реальные личности пользователей.", + "privacy-matters-overlay-card-1-p-3": "Некоторые финансовые и страховые компании используют социальные сети для определения процентных ставок и премий. Это часто заставляет людей с более низкими доходами платить больше — это известно как \"наценка за бедность\".", "comparison-point-3-text": "Зависимость от DNS", "yes": "Да", "docs-dropdown-6": "Сервера WebRTC", "newer-version-of-eng-msg": "Существует более новая версия этой страницы на английском языке.", "install-simplex-app": "Установите приложение SimpleX", - "hero-overlay-3-textlink": "Оценка безопасности", + "hero-overlay-3-textlink": "Аудит безопасности", "comparison-point-2-text": "Возможность MITM", - "scan-the-qr-code-with-the-simplex-chat-app-description": "Открытые ключи и адрес очереди сообщений, указанные в этой ссылке, НЕ отправляются по сети при просмотре этой страницы —
они содержатся в хэш-фрагменте URL-адреса ссылки.", + "scan-the-qr-code-with-the-simplex-chat-app-description": "Публичные ключи и адрес очереди сообщений, указанные в этой ссылке, НЕ отправляются по сети при просмотре этой страницы —
они содержатся в хэш-фрагменте URL-адреса ссылки.", "guide-dropdown-8": "Настройки приложения", - "simplex-explained-tab-2-p-2": "Серверы передают сообщения только в одну сторону, не имея полной картины общения пользователя или подключений.", + "simplex-explained-tab-2-p-2": "Серверы передают сообщения только в одну сторону, не имея полной информации о разговорах и контактах пользователя.", "smp-protocol": "Протокол SMP", "open-simplex-app": "Откройте приложение SimpleX", "see-simplex-chat": "Инструкции по загрузке или компиляции SimpleX Chat из исходного кода приведены в", "terminal-cli": "Приложение для терминала (CLI)", "comparison-section-list-point-1": "Обычно требуется номера телефона, в некоторых случаях — имя пользователя", - "simplex-explained-tab-3-text": "3. Что видят сервера", - "github-repository": "репозиторий Github", + "simplex-explained-tab-3-text": "3. Что видят серверы", + "github-repository": "репозитории GitHub", "feature-5-title": "Исчезающие сообщения", "connect-in-app": "Подключитесь в приложении", "menu": "Меню", - "simplex-private-card-4-point-1": "Чтобы защитить свой IP-адрес вы можете подключаться к серверам через сеть Tor или какую-либо другую транспортную оверлейную сеть.", - "privacy-matters-3-title": "Судебное преследование не виновных", - "comparison-point-5-text": "Атака на центральный компонент или другая сетевая атака", + "simplex-private-card-4-point-1": "Чтобы защитить свой IP-адрес Вы можете подключаться к серверам через сеть Tor или какую-либо другую транспортную оверлейную сеть.", + "privacy-matters-3-title": "Преследование из-за невиновного общения", + "comparison-point-5-text": "Центральный компонент или атака на всю сеть", "click-to-see": "Нажать здесь, чтобы увидеть", - "donate-here-to-help-us": "Пожертвуйте здесь, чтобы помочь нам", + "donate-here-to-help-us": "Пожертвовать", "simplex-private-1-title": "2-уровневое
сквозное шифрование", - "simplex-unique-card-1-p-2": "В отличие от любой другой существующей платформы обмена сообщениями, SimpleX не имеет идентификаторов пользователей — нету даже случайных цифр.", - "privacy-matters-2-overlay-1-title": "Конфиденциальность дает вам власть", - "simplex-unique-overlay-card-2-p-2": "Хоть злоумышленники и могут использовать постоянный адрес для отправки нежелательных запросов или спама, вы можете легко его изменить или просто удалить, не теряя связи с уже установленными контактами.", + "simplex-unique-card-1-p-2": "В отличие от любой другой существующей сети обмена сообщениями, SimpleX не имеет идентификаторов пользователей — нет даже случайных цифр.", + "privacy-matters-2-overlay-1-title": "Конфиденциальность дает Вам власть", + "simplex-unique-overlay-card-2-p-2": "Хоть злоумышленники и могут использовать постоянный адрес для отправки нежелательных запросов или спама, Вы можете легко его изменить или просто удалить, не теряя связи с уже установленными контактами.", "simplex-unique-4-overlay-1-title": "Полностью децентрализованная — пользователи владеют сетью SimpleX", "guide-dropdown-2": "Отправка сообщений", - "simplex-network-overlay-card-1-li-5": "Все известные P2P-сети могут быть уязвимы для Атаки Сивиллы, поскольку каждый узел доступен для обнаружения, и сеть работает как единое целое. Известные меры по его смягчению требуют либо централизованного компонента, либо дорогостоящего Proof-of-work. Сеть SimpleX не имеет функционала по обмену серверами, она фрагментирована и работает как множество изолированных подсетей, из-за чего провести атаку по всей сети - невозможно.", - "simplex-private-2-title": "Дополнительный уровень
шифрования сервера", - "hero-overlay-card-1-p-4": "Такая конструкция предотвращает утечку любых пользовательских метаданных на уровне приложения. Для дальнейшего улучшения конфиденциальности и защиты вашего IP-адреса вы можете подключиться к серверам обмена сообщениями через сеть Tor.", + "simplex-network-overlay-card-1-li-5": "Все известные P2P-сети могут быть уязвимы для Атаки Сивиллы, поскольку каждый узел доступен для обнаружения, и сеть работает как единое целое. Известные меры по уменьшению риска требуют либо централизованного компонента, либо дорогостоящего Proof-of-work. Сеть SimpleX не позволяет обнаруживать серверы, она фрагментирована и работает как множество изолированных подсетей, из-за чего провести атаку на всю сеть невозможно.", + "simplex-private-2-title": "Дополнительный уровень
шифрования с сервером", + "hero-overlay-card-1-p-4": "Этот подход предотвращает утечку любых пользовательских метаданных на уровне приложения. Для дальнейшего улучшения конфиденциальности и защиты Вашего IP-адреса Вы можете подключиться к серверам обмена сообщениями через сеть Tor.", "f-droid-org-repo": "Репозиторий F-Droid.org", "guide-dropdown-4": "Профили чата", "simplex-network-2-header": "В отличие от федеративных сетей", - "see-here": "подробней тут", - "simplex-private-3-title": "Безопасный аутентифицированный
протокол TLS", - "comparison-section-list-point-3": "Открытый ключ или какой-либо другой глобально уникальный идентификатор", - "hero-overlay-card-2-p-3": "Даже в самых приватных приложениях, использующих скрытые сервисы Tor v3, если вы общаетесь с двумя разными контактами через один и тот же профиль, они могут доказать, что они являются связаны с одним и тем же человеком.", - "simplex-private-4-title": "Вариант доступа
через сеть Tor", + "see-here": "подробнее здесь", + "simplex-private-3-title": "Безопасное аутентифицированное
соединение TLS", + "comparison-section-list-point-3": "Публичный ключ или какой-либо другой глобально уникальный идентификатор", + "hero-overlay-card-2-p-3": "Даже в самых конфиденциальных приложениях, использующих скрытые адреса Tor, если Вы общаетесь с двумя разными контактами через один и тот же профиль, они могут доказать, что они связаны с одним и тем же человеком.", + "simplex-private-4-title": "Возможен доступ
через сеть Tor", "privacy-matters-1-title": "Реклама и ценовая дискриминация", "simplex-unique-card-3-p-1": "SimpleX хранит все пользовательские данные на клиентских устройствах в портативном формате зашифрованной базы данных — их можно перенести на другое устройство.", - "hero-overlay-1-title": "Как работает SimpleX?", + "hero-overlay-1-title": "Как SimpleX работает?", "stable-versions-built-by-f-droid-org": "Стабильные версии, созданные F-Droid.org", - "contact-hero-p-3": "Воспользуйтесь ссылками ниже, чтобы загрузить приложение.", + "contact-hero-p-3": "Используйте ссылки ниже, чтобы загрузить приложение.", "simplex-network": "Сеть SimpleX", - "privacy-matters-3-overlay-1-title": "Конфиденциальность защищает вашу свободу", + "privacy-matters-3-overlay-1-title": "Конфиденциальность защищает Вашу свободу", "docs-dropdown-7": "Перевести SimpleX Chat", "back-to-top": "Вернуться к началу", "simplex-network-1-desc": "Все сообщения отправляются через серверы, что обеспечивает лучшую конфиденциальность метаданных и надежную асинхронную доставку сообщений, избегая при этом многих", "simplex-chat-repo": "Репозиторий SimpleX Chat", "simplex-private-card-6-point-1": "Многие коммуникационные платформы уязвимы для MITM-атак со стороны серверов или сетевых провайдеров.", - "privacy-matters-3-overlay-1-linkText": "Конфиденциальность защищает вашу свободу", - "simplex-unique-overlay-card-1-p-2": "Для доставки сообщений SimpleX использует попарные, анонимные адреса однонаправленных очередей сообщений, раздельные для полученных и отправленных сообщений, обычно через разные серверы. Использование SimpleX это как иметь отдельный “одноразовый” адрес электронной почты или номер телефона для каждого контакта, при это, не обременяя вас управлять эти вручную.", - "simplex-unique-overlay-card-3-p-4": "Со стороны не видно разницу между отправлением или получением сообщений — если кто-то наблюдает за этим, он не cможет легко определить, кто с кем общается, даже если протокол TLS будет скомпрометирован.", + "privacy-matters-3-overlay-1-linkText": "Конфиденциальность защищает Вашу свободу", + "simplex-unique-overlay-card-1-p-2": "Для доставки сообщений SimpleX использует попарные, анонимные адреса однонаправленных очередей сообщений, раздельные для полученных и отправленных сообщений, обычно через разные серверы.", + "simplex-unique-overlay-card-3-p-4": "Со стороны не видно разницы между отправлением или получением сообщений — если кто-то наблюдает за этим, он не cможет легко определить, кто с кем общается, даже если протокол TLS будет скомпрометирован.", "docs-dropdown-2": "Доступ к файлам в версии для Android", - "get-simplex": "Скачать SimpleX для ПК", - "privacy-matters-overlay-card-3-p-1": "Каждый должен заботиться о конфиденциальности и безопасности своих коммуникаций — безобидные разговоры могут подвергнуть вас опасности, например за ваши политические взгляды, даже если кажется, что вам \"нечего скрывать\".", + "get-simplex": "Скачать SimpleX для компьютера", + "privacy-matters-overlay-card-3-p-1": "Каждый должен заботиться о конфиденциальности и безопасности своих коммуникаций — безобидные разговоры могут подвергнуть Вас опасности, например за Ваши политические взгляды, даже если кажется, что Вам \"нечего скрывать\".", "simplex-unique-2-title": "Вы защищены от
спама и злоупотреблений", - "simplex-unique-overlay-card-4-p-3": "Если вы рассматриваете возможность разработки платформе SimpleX, например, чат-бота для пользователей SimpleX или интеграции библиотеки SimpleX Chat в ваше мобильное приложение, пожалуйста, обращайтесь за любыми советами и поддержкой.", + "simplex-unique-overlay-card-4-p-3": "Если Вы рассматриваете возможность разработки для сети SimpleX, например, чат-бота для пользователей SimpleX или интеграции библиотеки SimpleX Chat в Ваше мобильное приложение, пожалуйста, обращайтесь за любыми советами и поддержкой.", "comparison-section-list-point-2": "Адреса на основе DNS", "stable-and-beta-versions-built-by-developers": "Стабильные и бета-версии, созданные разработчиками", "simplex-network-3-header": "Сеть SimpleX", - "simplex-unique-card-2-p-1": "Поскольку у вас нет идентификатора или фиксированного адреса на платформе SimpleX, никто не сможет связаться с вами, без вашего явного согласия, только если вы сами поделитесь адресом в виде QR-кода или ссылки.", - "hero-overlay-card-3-p-1": "Trail of Bits - ведущая консалтинговая компания в области безопасности и технологий, клиентами которой являются крупные технологические компании, правительственные агентства и крупные блокчейн проекты.", - "hero-header": "Иной взгляд на приватность", + "simplex-unique-card-2-p-1": "Поскольку у Вас нет идентификатора или фиксированного адреса в сети SimpleX, никто не сможет связаться с Вами без Вашего явного согласия mdash; это возможно, только если Вы сами поделитесь адресом в виде QR-кода или ссылки.", + "hero-overlay-card-3-p-1": "Trail of Bits — ведущая консалтинговая компания в области безопасности, клиентами которой являются крупные технологические компании, правительственные агентства и крупные блокчейн проекты.", + "hero-header": "Более конфиденциальный", "comparison-section-list-point-4": "Если операторы серверов скомпрометированы. В Signal, и некоторых других приложениях, есть возможность подтвердить код безопасности", - "simplex-private-card-2-point-1": "Дополнительный уровень серверного шифрования для доставки получателю, чтобы предотвратить корреляцию между полученным и отправленным трафиком сервера, если используемый протокол TLS скомпрометированный.", - "f-droid-page-simplex-chat-repo-section-text": "Чтобы добавить его в свой клиент F-Droid, отсканируйте QR-код или воспользуйтесь этим URL-адресом:", - "guide-dropdown-7": "Приватность и безопасность", - "join-the-REDDIT-community": "Присоединяйтесь к нам в сообществе REDDIT", - "simplex-private-card-10-point-2": "Это позволяет доставлять сообщения без идентификаторов профиля пользователя, обеспечивая лучшую конфиденциальность метаданных, чем другие альтернативы.", + "simplex-private-card-2-point-1": "Дополнительный уровень шифрования между сервером и получателем для предотвращения корреляции полученного и отправленного трафика сервера, в случае компрометированного TLS.", + "f-droid-page-simplex-chat-repo-section-text": "Чтобы добавить его в свой клиент F-Droid, отсканируйте QR-код или используйте этот адрес:", + "guide-dropdown-7": "Конфиденциальность и безопасность", + "join-the-REDDIT-community": "Присоединяйтесь в REDDIT", + "simplex-private-card-10-point-2": "Это позволяет доставлять сообщения без идентификаторов профиля пользователя, и обеспечивает лучшую конфиденциальность метаданных, чем альтернативы.", "privacy-matters-2-title": "Манипулирование выборами", - "home": "Домашняя страница", + "home": "Главная", "chat-protocol": "Протокол чата", - "simplex-private-card-5-point-2": "Это позволяет сообщениям разного размера выглядеть одинаково для серверов и сетевых наблюдателей.", - "hero-overlay-card-1-p-1": "Многие спрашивают:Если у SimpleX нету никаких идентификаторов пользователя, то как приложение знает, куда доставлять сообщения?", - "feature-6-title": "Зашифрованные E2E-шифрованием аудио и видео звонки", - "hero-p-1": "Другие приложения имеют ID своих пользователей: Signal, Matrix, Session, Briar, Jami, Cwtch и т. п.
SimpleX не имет, нету даже случайных цифр.
Это значительно повышает вашу приватность.", - "simplex-network-overlay-card-1-li-2": "В отличие от многих P2P сетей, SimpleX спроектирован так, чтобы не нуждаться в глобальных идентификаторов его пользователей, даже временных, используя только временные попарные идентификаторы, обеспечивая лучшую анонимность и защиту метаданных пользователя.", - "simplex-unique-4-title": "Только вы владеете
сетью SimpleX", + "simplex-private-card-5-point-2": "Это делает сообщениям разного размера одинаковыми для серверов и сети.", + "hero-overlay-card-1-p-1": "Многие спрашивают: Если у SimpleX нет никаких идентификаторов пользователя, то как приложение знает, куда доставлять сообщения?", + "feature-6-title": "Аудио и видео звонки с E2E-шифрованием", + "hero-p-1": "Другие приложения используют ID пользователей: Signal, Matrix, Session, Briar, Jami, Cwtch и т. п.
SimpleX не использует, даже случайных цифр.
Это значительно повышает Вашу конфиденциальность.", + "simplex-network-overlay-card-1-li-2": "В отличие от многих P2P сетей, SimpleX спроектирован так, чтобы не нуждаться в глобальных идентификаторах пользователей, даже временных, используя только временные попарные идентификаторы, обеспечивая лучшую анонимность и защиту метаданных пользователей.", + "simplex-unique-4-title": "Вы владеете
сетью SimpleX", "privacy-matters-overlay-card-3-p-3": "Обычных людей арестовывают за то, чем они делятся в Интернете, даже через свои \"анонимные\" аккаунты, даже в демократических странах.", "simplex-unique-overlay-card-3-p-2": "Сквозные зашифрованные сообщения временно хранятся на серверах SimpleX до получения, после чего они удаляются безвозвратно.", - "blog": "Новости", - "simplex-private-card-7-point-1": "Чтобы гарантировать целостность, сообщения последовательно нумеруются и включают в себя хэш предыдущего сообщения.", - "simplex-unique-overlay-card-4-p-2": "Платформа SimpleX использует открытый протокол и предоставляет SDK для создания чат-ботов, позволяя внедрять сервисы, с которыми пользователи могут взаимодействовать через приложение SimpleX Chat — мы с нетерпением ждем возможности увидеть, какие сервисы SimpleX вы сможете создать.", + "blog": "Блог", + "simplex-private-card-7-point-1": "Для обеспечения неизменности, сообщения нумеруются по порядку и содержат хэш предыдущего сообщения.", + "simplex-unique-overlay-card-4-p-2": "Сеть SimpleX использует открытый протокол и предоставляет SDK для создания чат-ботов, позволяя внедрять сервисы, с которыми пользователи могут взаимодействовать через приложение SimpleX Chat — мы с нетерпением ждем сервисы SimpleX, которые Вы создадите.", "simplex-explained-tab-1-p-1": "Вы можете создавать контакты и группы, а также вести двусторонние беседы, как и в любом другом мессенджере.", "contact-hero-p-2": "Еще не скачали SimpleX Chat?", - "why-simplex-is": "Почему SimpleX", + "why-simplex-is-unique": "Почему SimpleX уникальный", "simplex-network-section-header": "Сеть SimpleX", "simplex-private-10-title": "Временные анонимные парные идентификаторы", - "privacy-matters-1-overlay-1-linkText": "Конфиденциальность экономит ваши деньги", + "privacy-matters-1-overlay-1-linkText": "Конфиденциальность экономит Ваши деньги", "tap-the-connect-button-in-the-app": "Нажмите на кнопку ’подключиться’ в приложении", "comparison-section-list-point-4a": "Сервера SimpleX не могут скомпрометировать сквозное шифрование", - "unique": "уникальный", "simplex-network-1-overlay-linktext": "проблем P2P сетей", - "no-private": "Нет - приватно", - "simplex-unique-1-title": "У вас есть полная
конфиденциальность", + "no-private": "Нет - конфиденциальный", + "simplex-unique-1-title": "У Вас есть полная
конфиденциальность", "protocol-2-text": "XMPP, Matrix", "guide": "Руководство", - "simplex-network-overlay-card-1-li-4": "Реализации P2P могут быть заблокированы некоторыми интернет-провайдерами (например, BitTorrent). SimpleX не зависит от транспорта - он может работать по стандартным веб-протоколам, например WebSockets.", - "hero-overlay-2-title": "Почему идентификаторы пользователя - вредны для приватности?", + "simplex-network-overlay-card-1-li-4": "Реализации P2P могут быть заблокированы некоторыми интернет-провайдерами (например, BitTorrent). SimpleX протокол не зависит от транспорта — он может быть реализован через стандартные веб-протоколы, например WebSockets.", + "hero-overlay-2-title": "Почему идентификаторы пользователя уменьшают конфиденциальность?", "simplex-explained-tab-2-text": "2. Как это работает", "docs-dropdown-4": "Свой SMP Сервер", - "feature-4-title": "Зашифрованные E2E-шифрованием голосовые сообщения", - "privacy-matters-overlay-card-2-p-1": "Не так давно мы наблюдали, как авторитетная консалтинговая компания манипулировала крупными выборами, используя наши социальные графики для искажения нашего мнения из реального мира и манипулируют нашими голосами.", - "privacy-matters-overlay-card-2-p-3": "SimpleX - это первая платформа, которая по своей конструкции не имеет никаких идентификаторов пользователей, таким образом защищая график ваших контактов лучше, чем любая известная альтернатива.", - "learn-more": "Учить больше", + "feature-4-title": "Голосовые сообщения с E2E-шифрованием", + "privacy-matters-overlay-card-2-p-1": "Не так давно мы наблюдали, как авторитетная консалтинговая компания манипулировала крупными выборами, используя наши контакты в социальных сетях, чтобы изменять наши мнения и манипулировать нашими голосами.", + "privacy-matters-overlay-card-2-p-3": "SimpleX — это первая сеть, которая не имеет никаких идентификаторов пользователей, таким образом защищая Ваши контакты лучше, чем любая известная альтернатива.", + "learn-more": "Узнать больше", "donate": "Пожертвовать", "simplex-private-8-title": "Смешивание сообщений
для уменьшения корреляции", "scan-qr-code-from-mobile-app": "Отсканируйте QR-код в мобильном приложении", "simplex-private-card-3-point-3": "Возобновление соединения отключено для предотвращения сеансовых атак.", - "simplex-private-card-10-point-1": "SimpleX использует временные анонимные попарные адреса и учетные данные для каждого контакта пользователя или члена группы.", + "simplex-private-card-10-point-1": "SimpleX использует временные анонимные парные адреса и учетные данные для каждого контакта пользователя или члена группы.", "guide-dropdown-6": "Аудио и видео Звонки", - "more-info": "Больше информации", + "more-info": "Дополнительная информации", "no-decentralized": "Нет - децентрализованный", "protocol-1-text": "Signal, большие платформы", - "hero-2-header-desc": "В видео показано, как подключиться к своему другу с помощью его одноразового QR-кода, лично или по видеосвязи. Вы также можете подключиться, поделившись ссылкой-приглашением.", - "simplex-network-overlay-card-1-li-6": "Сети P2P могут быть уязвимы для DRDoS атаки, когда клиенты могут ретранслировать и усиливать/увеличивать объём трафика, что приводит к отказу в обслуживании по всей сети. Клиенты SimpleX ретранслируют трафик только из известного соединения и не могут быть использованы злоумышленником для нагрузки трафика всей сети.", - "if-you-already-installed-simplex-chat-for-the-terminal": "Если вы уже установили SimpleX Chat для терминала", + "hero-2-header-desc": "В видео показано, как подключиться к Вашему другу через одноразовый QR-код, при встрече или во время видеосвязи. Вы также можете соединится, поделившись ссылкой-приглашением.", + "simplex-network-overlay-card-1-li-6": "Сети P2P могут быть уязвимы для DRDoS атаки, когда клиенты могут ретранслировать и увеличивать трафик, что приводит к отказу всей сети. Клиенты SimpleX ретранслируют трафик только из известного соединения и не могут быть использованы злоумышленником для создания трафика во всей сети.", + "if-you-already-installed-simplex-chat-for-the-terminal": "Если Вы уже установили SimpleX Chat для терминала", "docs-dropdown-8": "Служба Каталогов SimpleX", - "simplex-private-card-1-point-1": "Протокол с двойным храповым механизмом —
обмен сообщениями OTR с идеальной секретностью пересылки и восстановлением после взлома.", + "simplex-private-card-1-point-1": "Протокол двойного обновления ключей —
\"отрицаемые\" сообщения с идеальной прямой секретностью и восстановлением после взлома", "simplex-private-card-8-point-1": "Серверы SimpleX действуют как узлы-миксеры с низкой задержкой — входящие и исходящие сообщения имеют разный порядок.", - "simplex-unique-overlay-card-2-p-1": "Поскольку у вас нет идентификатора на платформе SimpleX, никто не сможет связаться с вами, если вы сами не предоставите одноразовый или временный адрес в виде QR-кода или ссылки.", - "sign-up-to-receive-our-updates": "Введите ваш Email, чтобы получать рассылку обновлений от нас", + "simplex-unique-overlay-card-2-p-1": "Поскольку у Вас нет идентификатора в сети SimpleX, никто не сможет связаться с Вами, если Вы сами не предоставите одноразовый или временный адрес в виде QR-кода или ссылки.", + "sign-up-to-receive-our-updates": "Подпишитесь на нашу рассылку новостей", "guide-dropdown-1": "Быстрый старт", - "simplex-explained-tab-2-p-1": "Для каждого подключения вы используете две отдельные очереди обмена сообщениями, то есть отправка и получения сообщений происходит через разные серверы.", - "simplex-private-section-header": "Что делает SimpleX приватным", - "we-invite-you-to-join-the-conversation": "Мы приглашаем вас присоединиться к беседе", - "feature-2-title": "Изображения, видео и файлы
зашифрованные E2E-шифрованием", + "simplex-explained-tab-2-p-1": "Для каждого контакта Вы используете две отдельные очереди сообщений, отправляя и получая сообщения через разные серверы.", + "simplex-private-section-header": "Что делает SimpleX конфиденциальным", + "we-invite-you-to-join-the-conversation": "Мы приглашаем Вас присоединиться к разговору", + "feature-2-title": "Изображения, видео и файлы
с E2E-шифрованием", "simplex-private-9-title": "Однонаправленные
очереди сообщений", - "simplex-unique-overlay-card-1-p-3": "Этот дизайн защищает конфиденциальность того, с кем вы общаетесь, скрывая это от серверов SimpleX и от любых наблюдателей из вне. Чтобы скрыть свой IP-адрес от серверов, вы можете подключиться к серверам SimpleX через сеть Tor.", + "simplex-unique-overlay-card-1-p-3": "Этот дизайн защищает конфиденциальность Ваших контактов, скрывая их от серверов SimpleX и от любых внешних наблюдателей. Чтобы скрыть свой IP-адрес от серверов, Вы можете подключиться к серверам SimpleX через сеть Tor.", "developers": "Разработчики", - "simplex-private-7-title": "Проверка целостности
сообщения", - "privacy-matters-overlay-card-1-p-4": "Платформа SimpleX защищает конфиденциальность ваших контактов лучше, чем любая другая альтернатива, полностью предотвращая доступ к вашему социальному графику каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.", - "hero-overlay-card-1-p-6": "Подробнее читайте в техническом документе SimpleX.", + "simplex-private-7-title": "Проверка неизменности
сообщений", + "privacy-matters-overlay-card-1-p-4": "Сеть SimpleX защищает конфиденциальность Ваших контактов лучше, чем альтернативы, предотвращая доступ к Вашей социальной сети каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.", + "hero-overlay-card-1-p-6": "Подробнее читайте в техническом описании SimpleX.", "simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями P2P имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.", "terms-and-privacy-policy": "Политика Конфиденциальности", - "simplex-network-overlay-card-1-li-1": "Сети P2P полагаются на тот или иной вариант DHT для маршрутизации сообщений. Проекты DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети - O(log N).", - "privacy-matters-section-label": "Убедитесь, что ваш мессенджер не может получить доступ к вашим данным!", + "simplex-network-overlay-card-1-li-1": "Сети P2P используют DHT (распределенные хэш-таблицы) для маршрутизации сообщений. DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети — O(log N).", + "privacy-matters-section-label": "Убедитесь, что Ваш мессенджер не может получить доступ к Вашим данным!", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat хранит все пользовательские данные на клиентских устройствах в портативном формате зашифрованной базы данных которую можно перенести на другое устройство.", "simplex-network-3-desc": "серверы предоставляют однонаправленные очереди для подключения пользователей, но у них нет видимости графика сетевых подключений — это делают только пользователи.", - "simplex-unique-card-1-p-1": "SimpleX защищает конфиденциальность вашего профиля, контактов и метаданных, скрывая их от серверов платформы SimpleX и любых наблюдателей.", - "simplex-private-card-3-point-1": "Для соединений клиент-сервер используется только протокол TLS 1.2/1.3 с надежными алгоритмами.", - "simplex-unique-card-4-p-1": "Сеть SimpleX полностью децентрализована и независима от любой криптовалюты/блокчейна или любой другой платформы, кроме Интернета.", - "features": "Возможности", - "hero-overlay-card-1-p-3": "Вы определяете, какие серверы будете использовать для получения сообщений, а ваши контакты — серверы, которые вы используете для отправки им сообщений. Каждый новый чат, скорее всего, будет вестись на двух разных серверах.", + "simplex-unique-card-1-p-1": "SimpleX защищает конфиденциальность Вашего профиля, контактов и метаданных, скрывая их от серверов сети SimpleX и любых наблюдателей.", + "simplex-private-card-3-point-1": "Для соединений клиента и сервера используется только протокол TLS 1.2/1.3 с безопасными алгоритмами.", + "simplex-unique-card-4-p-1": "Сеть SimpleX полностью децентрализована и независима от любой криптовалюты/блокчейна или любой другой сети, кроме Интернета.", + "features": "Функции", + "hero-overlay-card-1-p-3": "Вы определяете, какие серверы использовать для получения сообщений, а Ваши контакты — серверы, которые Вы используете для отправки им сообщений. Каждый новый чат, скорее всего, будет вестись на двух разных серверах.", "docs-dropdown-9": "Скачать", "simplex-chat-protocol": "Протокол SimpleX Chat", - "simplex-unique-overlay-card-1-p-1": "В отличие от других платформ обмена сообщениями, SimpleX не имеет идентификаторов, присвоенных пользователям. Он не полагается на номера телефонов, доменные адреса (например, электронную почту или XMPP), имена пользователей, открытые ключи или даже случайные числа для идентификации своих пользователей — мы не знаем, сколько людей пользуются нашими SimpleX серверами.", - "reference": "Ссылки", - "f-droid-page-f-droid-org-repo-section-text": "Приложение SimpleX Chat от разработчиков и от репозитория F-Droid.org имеют разные ключи подписи. Если вы хотите сменить одно на другое, вам сначала нужно будет экспортировать базу данных и только потом скачать другое приложение.", - "simplex-private-5-title": "Многоуровневое
Заполнения содержимого", - "please-use-link-in-mobile-app": "Пожалуйста, воспользуйтесь ссылкой в мобильном приложении", + "simplex-unique-overlay-card-1-p-1": "В отличие от других сетей обмена сообщениями, SimpleX не имеет идентификаторов, присвоенных пользователям. Он не полагается на номера телефонов, доменные адреса (например, электронную почту или XMPP), имена пользователей, публичные ключи или даже случайные числа для идентификации своих пользователей — операторы серверов SimpleX не знают, сколько людей пользуются их серверами.", + "reference": "Документы", + "f-droid-page-f-droid-org-repo-section-text": "Приложение SimpleX Chat от разработчиков и от репозитория F-Droid.org имеют разные ключи подписи. Если Вы хотите сменить одно на другое, Вам сначала нужно будет экспортировать базу данных и только потом скачать другое приложение.", + "simplex-private-5-title": "Многоуровневое
дополнение сообщений", + "please-use-link-in-mobile-app": "Пожалуйста, используйте ссылку в мобильном приложении", "please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код.", - "docs-dropdown-10": "Прозрачность", + "docs-dropdown-10": "Запросы данных", "docs-dropdown-12": "Безопасность", - "docs-dropdown-11": "Часто задаваемые вопросы" + "docs-dropdown-11": "Часто задаваемые вопросы", + "docs-dropdown-14": "SimpleX для бизнеса" } diff --git a/website/langs/uk.json b/website/langs/uk.json index 794c65c956..1c1780edb3 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -14,19 +14,19 @@ "simplex-network-overlay-card-1-li-6": "P2P-мережі можуть бути вразливими до атаки DRDoS, коли клієнти можуть ребродкастити та збільшувати трафік, що призводить до відмови в обслуговуванні на рівні всієї мережі. Клієнти SimpleX лише пересилають трафік від відомого підключення і не можуть бути використані зловмисником для збільшення трафіку в усій мережі.", "privacy-matters-overlay-card-1-p-2": "Інтернет-роздрібники знають, що люди з низьким доходом частіше роблять термінові покупки, тому вони можуть встановлювати вищі ціни або скасовувати знижки.", "privacy-matters-overlay-card-1-p-3": "Деякі фінансові та страхові компанії використовують соціальні графи для визначення ставок та страхових премій. Це часто змушує людей з низькими доходами платити більше — це відомо як 'поширений преміум'.", - "privacy-matters-overlay-card-1-p-4": "Платформа SimpleX захищає конфіденційність ваших з'єднань краще, ніж будь-яка альтернатива, повністю запобігаючи доступу вашого соціального графа будь-яким компаніям чи організаціям. Навіть коли люди використовують сервери, надані SimpleX Chat, ми не знаємо кількість користувачів чи їх зв'язки.", + "privacy-matters-overlay-card-1-p-4": "Мережа SimpleX краще захищає конфіденційність ваших з'єднань, ніж будь-яка інша альтернатива, повністю запобігаючи тому, щоб ваш соціальний граф став доступним для будь-яких компаній чи організацій. Навіть коли люди використовують сервери, попередньо налаштовані в додатках SimpleX Chat, оператори серверів не знають кількості користувачів або їхніх з'єднань.", "privacy-matters-overlay-card-2-p-1": "Не так давно ми спостерігали, як великі вибори маніпулювалися поважною консалтинговою компанією, яка використовувала наші соціальні графи для спотворення нашого уявлення про реальний світ та маніпулювання нашими голосами.", - "privacy-matters-overlay-card-2-p-2": "Щоб бути об'єктивним та приймати незалежні рішення, вам потрібно контролювати ваш інформаційний простір. Це можливо лише за умови використання приватної платформи для спілкування, яка не має доступу до вашого соціального графа.", - "privacy-matters-overlay-card-2-p-3": "SimpleX - це перша платформа, яка не має жодних ідентифікаторів користувачів за своїм дизайном, таким чином, захищаючи ваш графік з'єднань краще, ніж будь-яка відома альтернатива.", + "privacy-matters-overlay-card-2-p-2": "Щоб бути об'єктивним та приймати незалежні рішення, вам потрібно контролювати ваш інформаційний простір. Це можливо лише за умови використання приватної мережі для спілкування, яка не має доступу до вашого соціального графа.", + "privacy-matters-overlay-card-2-p-3": "SimpleX - це перша мережа, яка не має жодних ідентифікаторів користувачів за своїм дизайном, таким чином, захищаючи ваш графік з'єднань краще, ніж будь-яка відома альтернатива.", "privacy-matters-overlay-card-3-p-1": "Кожен повинен турбуватися про конфіденційність та безпеку свого спілкування — безпечні розмови можуть поставити вас під загрозу, навіть якщо у вас немає чого приховувати.", "privacy-matters-overlay-card-3-p-2": "Однією з найшокуючих історій є досвід Мохамеду Ульд Слахі, описаний в його мемуарах і показаний у фільмі \"Мавританець\". Його посадили в табір Гуантанамо без суду і мукили там 15 років після телефонного дзвінка йому родичеві в Афганістані, під підозрою в причетності до атак 11 вересня, навіть не дивлячись на те, що він проживав у Німеччині протягом попередніх 10 років.", "privacy-matters-overlay-card-3-p-4": "Недостатньо використовувати зашифрований від кінця до кінця месенджер; ми всі повинні використовувати месенджери, які захищають конфіденційність наших особистих мереж — з ким ми з'єднані.", - "simplex-unique-overlay-card-1-p-1": "На відміну від інших платформ обміну повідомленнями, у SimpleX не присвоюються ідентифікатори користувачів. Він не покладається на номери телефонів, адреси на основі домену (наприклад, електронна пошта або XMPP), імена користувачів, публічні ключі або навіть випадкові номери для ідентифікації своїх користувачів — ми не знаємо, скільки людей використовує наші сервери SimpleX.", + "simplex-unique-overlay-card-1-p-1": "На відміну від інших мереж для обміну повідомленнями, у SimpleX немає ідентифікаторів, призначених користувачам. Він не покладається на номери телефонів, адреси на основі доменів (як електронна пошта або XMPP), імена користувачів, публічні ключі або навіть випадкові числа для ідентифікації своїх користувачів — оператори серверів SimpleX не знають, скільки людей використовують їхні сервери.", "simplex-unique-overlay-card-3-p-3": "На відміну від федеративних мереж серверів (електронна пошта, XMPP або Matrix), сервери SimpleX не зберігають облікові записи користувачів, вони лише ретранслюють повідомлення, захищаючи приватність обох сторін.", "simplex-unique-card-3-p-2": "Зашифровані повідомлення від кінця до кінця тимчасово зберігаються на ретрансляційних серверах SimpleX до їх отримання, після чого вони назавжди видаляються.", - "simplex-unique-card-4-p-1": "Мережа SimpleX є повністю децентралізованою та незалежною від будь-якої криптовалюти чи іншої платформи, крім Інтернету.", + "simplex-unique-card-4-p-1": "Мережа SimpleX повністю децентралізована та незалежна від будь-якої криптовалюти або будь-якої іншої мережі, окрім Інтернету.", "simplex-network-overlay-card-1-li-3": "P2P не вирішує проблему атаки MITM, і більшість існуючих реалізацій не використовують поза каналом повідомлень для початкового обміну ключами. SimpleX використовує поза каналом повідомлень або, у деяких випадках, передбачені і безпечні з'єднання для початкового обміну ключами.", - "simplex-network-overlay-card-1-li-4": "Реалізації P2P можуть бути блоковані деякими інтернет-провайдерами (наприклад, BitTorrent). SimpleX є транспортно-агностичним - він може працювати через стандартні веб-протоколи, наприклад, WebSockets.", + "simplex-network-overlay-card-1-li-4": "Реалізації P2P можуть бути заблоковані деякими інтернет-провайдерами (наприклад, BitTorrent). SimpleX є незалежним від транспорту — він може працювати через стандартні веб-протоколи, наприклад, WebSockets.", "simplex-unique-card-4-p-2": "Ви можете використовувати SimpleX з власними серверами або з серверами, які ми надаємо — і все одно підключатися до будь-якого користувача.", "contact-hero-p-1": "Публічні ключі та адреса черги повідомлень в цьому посиланні НЕ відправляються по мережі під час перегляду цієї сторінки — вони містяться в хеш-фрагменті URL-посилання.", "scan-qr-code-from-mobile-app": "Сканувати QR-код з мобільного додатка", @@ -46,7 +46,7 @@ "guide-dropdown-8": "Налаштування додатка", "guide-dropdown-9": "Створення підключень", "guide": "Посібник", - "docs-dropdown-1": "Платформа SimpleX", + "docs-dropdown-1": "SimpleX мережа", "docs-dropdown-2": "Доступ до файлів Android", "docs-dropdown-3": "Доступ до бази даних чату", "docs-dropdown-4": "Хостинг сервера SMP", @@ -71,14 +71,14 @@ "simplex-explained-tab-1-p-1": "Ви можете створювати контакти та групи і вести двосторонні розмови, як у будь-якому іншому месенджері.", "simplex-explained-tab-1-p-2": "Як це може працювати з однобічними чергами та без ідентифікаторів профілю користувача?", "simplex-explained-tab-2-p-1": "Для кожного з'єднання ви використовуєте дві окремі черги обміну повідомленнями для відправки та отримання повідомлень через різні сервери.", - "simplex-explained-tab-2-p-2": "Сервери передають повідомлення тільки в одному напрямку, не маючи повної картини розмови або підключень користувача.", + "simplex-explained-tab-2-p-2": "Сервери передають повідомлення тільки в одному напрямку, не маючи повної картини розмов або підключень користувача.", "simplex-explained-tab-3-p-1": "Сервери мають окремі анонімні облікові дані для кожної черги і не знають, які користувачі до них відносяться.", "simplex-explained-tab-3-p-2": "Користувачі можуть додатково підвищити конфіденційність метаданих, використовуючи Tor для доступу до серверів, що запобігає кореляції за IP-адресою.", "chat-bot-example": "Приклад чат-бота", "smp-protocol": "Протокол SMP", "chat-protocol": "Протокол чату", "donate": "Пожертвувати", - "copyright-label": "© 2020-2024 SimpleX | Проект з відкритим кодом", + "copyright-label": "© 2020-2025 SimpleX | Проект з відкритим кодом", "simplex-chat-protocol": "Протокол чату SimpleX", "terminal-cli": "Термінал CLI", "hero-header": "Приватність переосмислена", @@ -116,7 +116,7 @@ "simplex-private-card-4-point-2": "Для використання SimpleX через Tor, будь ласка, встановіть додаток Orbot та активуйте SOCKS5-проксі (або VPN на iOS).", "simplex-private-card-5-point-1": "SimpleX використовує наповнення вмісту для кожного шару шифрування для ускладнення атак за розміром повідомлень.", "simplex-private-card-5-point-2": "Це забезпечує, що повідомлення різних розмірів виглядають однаково для серверів та спостерігачів мережі.", - "simplex-private-card-6-point-1": "Багато комунікаційних платформ вразливі до MITM-атак серверів чи постачальників мережі.", + "simplex-private-card-6-point-1": "Багато комунікаційних мереж вразливі до MITM-атак серверів чи постачальників мережі.", "simplex-private-card-6-point-2": "Щоб запобігти цьому, програми SimpleX передають одноразові ключі позаканально, коли ви ділитесь адресою як посиланням або QR-кодом.", "simplex-private-card-7-point-2": "Якщо будь-яке повідомлення додається, вилучається чи змінюється, отримувач буде проінформований.", "simplex-private-card-8-point-1": "Сервери SimpleX виступають як вузли низької затримки для змішування — вхідні та вихідні повідомлення мають різний порядок.", @@ -140,7 +140,7 @@ "simplex-unique-4-title": "Ви власник мережі SimpleX", "simplex-unique-4-overlay-1-title": "Повністю децентралізована — користувачі володіють мережею SimpleX", "hero-overlay-card-1-p-1": "Багато користувачів запитували: якщо у SimpleX немає ідентифікаторів користувачів, як він може знати, куди відправити повідомлення?", - "hero-overlay-card-1-p-2": "Для доставки повідомлень, замість ідентифікаторів користувачів, які використовують усі інші платформи, SimpleX використовує тимчасові анонімні парні ідентифікатори черг повідомлень, окремі для кожного вашого підключення — тут немає довгострокових ідентифікаторів.", + "hero-overlay-card-1-p-2": "Для доставки повідомлень, замість ідентифікаторів користувачів, які використовують усі інші мережі, SimpleX використовує тимчасові анонімні парні ідентифікатори черг повідомлень, окремі для кожного вашого підключення — тут немає довгострокових ідентифікаторів.", "hero-overlay-card-1-p-3": "Ви визначаєте, який(кі) сервер(и) використовувати для отримання повідомлень, ваші контакти — сервери, які ви використовуєте для відправки повідомлень їм. Кожна розмова, ймовірно, використовує два різних сервери.", "hero-overlay-card-1-p-4": "Цей дизайн запобігає витоку метаданих будь-яких користувачів на рівні додатка. Для подальшого покращення конфіденційності та захисту вашої IP-адреси ви можете підключитися до серверів обміну повідомленнями через Tor.", "hero-overlay-card-1-p-6": "Докладніше читайте у білетному запису SimpleX.", @@ -152,19 +152,19 @@ "simplex-network-overlay-card-1-li-2": "У дизайні SimpleX, на відміну від більшості P2P-мереж, немає жодних глобальних ідентифікаторів користувачів будь-якого виду, навіть тимчасових, та використовуються лише тимчасові парні ідентифікатори, що забезпечує кращу анонімність та захист метаданих.", "privacy-matters-overlay-card-1-p-1": "Багато великих компаній використовують інформацію про те, з ким ви з'єднані, щоб оцінити ваш дохід, продавати вам продукти, які вам дійсно не потрібні, і визначати ціни.", "privacy-matters-overlay-card-3-p-3": "Звичайних людей арештовують за те, що вони публікують онлайн, навіть через свої 'анонімні' облікові записи, навіть у демократичних країнах.", - "simplex-unique-overlay-card-1-p-2": "Для доставки повідомлень SimpleX використовує парні анонімні адреси однобічних черг повідомлень, окремо для отриманих та відправлених повідомлень, зазвичай через різні сервери. Використання SimpleX схоже на наявність різної “витратної” електронної пошти або телефону для кожного контакту, і немає неприємностей у їх управлінні.", - "simplex-unique-overlay-card-1-p-3": "Цей дизайн захищає конфіденційність осіб, з якими ви спілкуєтеся, приховуючи це від серверів платформи SimpleX та будь-яких спостерігачів. Щоб сховати свою IP-адресу від серверів, ви можете підключитися до серверів SimpleX через Tor.", - "simplex-unique-overlay-card-2-p-1": "Оскільки у вас немає ідентифікатора на платформі SimpleX, ніхто не може з вами зв'язатися, якщо ви не поділитеся одноразовою або тимчасовою адресою користувача, у вигляді QR-коду або посилання.", + "simplex-unique-overlay-card-1-p-2": "Для доставки повідомлень SimpleX використовує парні анонімні адреси однобічних черг повідомлень, окремо для отриманих та відправлених повідомлень, зазвичай через різні сервери.", + "simplex-unique-overlay-card-1-p-3": "Цей дизайн захищає конфіденційність того, з ким ви спілкуєтесь, приховуючи це від серверів мережі SimpleX та від будь-яких спостерігачів. Щоб приховати вашу IP-адресу від серверів, ви можете підключатися до серверів SimpleX через Tor.", + "simplex-unique-overlay-card-2-p-1": "Оскільки у вас немає ідентифікатора в мережі SimpleX, ніхто не може зв'язатися з вами, якщо ви не поділитеся одноразовою або тимчасовою адресою користувача, як QR-кодом або посиланням.", "simplex-unique-overlay-card-2-p-2": "Навіть з необов'язковою адресою користувача, яка може бути використана для відправки спамових запитань на зв'язок, ви можете змінити або повністю видалити її, не втрачаючи жодного з ваших з'єднань.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat зберігає всі дані користувачів лише на пристроях клієнтів за допомогою переносного зашифрованого формату бази даних, який можна експортувати і передавати на будь-який підтримуваний пристрій.", "simplex-unique-overlay-card-3-p-2": "Зашифровані повідомлення від кінця до кінця тимчасово зберігаються на ретрансляційних серверах SimpleX до їх отримання, після чого вони назавжди видаляються.", "simplex-unique-overlay-card-3-p-4": "Між надісланим і отриманим серверним трафіком немає спільних ідентифікаторів чи шифрованого тексту — якщо хтось його спостерігає, він не може легко визначити, хто спілкується з ким, навіть якщо TLS скомпрометовано.", - "simplex-unique-overlay-card-4-p-1": "Ви можете використовувати SimpleX з власними серверами і все одно спілкуватися з людьми, які використовують надані нам сервери заздалегідь налаштовані.", - "simplex-unique-overlay-card-4-p-2": "Платформа SimpleX використовує відкритий протокол та надає SDK для створення чат-ботів, що дозволяє впроваджувати сервіси, з якими користувачі можуть взаємодіяти через додатки SimpleX Chat — ми дійсно чекаємо, які сервіси SimpleX ви зможете побудувати.", - "simplex-unique-overlay-card-4-p-3": "Якщо ви розглядаєте можливість розробки для платформи SimpleX, наприклад, чат-бота для користувачів додатку SimpleX або інтеграції бібліотеки SimpleX Chat у свої мобільні додатки, будь ласка, зв'яжіться з нами для отримання порад та підтримки.", - "simplex-unique-card-1-p-1": "SimpleX захищає конфіденційність вашого профілю, контактів та метаданих, приховуючи їх від серверів платформи SimpleX та будь-яких спостерігачів.", - "simplex-unique-card-1-p-2": "На відміну від будь-якої іншої існуючої платформи обміну повідомленнями, SimpleX не має ідентифікаторів, призначених користувачам — навіть випадкових чисел.", - "simplex-unique-card-2-p-1": "Оскільки у вас немає ідентифікатора або фіксованої адреси на платформі SimpleX, ніхто не може з вами зв'язатися, якщо ви не поділитесь одноразовою або тимчасовою адресою користувача, як QR-код або посиланням.", + "simplex-unique-overlay-card-4-p-1": "Ви можете використовувати SimpleX зі своїми власними серверами і при цьому спілкуватися з людьми, які використовують сервери, попередньо налаштовані в додатках.", + "simplex-unique-overlay-card-4-p-2": "Мережа SimpleX використовує відкритий протокол і надає SDK для створення чат-ботів, що дозволяє реалізувати сервіси, з якими користувачі можуть взаємодіяти через додатки SimpleX Chat — ми справді чекаємо, щоб побачити, які сервіси SimpleX ви створите.", + "simplex-unique-overlay-card-4-p-3": "Якщо ви плануєте розробляти для мережі SimpleX, наприклад, чат-бота для користувачів додатка SimpleX або інтеграцію бібліотеки SimpleX Chat у ваші мобільні додатки, будь ласка, зв'яжіться з нами для отримання порад та підтримки.", + "simplex-unique-card-1-p-1": "SimpleX захищає конфіденційність вашого профілю, контактів та метаданих, приховуючи їх від серверів мережі SimpleX та будь-яких спостерігачів.", + "simplex-unique-card-1-p-2": "На відміну від будь-якої іншої існуючої мережі для обміну повідомленнями, у SimpleX немає ідентифікаторів, призначених користувачам — не навіть випадкових чисел.", + "simplex-unique-card-2-p-1": "Оскільки у вас немає ідентифікатора або фіксованої адреси в мережі SimpleX, ніхто не може зв'язатися з вами, якщо ви не поділитеся одноразовою або тимчасовою адресою користувача, як QR-кодом або посиланням.", "simplex-unique-card-3-p-1": "SimpleX зберігає всі дані користувачів на пристроях клієнтів у переносному зашифрованому форматі бази даних — його можна передавати на інший пристрій.", "join": "Приєднатися", "we-invite-you-to-join-the-conversation": "Ми запрошуємо вас приєднатися до розмови", @@ -174,8 +174,7 @@ "sign-up-to-receive-our-updates": "Підпишіться, щоб отримувати наші оновлення", "enter-your-email-address": "Введіть свою електронну адресу", "get-simplex": "Отримати SimpleX додаток для настільних комп'ютерів", - "why-simplex-is": "Чому SimpleX є", - "unique": "унікальним", + "why-simplex-is-unique": "Чому SimpleX є унікальним", "learn-more": "Дізнатися більше", "more-info": "Додаткова інформація", "hide-info": "Приховати інформацію", @@ -192,7 +191,7 @@ "installing-simplex-chat-to-terminal": "Встановлення SimpleX Chat для терміналу", "use-this-command": "Використовуйте цю команду:", "see-simplex-chat": "Дивіться SimpleX Chat", - "the-instructions--source-code": "інструкції з того, як його завантажити чи скомпілювати з вихідного коду.", + "the-instructions--source-code": "для інструкції з того, як його завантажити чи скомпілювати з вихідного коду.", "if-you-already-installed-simplex-chat-for-the-terminal": "Якщо ви вже встановили SimpleX Chat для терміналу", "if-you-already-installed": "Якщо ви вже встановили", "simplex-chat-for-the-terminal": "SimpleX Chat для терміналу", @@ -228,7 +227,7 @@ "no-decentralized": "Ні - децентралізовано", "comparison-section-list-point-4": "Якщо сервери оператора порушені. Перевірте безпековий код в Signal та деяких інших додатках для зменшення ризику", "comparison-section-list-point-5": "Не захищає конфіденційність метаданих користувачів", - "comparison-section-list-point-6": "Хоча P2P є розподіленими, вони не є федеративними - вони працюють як єдина мережа", + "comparison-section-list-point-6": "Хоча P2P є розподіленими, вони не є федеративними — вони працюють як одна мережа", "guide-dropdown-1": "Швидкий старт", "guide-dropdown-2": "Відправлення повідомлень", "guide-dropdown-3": "Таємні групи", diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index c32bc2f123..c0b843ca9e 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -24,7 +24,7 @@ "simplex-network-overlay-card-1-li-2": "与大多数 P2P 网络不同,SimpleX 在设计上没有任何类型的全局用户标识符,甚至临时的也没有。SimpleX 仅使用临时的成对标识符,提供更好的匿名性和元数据保护。", "privacy-matters-overlay-card-1-p-1": "许多大型公司会使用您的人际关系来估算您的收入,决定商品的价格,并向您兜售您并不真正需要的产品。", "simplex-unique-overlay-card-3-p-4": "发送和接收的服务器流量之间没有共同的标识符或密文—— 如果有人在观察它,他们也无法轻易确定谁与谁通信,即使 TLS 受到威胁。", - "simplex-unique-card-4-p-1": "SimpleX 网络是完全去中心化的,并且独立于任何加密货币或除互联网以外的任何其他平台。", + "simplex-unique-card-4-p-1": "SimpleX 网络是完全去中心化的,并且独立于任何加密货币或除互联网以外的任何其他网络。", "join": "加入", "get-simplex": "获取 SimpleX desktop app", "hide-info": "隐藏信息", @@ -49,15 +49,15 @@ "simplex-explained": "SimpleX 简述", "simplex-explained-tab-3-text": "3. 服务器能看到什么", "hero-header": "重新定义隐私", - "simplex-explained-tab-1-p-2": "它是如何在单向消息队列与没有用户识别符的情况下工作的?", - "simplex-explained-tab-2-p-2": "服务器只会单向传输消息,而无法掌握用户的会话或连接的全貌。", + "simplex-explained-tab-1-p-2": "它是如何利用单向消息队列并不利用用户识别符工作的?", + "simplex-explained-tab-2-p-2": "服务器只会单向传输消息,而无法掌握用户的对话或连接的全貌。", "simplex-explained-tab-3-p-1": "服务器对每个队列都有单独的匿名凭证,并且不知道这些凭证属于哪些用户。", "donate": "捐赠", "simplex-explained-tab-2-p-1": "对于每个连接,您都会使用两个单独的消息队列,通过不同的服务器发送和接收消息。", "simplex-chat-protocol": "SimpleX 聊天协议", "smp-protocol": "SMP协议", "chat-protocol": "聊天协议", - "copyright-label": "© 2020-2024 SimpleX | 开源项目", + "copyright-label": "© 2020-2025 SimpleX | 开源项目", "terminal-cli": "命令行程式", "simplex-explained-tab-1-p-1": "您可以创建联系人和群组,并进行双向对话,就像是任何其他即时通讯软件一样。", "hero-p-1": "其他应用——如Signal、Matrix、Session、Briar、Jami、Cwtch 等——都需要用户 ID。
而SimpleX 不需要用户ID,连随机生成的也不需要。
这从根本上改善了您的隐私。", @@ -104,7 +104,7 @@ "simplex-private-card-10-point-1": "SimpleX 为每个用户的联系人或群组成员均使用临时匿名成对地址和凭据。", "privacy-matters-3-title": "因无辜结社而被起诉", "privacy-matters-2-overlay-1-title": "隐私给您力量", - "hero-overlay-card-1-p-2": "为了传递消息,SimpleX 使用消息队列的临时匿名成对标识符(而非其他平台使用的用户 ID)来区分您的每个连接——没有长期标识符这种东西。", + "hero-overlay-card-1-p-2": "SimpleX 使用消息队列的临时匿名成对标识符(而非其他网络使用的用户 ID)来传递消息,而每个连接的标识符均不尽相同 — 没有长期标识符这种东西。", "hero-overlay-card-1-p-3": "您定义用于接收消息的服务器、您的联系人—— 您用来向他们发送消息的服务器。 每个会话都很可能会使用两个不同的服务器。", "hero-overlay-card-1-p-4": "此设计可防止在应用程序级别泄漏任何用户元数据。 为了进一步改善隐私并保护您的 IP 地址,您可以通过 Tor 连接到消息服务器。", "hero-overlay-card-1-p-5": "只有客户端设备存储用户配置文件、联系人和群组; 消息均使用两层端到端加密发送。", @@ -115,35 +115,35 @@ "hero-overlay-card-2-p-3": "即使是使用 Tor v3 服务的最私密的应用程序,如果您通过同一个人资料与两个不同的联系人交谈,他们也可以证明他们与同一个人有联系。", "hero-overlay-card-2-p-4": "为防止这些攻击,SimpleX 在其设计中不包含任何用户 ID 。 而且,如果您使用隐身模式,您将为每个联系人显示不同的名称,避免他们之间共享任何数据。", "simplex-network-overlay-card-1-li-1": "P2P 网络依赖于 分布式散列表(DHT) 的某些变体来路由消息。 DHT 在设计上必须平衡可达性和延迟。 SimpleX 比 P2P 具有更好的可达性和更低的延迟,因为消息可以通过通讯双方选择的多个服务器并行地冗余传递。若是在 P2P 网络中,消息则需要使用算法选择,并依次通过 O(log N) 个节点。", - "simplex-network-overlay-card-1-li-4": "P2P 实现(如 BitTorrent)可能会被某些互联网提供商阻止。 SimpleX 与传输协议无关——它可以在标准网络协议上工作,例如 WebSockets。", + "simplex-network-overlay-card-1-li-4": "P2P 实现(如 BitTorrent)可能会被某些互联网提供商阻止。 SimpleX 与传输协议无关 — 它可以在标准网络协议上工作,例如 WebSockets。", "simplex-network-overlay-card-1-li-3": "P2P 并未解决中间人攻击(MITM Attack) 问题。大多数现有的 P2P 实现没有使用带外通讯来进行初始密钥的交换,而 SimpleX 使用带外通讯,或者在某些情况下,使用预先存在的安全和可信连接来进行初始密钥交换。", "simplex-network-overlay-card-1-li-5": "所有已知的 P2P 网络都可能受到 Sybil 攻击,因为每个节点都是可发现的,并且网络作为一个整体运行。 已知的缓解措施不是需要一个中心化的组件就是需要昂贵的工作量证明。而 SimpleX 网络没有服务器可发现性,它是碎片化的并且作为多个隔离的子网运行,这样全网络范围的攻击便无从实现。", "simplex-network-overlay-card-1-li-6": "P2P 网络可能受到 DRDoS 攻击 。客户端有能力重新广播和放大流量,从而导致整个网络范围内的服务中断。 SimpleX 客户端仅中继来自已知连接的流量,因此不能被攻击者用来放大整个网络的流量。", "privacy-matters-overlay-card-1-p-2": "在线零售商知道收入较低的人更有可能在紧急情况下购买商品,因此他们可能会收取更高的价格或取消折扣。", "privacy-matters-overlay-card-1-p-3": "一些金融和保险公司使用社交图谱来确定利率和保费。 它通常会让收入较低的人支付更多—— 它被称为“贫困溢价”。", - "privacy-matters-overlay-card-1-p-4": "SimpleX 平台比任何替代方案都能更好地保护您人际关系层面的隐私,防止您的社交图谱被任何公司或组织使用。 即使人们使用 SimpleX Chat 提供的服务器,我们也不知道用户数量或他们的连接数。", + "privacy-matters-overlay-card-1-p-4": "SimpleX 网络比任何替代方案都能更好地保护您人际关系层面的隐私,防止您的社交图谱被任何公司或组织使用。 即使人们使用 SimpleX Chat 应用预配置的服务器,服务器运营方也不知道用户数量或他们的连接数。", "privacy-matters-overlay-card-2-p-1": "不久前,我们观察到几次大选被一家知名咨询公司操纵,该公司使用我们的社交图谱扭曲我们对现实世界的看法并操纵我们的选票。", - "privacy-matters-overlay-card-2-p-2": "为了客观并做出独立的决定,您需要控制您的信息空间。 而这只有当您使用没有能力访问您的社交图谱的,注重隐私的通信平台时,这才有可能。", - "privacy-matters-overlay-card-2-p-3": "SimpleX 是第一个没有设计任何用户标识符的平台,这样能比任何已知的替代方案都更好地保护您的连接图谱。", + "privacy-matters-overlay-card-2-p-2": "为了客观并做出独立的决定,您需要控制您的信息空间。 而这只有当您使用没有能力访问您的社交图谱的,注重隐私的通信网络时,这才有可能。", + "privacy-matters-overlay-card-2-p-3": "SimpleX 是第一个没有设计任何用户标识符的网络,这样能比任何已知的替代方案都更好地保护您的连接图谱。", "privacy-matters-overlay-card-3-p-2": "最令人震惊的故事之一是 Mohamedou Ould Salahi 在他的回忆录中描述并在毛里塔尼亚电影中展示的经历。 他在未经审判的情况下被关进关塔那摩集中营,并在打电话给他在阿富汗的亲戚后在那里遭受了 15 年的折磨,他被怀疑参与了 9/11 袭击,尽管他在过去 10 年住在德国。", "privacy-matters-overlay-card-3-p-1": "每个人都应该关心他们通信的隐私和安全——无害的谈话会让您处于危险之中,即使您没有什么可隐瞒的。", "privacy-matters-overlay-card-3-p-3": "普通人会因为他们在网上分享的内容而被捕,即使是通过他们的“匿名”帐户,即使是在民主国家。", "privacy-matters-overlay-card-3-p-4": "使用端到端加密的即时通讯软件还不够,我们都应该使用保护我们个人网络隐私——即我们与谁有联系的即时通讯软件。", - "simplex-unique-overlay-card-1-p-1": "与其他消息传递平台不同,SimpleX 没有分配给用户的标识符。 它不依赖电话号码、基于域的地址(如电子邮件或 XMPP)、用户名、公钥甚至随机数来识别其用户—— 我们不知道有多少人使用我们的 SimpleX 服务器。", - "simplex-unique-overlay-card-1-p-2": "为了传递消息,SimpleX 使用单向消息队列的成对匿名地址,通常通过不同的服务器将接收和发送的消息分开。 使用 SimpleX 就像为每个联系人使用不同的即时电子邮件或电话,并且无需管理它们。", - "simplex-unique-overlay-card-1-p-3": "这种设计保护了您正在与之通信的人的隐私,将其隐藏在 SimpleX 平台服务器和任何观察者之外。 要对服务器隐藏您的 IP 地址,您可以通过 Tor 连接到 SimpleX 服务器。", - "simplex-unique-overlay-card-4-p-1": "您可以将 SimpleX 与您自己的服务器一起使用,并且仍然可以与使用我们提供的预配置服务器的人进行通信。", + "simplex-unique-overlay-card-1-p-1": "与其他消息网络不同,SimpleX 没有分配给用户的标识符。 它不依赖电话号码、基于域的地址(如电子邮件或 XMPP)、用户名、公钥甚至随机数来识别其用户—— SimpleX 服务器运营方不知道有多少人使用其服务器。", + "simplex-unique-overlay-card-1-p-2": "为了传递消息,SimpleX 使用单向消息队列的成对匿名地址,通常通过不同的服务器将接收和发送的消息分开。", + "simplex-unique-overlay-card-1-p-3": "这种设计保护了您正在与之通信的人的隐私,将其隐藏在 SimpleX 网络的服务器和任何观察者之外。 要对服务器隐藏您的 IP 地址,您可以通过 Tor 连接到 SimpleX 服务器。", + "simplex-unique-overlay-card-4-p-1": "您可以将 SimpleX 与您自己的服务器一起使用,并且仍然可以与使用应用中预配置服务器的人们进行通信。", "simplex-unique-card-3-p-2": "端到端加密的消息在被收到前会暂时保存在 SimpleX 中继服务器上,传送完成后它们会被永久删除。", - "simplex-unique-card-1-p-1": "SimpleX 保护您的个人资料、联系人和元数据的隐私,将其隐藏在 SimpleX 平台服务器和任何观察者之外。", - "simplex-unique-overlay-card-4-p-3": "如果您正在考虑为在SimpleX 平台上开发,例如,为 SimpleX 应用程序用户开发聊天机器人,或将 SimpleX 聊天库集成到您的移动应用程序中,请 联系我们 以获取建议和支持。", + "simplex-unique-card-1-p-1": "SimpleX 保护您的个人资料、联系人和元数据的隐私,不让 SimpleX 网络服务器和任何观察者看到它们。", + "simplex-unique-overlay-card-4-p-3": "如果您正在考虑为在SimpleX 网络上开发,例如,为 SimpleX 应用程序用户开发聊天机器人,或将 SimpleX 聊天库集成到您的移动应用程序中,请 联系我们 以获取建议和支持。", "simplex-unique-card-4-p-2": "您可以搭配自己的服务器来使用 SimpleX 或使用我们提供的服务器 — 并仍然连接到任何用户。", - "simplex-unique-card-2-p-1": "因为您在 SimpleX 平台上没有标识符或固定地址,所以除非您以二维码或链接的形式分享一次性或临时用户地址,没有人可以联系您。", + "simplex-unique-card-2-p-1": "因为您在 SimpleX 网络上没有标识符或固定地址,所以除非您以二维码或链接的形式分享一次性或临时用户地址,没有人可以联系您。", "simplex-unique-card-3-p-1": "SimpleX 以便携式加密数据库格式将所有用户数据存储在客户端设备上—— 它可以转移到另一个设备。", "sign-up-to-receive-our-updates": "注册以接收我们的更新", "we-invite-you-to-join-the-conversation": "我们邀请您加入对话", "enter-your-email-address": "输入您的电子邮箱地址", "join-the-REDDIT-community": "加入 REDDIT 社区", - "why-simplex-is": "为什么 SimpleX 是", + "why-simplex-is-unique": "为什么 SimpleX 是 独特的", "learn-more": "了解更多", "join-us-on-GitHub": "在 GitHub 上加入我们", "more-info": "更多信息", @@ -164,7 +164,7 @@ "github-repository": "GitHub 仓库", "copy-the-command-below-text": "复制下面的命令并在聊天中使用它:", "privacy-matters-section-header": "为什么隐私很重要", - "the-instructions--source-code": "如何从源代码下载或编译它的说明。", + "the-instructions--source-code": "如何下载或从源代码下载进行编译的说明。", "if-you-already-installed-simplex-chat-for-the-terminal": "如果您已经为终端安装了 SimpleX Chat", "if-you-already-installed": "如果您已经安装", "simplex-chat-for-the-terminal": "用于终端的 SimpleX Chat", @@ -197,17 +197,16 @@ "comparison-section-list-point-4": "如果运营商的服务器受到威胁。 验证 Signal 和其他一些应用程序中的安全代码以缓解该问题", "comparison-section-list-point-1": "通常基于电话号码,在某些情况下基于用户名", "comparison-section-list-point-2": "基于 DNS 的地址", - "comparison-section-list-point-6": "P2P 是分布式的,而非联邦式的 - 它们作为单个网络运行", + "comparison-section-list-point-6": "P2P 是分布式的,但并非联邦式的 — 它们作为单个网络运行", "see-here": "参见此处", "comparison-section-list-point-7": "P2P 网络要么拥有中央权威,要么整个网络可能被攻陷", "simplex-private-card-5-point-1": "SimpleX 为每个加密层进行内容填充来对抗长度扩展攻击。", - "simplex-unique-overlay-card-4-p-2": "SimpleX 平台使用开放协议并提供用于创建聊天机器人的 SDK, 允许用户实现通过 SimpleX Chat 应用程序与之交互的服务—— 我们真的很期待看到您可以依托SimpleX构建哪些服务。", + "simplex-unique-overlay-card-4-p-2": "SimpleX 网络使用开放协议并提供用于创建聊天机器人的 SDK, 允许用户实现通过 SimpleX Chat 应用程序与之交互的服务—— 我们真的很期待看到您会依托SimpleX构建哪些服务。", "simplex-unique-overlay-card-2-p-2": "即使使用可选的用户地址,当它被用于发送垃圾邮件联系请求,您可以更改或完全删除它而不会丢失任何连接。", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat 使用便携式加密数据库格式仅将所有用户数据存储在客户端设备上,该格式可以导出并传输到任何支持的设备。", "donate-here-to-help-us": "在这里捐款来帮助我们", - "unique": "独特的", - "simplex-unique-card-1-p-2": "与任何其他现有的消息传递平台不同,SimpleX 没有分配给用户的标识符—— 甚至随机数也没有。", - "simplex-unique-overlay-card-2-p-1": "因为您在 SimpleX 平台上没有标识符,所以除非您以二维码或链接的形式分享一次性或临时用户地址,没有人可以联系您。", + "simplex-unique-card-1-p-2": "与任何其他现有的消息传递网络不同,SimpleX 没有分配给用户的标识符—— 甚至随机数也没有。", + "simplex-unique-overlay-card-2-p-1": "因为您在 SimpleX 网络上没有标识符,所以除非您以二维码或链接的形式分享一次性或临时用户地址,没有人可以联系您。", "simplex-unique-overlay-card-3-p-2": "端到端加密的消息在被收到前会暂时保存在 SimpleX 中继服务器上,传送完成后它们会被永久删除。", "simplex-unique-overlay-card-3-p-3": "与联合网络服务器(电子邮件、XMPP 或 Matrix)不同,SimpleX 服务器不存储用户帐户,它们仅中继消息,保护双方的隐私。", "guide-dropdown-2": "发送消息", @@ -224,7 +223,7 @@ "click-to-see": "点击查看", "menu": "菜单", "guide-dropdown-1": "快速指南", - "docs-dropdown-1": "SimpleX 平台", + "docs-dropdown-1": "SimpleX 网络", "docs-dropdown-5": "托管 XFTP 服务器", "newer-version-of-eng-msg": "本页面有较新的英语版本。", "guide-dropdown-4": "聊天档案", diff --git a/website/langs/zh_Hant.json b/website/langs/zh_Hant.json index 0967ef424b..324360cb19 100644 --- a/website/langs/zh_Hant.json +++ b/website/langs/zh_Hant.json @@ -1 +1,206 @@ -{} +{ + "home": "开始", + "developers": "開發人員", + "reference": "參考", + "blog": "部落格", + "features": "特徵", + "why-simplex": "為什麼選擇SimpleX", + "simplex-privacy": "SimpleX 隱私", + "simplex-network": "SimpleX 網路", + "simplex-explained": "SimpleX 解釋", + "simplex-explained-tab-1-text": "1. 用戶體驗", + "simplex-explained-tab-2-text": "2. 它是如何工作的", + "simplex-explained-tab-3-text": "3. 伺服器可以看到什麼", + "terminal-cli": "終端 CLI", + "hero-header": "重新定義隱私", + "hero-subheader": "第一個沒有 User ID 的 Messenger", + "simplex-explained-tab-3-p-2": "用戶可以通過使用 Tor 訪問伺服器來進一步提高元數據隱私,防止按 IP 位址進行序列化。", + "smp-protocol": "SMP 協定", + "simplex-explained-tab-2-p-2": "伺服器僅單向傳遞消息,無法全面瞭解使用者的對話記錄或連接。", + "simplex-explained-tab-2-p-1": "對於每個連接,您可以使用兩個單獨的消息佇列通過不同的伺服器發送和接收消息。", + "chat-protocol": "聊天協定", + "copyright-label": "© 2020-2025 SimpleX |開源專案", + "donate": "捐助", + "simplex-explained-tab-1-p-1": "您可以創建聯繫人和群組,並進行雙向對話,就像在任何其他 Messenger 中一樣。", + "simplex-explained-tab-1-p-2": "它如何在沒有使用者配置檔標識符的情況下使用單向佇列?", + "simplex-explained-tab-3-p-1": "伺服器對每個佇列都有單獨的匿名憑證,並且不知道它們屬於哪些使用者。", + "chat-bot-example": "聊天機器人示例", + "simplex-chat-protocol": "SimpleX Chat 協定", + "terms-and-privacy-policy": "隱私策略", + "hero-overlay-card-3-p-3": "Trail of Bits 在 2024 年 7 月檢閱了 SimpleX 網路通訊協定的加密設計。閱讀更多。", + "hero-overlay-3-textlink": "安全評估", + "hero-overlay-card-3-p-2": "Trail of Bits 在 2022 年 11 月檢閱了 SimpleX 網路加密和網路元件。閱讀更多。", + "hero-overlay-1-textlink": "用戶ID為何有害私隱?", + "hero-overlay-2-textlink": "SimpleX如何工作?", + "simplex-private-card-5-point-2": "它讓不同長度的訊息在伺服器和網路觀察者看來相同。", + "simplex-private-card-10-point-1": "對用戶與群組成員,SimpleX使用臨時、匿名的地址對與憑證對。", + "privacy-matters-1-title": "廣告投放與價格歧視", + "privacy-matters-1-overlay-1-linkText": "私隱為您省錢", + "privacy-matters-2-title": "操縱選舉", + "privacy-matters-2-overlay-1-title": "私隱給您權力", + "privacy-matters-2-overlay-1-linkText": "私隱給您權力", + "privacy-matters-3-overlay-1-title": "私隱保護您的自由", + "privacy-matters-3-overlay-1-linkText": "私隱保護您的自由", + "simplex-unique-4-title": "您擁有SimpleX網路", + "simplex-unique-4-overlay-1-title": "完全去中心化—用戶擁有 SimpleX 網路", + "hero-overlay-card-1-p-4": "此設計可防止在應用程式層級洩漏任何使用者' 元資料。為了進一步改善隱私並保護您的 IP 位址,您可以透過 Tor 連接到訊息伺服器。", + "hero-overlay-card-1-p-5": "只有用戶端會儲存使用者設定檔、聯絡人和群組;訊息傳送採用雙層端對端加密。", + "hero-overlay-card-2-p-2": "然後他們可以將這些資訊與現有的公開社交網路進行關聯,並確定一些真實身份。", + "simplex-unique-card-1-p-1": "SimpleX 保護您的個人資料、聯絡人和元資料的隱私,使其對 SimpleX 網路伺服器和任何觀察者隱藏。", + "scan-qr-code-from-mobile-app": "用手機應用程式掃描二維碼", + "scan-the-qr-code-with-the-simplex-chat-app": "使用SimpleX Chat應用程式掃描二維碼", + "protocol-3-text": "點對點協議", + "hero-p-1": "其他聊天應用,如Signal、Matrix、Session、Briar、Jami、Cwtch等,使用用戶ID;SimpleX不這樣做。它甚至不使用隨機數。這種做法極大地保護用戶私隱。", + "hero-2-header-desc": "此視訊教您以一次性QR碼、親身或以視訊連結的方式與聯繫人連線。您也可以通過分享邀請連結與人連線。", + "feature-2-title": "端到端加密的圖像、視訊、檔案", + "simplex-private-card-9-point-2": "與傳統的訊息代理相比,它可減小攻擊媒介及可見的詮釋資料量。", + "simplex-private-10-title": "臨時匿名標識符對", + "simplex-private-card-1-point-1": "雙棘輪協定—帶有前向保密、入侵恢復特質的不留記錄即時通訊協定。", + "privacy-matters-3-title": "因無辜關聯而被起訴", + "signing-key-fingerprint": "簽名密鑰指紋(SHA-256)", + "hero-overlay-card-1-p-3": "您決定用哪個/哪些伺服器接收訊息。您的聯絡人用此伺服器向您傳送訊息。往往每個對話都使用兩台不同的伺服器。", + "docs-dropdown-2": "訪問Android檔案", + "simplex-unique-card-2-p-1": "由於您在 SimpleX 網路上沒有任何標識符或固定地址,因此除非您分享一次性或臨時地址 (如 二維碼或連結),任何人都無法與您聯絡。", + "see-here": "見此", + "privacy-matters-overlay-card-3-p-4": "僅使用端對端加密的通訊工具是不夠的,我們都應使用能保護個人網路隱私的通訊工具。", + "hero-overlay-card-1-p-1": "很多用戶問: 如果 SimpleX 沒有用戶標識符,它何以知道向何處傳送訊息?", + "simplex-unique-overlay-card-4-p-2": "SimpleX網路使用開源協議並提供開發包以編寫聊天機器人,允許使用者透過SimpleX Chat應用程式與服務互動—我們非常期待看到您的SimpleX服務之作!", + "hero-overlay-card-1-p-2": "為傳送訊息,SimpleX 使用消息佇列中的臨時匿名標識符對,每連線各異,而不像其他網路一樣使用用戶ID— SimpleX 沒有長期標識符。", + "privacy-matters-overlay-card-3-p-2": "最令人震驚的故事之一是 Mohamedou Ould Salahi 的經歷,描述於他的回憶錄中,並在《The Mauritanian movie》中呈現。他在未經審訊的情況下被送進關塔納摩集中營,並在那裏遭受了長達 15 年的酷刑,原因是他被懷疑參與了 9/11 襲擊事件,而此前 10 年他一直住在德國。", + "simplex-private-2-title": "伺服器附加加密層", + "hero-overlay-card-2-p-1": "如使用者擁有持久性身份,即使只是一個隨機數,例如 Session ID,也會有服務提供者或攻擊者可以觀察使用者如何連線以及傳送了多少條訊息的風險。", + "simplex-unique-overlay-card-4-p-3": "如您正考慮針對 SimpleX 網路進行開發,例如針對 SimpleX 使用者的聊天機器人,或將 SimpleX 聊天函式庫整合至您的手機應用程式,請與我們聯絡以獲取支持與建議。", + "simplex-network-overlay-card-1-li-1": "點對點網路依賴 DHT 的某些變體來路由訊息。DHT 設計必須平衡傳送保證和延遲。與點對點相比,SimpleX 具有更好的傳送保證和更低的延遲,因為訊息可以使用收件者選擇的伺服器,經由多個伺服器並行冗餘地傳送。在點對點網路中,訊息依次經由O(log N)個由演算法選擇的節點。", + "simplex-private-card-3-point-3": "連線恢復被停用,以防止會話攻擊。", + "simplex-private-card-2-point-1": "傳送至收件者的伺服器加密附加層,以防止 TLS 遭到攻擊時,接收和傳送的伺服器流量之間的關聯。", + "simplex-private-card-5-point-1": "SimpleX 為每個加密層使用內容填充,以挫敗通過監控訊息長度的攻擊。", + "comparison-section-list-point-4a": "SimpleX中繼不可能威脅端到端加密。由其他方式驗證安全碼以杜絕攻擊之可能", + "docs-dropdown-6": "WebRTC伺服器", + "feature-3-title": "端到端加密的、去中心化的群組—存在只有用戶自己知道", + "feature-7-title": "可攜、加密存儲—將設定檔移至另一設備", + "back-to-top": "回到頂部", + "click-to-see": "點擊查看", + "menu": "功能表", + "on-this-page": "此頁中", + "docs-dropdown-14": "在商業中使用SimpleX", + "glossary": "詞彙表", + "simplex-chat-repo": "SimpleX Chat儲存庫", + "newer-version-of-eng-msg": "本頁面有更新的英文版本。", + "f-droid-page-simplex-chat-repo-section-text": "掃描二維碼或使用此URL以添加SimpleX儲存庫至您的F-Droid客戶端:", + "jobs": "加入團隊", + "please-use-link-in-mobile-app": "請使用手機應用程式中的連結", + "stable-versions-built-by-f-droid-org": "由F-Droid.org編譯的穩定版", + "please-enable-javascript": "啟用JavaScript以查看二維碼。", + "docs-dropdown-8": "SimpleX通訊錄服務", + "hero-2-header": "創建私密連線", + "hero-overlay-1-title": "SimpleX如何工作?", + "hero-overlay-2-title": "用戶ID為何有害私隱?", + "hero-overlay-3-title": "安全評估", + "feature-1-title": "支持Markdown與編輯的、端到端加密的訊息", + "feature-4-title": "端到端加密的語音訊息", + "feature-5-title": "自刪除訊息", + "feature-6-title": "端到端加密的語音、視訊通話", + "simplex-network-overlay-1-title": "與點對點訊息傳輸協定的比較", + "simplex-private-1-title": "雙層端到端加密", + "simplex-private-3-title": "經安全鑑權的TLS 傳送", + "simplex-private-4-title": "可選的經由Tor訪問", + "simplex-private-5-title": "多層內容填充", + "feature-8-title": "隱身模式—SimpleX獨有", + "simplex-private-6-title": "頻帶外密鑰交換", + "simplex-private-7-title": "訊息完整性驗證", + "simplex-private-8-title": "訊息混雜以降低關聯性", + "simplex-private-card-1-point-2": "如TLS安全受威脅,每個隊列中的NaCL cryptobox可防止關聯訊息隊列間的通訊。", + "simplex-private-9-title": "單向訊息隊列", + "simplex-private-card-10-point-2": "SimpleX使不用用戶設定檔傳輸訊息成為可能,與其他軟體相比提供更強的詮釋資料私隱性。", + "privacy-matters-1-overlay-1-title": "私隱為您省錢", + "simplex-unique-1-title": "您擁有完整的私隱權", + "simplex-unique-1-overlay-1-title": "您的身份、個人資料、聯絡人與詮釋資料完全隱密", + "simplex-unique-2-title": "SimpleX保護您免受垃圾訊息、濫用之害", + "simplex-private-card-4-point-1": "為保護您的 IP 位址,您可透過 Tor 或其他傳輸覆蓋網路訪問伺服器。", + "simplex-unique-2-overlay-1-title": "防止垃圾郵件和濫用的最佳保護", + "simplex-unique-3-title": "您掌控您的數據", + "simplex-unique-3-overlay-1-title": "您的數據的擁有權、控制與安全", + "hero-overlay-card-2-p-4": "SimpleX 通過在設計中不使用任何使用者 ID以防止這些攻擊。而且,如果您使用匿名聊天模式,每個聯絡人都會有不同的顯示名稱,避免他們之間有任何共用資料。", + "hero-overlay-card-2-p-3": "即使是使用 Tor v3 服務的最隱私的應用程式,如果您透過相同的設定檔與兩個不同的人交談,他們也可以證明他們連線的是同一個人。", + "hero-overlay-card-1-p-6": "請參閱 SimpleX 白皮書以閱讀更多資訊。", + "hero-overlay-card-3-p-1": "Trail of Bits 是一家領先的安全和技術顧問公司,客戶包括大型科技公司、政府機構和主要的區塊鏈專案。", + "simplex-network-overlay-card-1-p-1": "點對點通訊協定和應用程式有多種問題,使得它們不如 SimpleX 可靠、分析起來更複雜,而且易受幾種類型的攻擊。", + "simplex-network-overlay-card-1-li-2": "SimpleX 的設計與大多數點對點網路不同,沒有任何類型的全局使用者標識符,即使是臨時標識符。SimpleX 只使用臨時的標識符對,提供更好的匿名性和元資料保護。", + "privacy-matters-overlay-card-1-p-1": "許多大公司利用與您有聯繫的人的資訊來估計您的收入,向您銷售您並不真正需要的產品,以及定價。", + "privacy-matters-overlay-card-1-p-2": "線上零售商知道收入較低的人更有可能進行緊急購物,因此他們可能收取較高的價格或取消折扣。", + "enter-your-email-address": "輸入您的電郵地址", + "get-simplex": "下載SimpleX 桌面版應用程式", + "join-us-on-GitHub": "在GitHub上加入我們", + "donate-here-to-help-us": "於此捐助以幫助我們", + "sign-up-to-receive-our-updates": "登記以收取更新", + "join": "加入", + "simplex-unique-card-3-p-1": "SimpleX 以 可攜式加密數據庫格式 儲存用戶端裝置上的所有使用者資料;這些資料可傳輸至其他設備。", + "simplex-unique-card-3-p-2": "端對端加密的訊息暫時保留在 SimpleX 中繼伺服器上,收到訊息後會被永久刪除。", + "simplex-unique-card-4-p-2": "您可以使用您自己的伺服器運行SimpleX,或使用我們提供的伺服器並仍連線到任何使用者。", + "simplex-unique-overlay-card-3-p-4": "傳送與接收的伺服器流量之間沒有共同的標識符或密文—如果有人觀察,即使 TLS 遭到破壞,也無法輕易確定誰與誰通訊。", + "join-the-REDDIT-community": "加入Reddit社群", + "privacy-matters-overlay-card-3-p-1": "人人都應關心自己通訊的隱私和安全;無害的對話可能會讓您陷入危險,即使您沒有什麼要隱瞞的。", + "privacy-matters-overlay-card-3-p-3": "普通人會因為他們在網路上分享的內容而被捕,即使是透過他們的「匿名」帳號,即使是在民主國家。", + "privacy-matters-overlay-card-2-p-1": "不久前我們觀察到大選被一家聲譽良好的顧問公司操縱,該公司利用我們的社交圖來扭曲我們對真實世界的看法,並操縱我們的選票。", + "comparison-section-list-point-2": "基於DNS的位址", + "comparison-section-list-point-1": "通常基於電話號碼,有時用戶名", + "simplex-private-card-6-point-1": "許多通訊網路容易受到伺服器或網路供應商的中間人攻擊。", + "privacy-matters-overlay-card-1-p-4": "SimpleX 網路比任何其他方式都能更好地保護您的連線隱私,完全防止您的社交圖表被任何公司或組織獲得。即使使用 SimpleX Chat 應用程式中預設的伺服器,伺服器操作員也不會知道使用者的數量或他們的連線。", + "privacy-matters-overlay-card-2-p-2": "要做到客觀和獨立決策,您需要控制自己的資訊空間。只有當使用無法存取您的社交圖表的私人通訊網路時,您才可能做到這一點。", + "simplex-unique-overlay-card-1-p-1": "與其他通訊網路不同,SimpleX 不為使用者指定標識符。它不依賴電話號碼、網域地址 (如電子郵件或 XMPP)、用戶名、公鑰或甚至隨機數來識別使用者;SimpleX 伺服器操作員不知道有多少人使用他們的伺服器。", + "privacy-matters-overlay-card-2-p-3": "SimpleX 是第一個在設計上不使用任何使用者標識符的網路,因此比任何已知的其他方案都能更好地保護您的社交圖。", + "simplex-unique-overlay-card-4-p-1": "您可以使用您自己的伺服器運行SimpleX,並仍與使用應用程式中預設伺服器的人進行通訊。", + "simplex-unique-card-1-p-2": "與任何其他現有的訊息網路不同,SimpleX 不指定使用者的標識符—甚至沒有隨機數。", + "why-simplex-is-unique": "SimpleX為何與眾不同", + "learn-more": "查看更多", + "more-info": "更多訊息", + "hide-info": "隱藏訊息", + "contact-hero-header": "您收到了SimpleX聯絡地址", + "simplex-unique-card-4-p-1": "SimpleX 網路是完全去中心化的,獨立於任何加密貨幣或任何其他網路(除Internet)。", + "invitation-hero-header": "您收到了SimpleX一次性連結", + "contact-hero-subheader": "使用手提電話或平板電腦中的SimpleX應用程式掃描二維碼。", + "connect-in-app": "在應用中連結", + "contact-hero-p-2": "尚未下載SimpleX?", + "contact-hero-p-1": "當您檢視此頁面時,此連結中的公鑰和訊息佇列位址不透過網路傳送;它們包含在連結 URL 的雜湊片段中。", + "contact-hero-p-3": "使用以下連結下載應用程式。", + "to-make-a-connection": "建立連結:", + "install-simplex-app": "安裝 SimpleX 應用程式", + "use-this-command": "使用此命令:", + "scan-the-qr-code-with-the-simplex-chat-app-description": "當您檢視此頁面時,此連結中的公鑰和訊息佇列位址不會透過網路傳送 —它們包含在連結 URL 的雜湊片段中。", + "installing-simplex-chat-to-terminal": "安裝SimpleX至終端", + "open-simplex-app": "開啟SimpleX應用程式", + "if-you-already-installed": "如已安裝", + "simplex-chat-for-the-terminal": "用於終端的 SimpleX Chat", + "if-you-already-installed-simplex-chat-for-the-terminal": "如您已經安裝了用於終端的 SimpleX Chat", + "tap-to-close": "輕觸以關閉", + "simplex-network-1-desc": "所有訊息都經由伺服器傳送,既能提供更好的元資料隱私和可靠的異步訊息傳送,又能避免許多", + "simplex-network-section-desc": "SinpleX Chat 結合點對點和互聯網路的優點,提供最佳的隱私性。", + "copy-the-command-below-text": "複製下面的命令並用於聊天:", + "privacy-matters-section-header": "隱私為何 重要", + "privacy-matters-section-label": "確保您的通訊應用程式無法竊取您的資料!", + "simplex-network-3-header": "SimpleX網路", + "simplex-network-1-overlay-linktext": "點對點網路的弊病", + "simplex-network-2-desc": "SimpleX 中繼伺服器不儲存使用者個人資料、聯絡人和傳送的訊息,也不連線彼此,也沒有伺服器目錄。", + "simplex-network-2-header": "與互聯網路不同", + "comparison-section-header": "與其他協議之比較", + "protocol-2-text": "XMPP、Matrix", + "comparison-point-1-text": "需要全局身份", + "simplex-network-3-desc": "伺服器提供單向佇列以連接使用者,但伺服器無法看到網路連線圖 —只有使用者能看到。", + "protocol-1-text": "Signal,大平台", + "comparison-point-2-text": "中間人攻擊之可能", + "comparison-point-3-text": "對DNS的依賴", + "comparison-point-4-text": "單一或集中式網路", + "comparison-section-list-point-3": "公鑰或其他某種全局獨一的標識符", + "comparison-section-list-point-5": "不保護使用者的元數據隱私", + "comparison-section-list-point-7": "點對點網路必須有中央權威,否則整個網路都可能受到攻擊", + "comparison-section-list-point-4": "如果運營商的伺服器受到攻擊,用其他應用程式(如 Signal)驗證 SimpleX 安全碼,以減輕風險", + "guide-dropdown-8": "應用程式設定", + "guide-dropdown-6": "音視訊通話", + "docs-dropdown-1": "SimpleX 網路", + "guide": "指南", + "docs-dropdown-9": "下載", + "docs-dropdown-11": "常見問題", + "docs-dropdown-12": "安全性", + "docs-dropdown-7": "翻譯SimpleX", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat 和F-Droid.org 儲存庫使用不同的金鑰為安裝包簽名。若要切換,請匯出聊天資料庫並重新安裝應用程式。" +} diff --git a/website/src/.well-known/apple-app-site-association/index.json b/website/src/.well-known/apple-app-site-association/index.json index 3cd3fdd043..3b513fe61e 100644 --- a/website/src/.well-known/apple-app-site-association/index.json +++ b/website/src/.well-known/apple-app-site-association/index.json @@ -17,6 +17,30 @@ }, { "/": "/invitation" + }, + { + "/": "/a/*" + }, + { + "/": "/a" + }, + { + "/": "/c/*" + }, + { + "/": "/c" + }, + { + "/": "/g/*" + }, + { + "/": "/g" + }, + { + "/": "/i/*" + }, + { + "/": "/i" } ] } diff --git a/website/src/_data/languages.json b/website/src/_data/languages.json index 0ac05063cc..340cc59da9 100644 --- a/website/src/_data/languages.json +++ b/website/src/_data/languages.json @@ -33,12 +33,31 @@ "flag": "/img/flags/es.svg", "enabled": true }, + { + "label": "fi", + "name": "Suomi", + "flag": "/img/flags/fi.svg", + "enabled": true + }, { "label": "fr", "name": "Français", "flag": "/img/flags/fr.svg", "enabled": true }, + { + "label": "he", + "name": "עִברִית", + "flag": "/img/flags/il.svg", + "enabled": true, + "rtl": true + }, + { + "label": "hu", + "name": "Magyar", + "flag": "/img/flags/hu.svg", + "enabled": true + }, { "label": "it", "name": "Italiano", @@ -89,9 +108,9 @@ }, { "label": "ru", - "name": "Russian", + "name": "Русский", "flag": "/img/flags/ru.svg", - "enabled": false + "enabled": true } - ] + ] } \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20250308.html b/website/src/_includes/blog_previews/20250308.html new file mode 100644 index 0000000000..ab55ecdbca --- /dev/null +++ b/website/src/_includes/blog_previews/20250308.html @@ -0,0 +1,12 @@ +

v6.3 is released:

+ +
    +
  • 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, thanks to our users and Weblate.

+ +

The last but not the least - server builds are now reproducible!

diff --git a/website/src/_includes/hero.html b/website/src/_includes/hero.html index c0e2b3f30b..6ebfb883ef 100644 --- a/website/src/_includes/hero.html +++ b/website/src/_includes/hero.html @@ -31,6 +31,10 @@ Privacy Guides + + Whonix + + Heise Online @@ -43,7 +47,7 @@ Opt Out Podcast - +
@@ -66,7 +70,7 @@ - + {#

PLAY

diff --git a/website/src/_includes/sections/simplex_unique.html b/website/src/_includes/sections/simplex_unique.html index 6759e3a5cb..cdccb3dd5a 100644 --- a/website/src/_includes/sections/simplex_unique.html +++ b/website/src/_includes/sections/simplex_unique.html @@ -1,6 +1,6 @@
-

{{ "why-simplex-is" | i18n({}, lang ) | safe }} {{ "unique" | i18n({}, lang ) | safe }}

+

{{ "why-simplex-is-unique" | i18n({}, lang ) | safe }}

diff --git a/website/src/img/flags/fi.svg b/website/src/img/flags/fi.svg new file mode 100644 index 0000000000..470be2d07c --- /dev/null +++ b/website/src/img/flags/fi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/website/src/img/flags/hu.svg b/website/src/img/flags/hu.svg new file mode 100644 index 0000000000..baddf7f5ea --- /dev/null +++ b/website/src/img/flags/hu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/website/src/img/flags/il.svg b/website/src/img/flags/il.svg new file mode 100644 index 0000000000..f43be7e8ed --- /dev/null +++ b/website/src/img/flags/il.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/website/src/img/whonix-dark.png b/website/src/img/whonix-dark.png new file mode 100644 index 0000000000..f361f4d1f6 Binary files /dev/null and b/website/src/img/whonix-dark.png differ diff --git a/website/src/img/whonix-light.png b/website/src/img/whonix-light.png new file mode 100644 index 0000000000..d8616b4a4b Binary files /dev/null and b/website/src/img/whonix-light.png differ diff --git a/website/web.sh b/website/web.sh index 4feba4be50..897c87743e 100755 --- a/website/web.sh +++ b/website/web.sh @@ -5,6 +5,7 @@ set -e cp -R docs website/src rm -rf website/src/docs/rfcs rm website/src/docs/lang/*/README.md +rm -rf website/src/docs/dependencies cp -R blog website/src cp -R images website/src rm website/src/blog/README.md