mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-07-01 06:30:07 +00:00
Compare commits
No commits in common. "stable" and "v6.3.0" have entirely different histories.
508 changed files with 13158 additions and 23436 deletions
47
.github/actions/prepare-build/action.yml
vendored
47
.github/actions/prepare-build/action.yml
vendored
|
@ -1,47 +0,0 @@
|
|||
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') }}
|
39
.github/actions/prepare-release/action.yml
vendored
39
.github/actions/prepare-release/action.yml
vendored
|
@ -1,39 +0,0 @@
|
|||
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 }}
|
44
.github/actions/swap/action.yml
vendored
44
.github/actions/swap/action.yml
vendored
|
@ -1,44 +0,0 @@
|
|||
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
|
646
.github/workflows/build.yml
vendored
646
.github/workflows/build.yml
vendored
|
@ -22,58 +22,17 @@ 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:
|
||||
|
||||
# =============================
|
||||
# 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:
|
||||
prepare-release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
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
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: simplex-chat/release-changelog-builder-action@v5
|
||||
uses: mikepenz/release-changelog-builder-action@v4
|
||||
with:
|
||||
configuration: .github/changelog_conf.json
|
||||
failOnError: true
|
||||
|
@ -83,8 +42,7 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: simplex-chat/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: ${{ steps.build_changelog.outputs.changelog }}
|
||||
prerelease: true
|
||||
|
@ -94,295 +52,183 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# =========================
|
||||
# Linux Build
|
||||
# =========================
|
||||
|
||||
build-linux:
|
||||
name: "ubuntu-${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}"
|
||||
needs: [maybe-release, variables]
|
||||
runs-on: ubuntu-${{ matrix.os }}
|
||||
build:
|
||||
name: build-${{ matrix.os }}-${{ matrix.ghc }}
|
||||
if: always()
|
||||
needs: prepare-release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 22.04
|
||||
- os: ubuntu-20.04
|
||||
ghc: "8.10.7"
|
||||
should_run: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }}
|
||||
- os: 22.04
|
||||
ghc: ${{ needs.variables.outputs.GHC_VER }}
|
||||
cli_asset_name: simplex-chat-ubuntu-22_04-x86-64
|
||||
cache_path: ~/.cabal/store
|
||||
- os: ubuntu-20.04
|
||||
ghc: "9.6.3"
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-ubuntu-20_04-x86-64
|
||||
desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb
|
||||
- os: ubuntu-22.04
|
||||
ghc: "9.6.3"
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-ubuntu-22_04-x86-64
|
||||
desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb
|
||||
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
|
||||
- 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
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
if: matrix.should_run == true
|
||||
- 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
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup swap
|
||||
if: matrix.ghc == '8.10.7' && matrix.should_run == true
|
||||
uses: ./.github/actions/swap
|
||||
- name: Setup Haskell
|
||||
uses: haskell-actions/setup@v2
|
||||
with:
|
||||
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
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
cabal-version: "3.10.1.0"
|
||||
|
||||
- name: Restore cached build
|
||||
if: matrix.should_run == true
|
||||
uses: actions/cache@v4
|
||||
id: restore_cache
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cabal/store
|
||||
${{ matrix.cache_path }}
|
||||
dist-newstyle
|
||||
key: ubuntu-${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
|
||||
key: ${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: matrix.should_run == true
|
||||
uses: simplex-chat/docker-setup-buildx-action@v3
|
||||
# / Unix
|
||||
|
||||
- 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
|
||||
- name: Unix prepare cabal.project.local for Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
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
|
||||
|
||||
- name: Prepare cabal.project.local
|
||||
if: matrix.should_run == true
|
||||
- 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'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
echo "package direct-sqlcipher" >> cabal.project.local
|
||||
echo " flags: +openssl" >> cabal.project.local
|
||||
|
||||
# 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
|
||||
- name: Unix build CLI
|
||||
id: unix_cli_build
|
||||
if: matrix.os != 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
docker cp builder:/out/simplex-chat-test .
|
||||
|
||||
- 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 }}"
|
||||
cabal build --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
|
||||
echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $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
|
||||
- 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:
|
||||
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 }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.unix_cli_build.outputs.bin_path }}
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Build Desktop
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
shell: docker exec -t builder sh -eu {0}
|
||||
- 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')
|
||||
shell: bash
|
||||
run: |
|
||||
scripts/desktop/build-lib-linux.sh
|
||||
cd apps/multiplatform
|
||||
./gradlew packageDeb
|
||||
|
||||
- name: Prepare Desktop
|
||||
id: linux_desktop_build
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/deb/simplex_*_amd64.deb )
|
||||
path=$(echo $PWD/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
|
||||
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- 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}
|
||||
- name: Linux make AppImage
|
||||
id: linux_appimage_build
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
|
||||
shell: bash
|
||||
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)
|
||||
path=$(echo $PWD/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
|
||||
echo "appimage_hash=$(echo SHA2-512\(simplex-desktop-x86_64.AppImage\)= $(openssl sha512 $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
|
||||
- name: Mac build desktop
|
||||
id: mac_desktop_build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13')
|
||||
shell: bash
|
||||
env:
|
||||
APPLE_SIMPLEX_SIGNING_KEYCHAIN: ${{ secrets.APPLE_SIMPLEX_SIGNING_KEYCHAIN }}
|
||||
|
@ -392,77 +238,88 @@ 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-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Desktop
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/actions/prepare-release
|
||||
- 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
|
||||
with:
|
||||
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 }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.linux_desktop_build.outputs.package_path }}
|
||||
asset_name: ${{ matrix.desktop_asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 120
|
||||
- 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
|
||||
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 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
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
# =========================
|
||||
# Windows Build
|
||||
# =========================
|
||||
# Unix /
|
||||
|
||||
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
|
||||
# / Windows
|
||||
# rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing
|
||||
|
||||
- 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'
|
||||
uses: simplex-chat/setup-msys2@v2
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: ucrt64
|
||||
update: true
|
||||
|
@ -474,9 +331,10 @@ jobs:
|
|||
toolchain:p
|
||||
cmake:p
|
||||
|
||||
# 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
|
||||
|
||||
- name: Windows build
|
||||
id: windows_build
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo)
|
||||
|
@ -491,42 +349,70 @@ jobs:
|
|||
|
||||
rm -rf dist-newstyle/src/direct-sq*
|
||||
sed -i "s/, unix /--, unix /" simplex-chat.cabal
|
||||
cabal build -j --enable-tests
|
||||
cabal build --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-256\(${{ matrix.cli_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload CLI
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/actions/prepare-release
|
||||
- name: Windows upload CLI binary to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
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 }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.windows_build.outputs.bin_path }}
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Build Desktop
|
||||
- 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
|
||||
id: windows_desktop_build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
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-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Desktop
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/actions/prepare-release
|
||||
- name: Windows upload desktop package to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
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 }}
|
||||
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 /
|
||||
|
|
45
.github/workflows/reproduce-schedule.yml
vendored
45
.github/workflows/reproduce-schedule.yml
vendored
|
@ -1,45 +0,0 @@
|
|||
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
|
2
.github/workflows/web.yml
vendored
2
.github/workflows/web.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
|||
./website/web.sh
|
||||
|
||||
- name: Deploy
|
||||
uses: simplex-chat/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
publish_dir: ./website/_site
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -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' --constraint 'simplex-chat +client_library'
|
||||
RUN cabal build exe:simplex-chat --constraint 'simplexmq +client_library'
|
||||
|
||||
# Strip the binary from debug symbols to reduce size
|
||||
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
# 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
|
30
README.md
30
README.md
|
@ -10,7 +10,7 @@
|
|||
|
||||
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
|
||||
|
||||
[<img src="./images/trail-of-bits.jpg" height="80">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [<img src="./images/privacy-guides.jpg" height="64">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [<img src="./images/whonix-logo.jpg" height="64">](https://www.whonix.org/wiki/Chat#Recommendation) [<img src="./images/kuketz-blog.jpg" height="64">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
|
||||
[<img src="./images/trail-of-bits.jpg" height="100">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [<img src="./images/privacy-guides.jpg" height="80">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [<img src="./images/kuketz-blog.jpg" height="80">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
|
||||
|
||||
## 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-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)
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
|
||||
|
||||
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
|
||||
|
||||
|
@ -110,15 +110,6 @@ 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.
|
||||
|
@ -150,6 +141,15 @@ 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,7 +169,6 @@ 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.
|
||||
|
@ -235,10 +234,6 @@ 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)
|
||||
|
@ -310,13 +305,12 @@ 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 clients builds – this is a complex problem, but we are aiming to have it in 2025 at least partially.
|
||||
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.
|
||||
4. Recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
|
||||
|
||||
## For developers
|
||||
|
|
|
@ -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? ChatError, case .errorAgent(.NTF(.AUTH)) = cr {
|
||||
if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr {
|
||||
m.tokenStatus = .expired
|
||||
}
|
||||
logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))")
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2 KiB |
|
@ -11,10 +11,12 @@ 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +76,7 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func allViews() -> some View {
|
||||
@ViewBuilder 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.
|
||||
|
@ -209,7 +211,7 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func activeCallInteractiveArea(_ call: Call) -> some View {
|
||||
@ViewBuilder private func activeCallInteractiveArea(_ call: Call) -> some View {
|
||||
HStack {
|
||||
Text(call.contact.displayName).font(.body).foregroundColor(.white)
|
||||
Spacer()
|
||||
|
@ -276,8 +278,10 @@ struct ContentView: View {
|
|||
let showWhatsNew = shouldShowWhatsNew()
|
||||
let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false
|
||||
noticesShown = showWhatsNew || showUpdatedConditions
|
||||
if showWhatsNew || showUpdatedConditions {
|
||||
if showWhatsNew {
|
||||
noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions)
|
||||
} else if showUpdatedConditions {
|
||||
noticesSheetItem = .updatedConditions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -296,6 +300,13 @@ struct ContentView: View {
|
|||
.if(updatedConditions) { v in
|
||||
v.task { await setConditionsNotified_() }
|
||||
}
|
||||
case .updatedConditions:
|
||||
UsageConditionsView(
|
||||
currUserServers: Binding.constant([]),
|
||||
userServers: Binding.constant([])
|
||||
)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.task { await setConditionsNotified_() }
|
||||
}
|
||||
}
|
||||
if chatModel.setDeliveryReceipts {
|
||||
|
@ -443,12 +454,12 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
func connectViaUrl() {
|
||||
let m = ChatModel.shared
|
||||
if let url = m.appOpenUrl {
|
||||
m.appOpenUrl = nil
|
||||
dismissAllSheets() {
|
||||
dismissAllSheets() {
|
||||
let m = ChatModel.shared
|
||||
if let url = m.appOpenUrl {
|
||||
m.appOpenUrl = nil
|
||||
var path = url.path
|
||||
if (path == "/contact" || path == "/invitation" || path == "/a" || path == "/c" || path == "/g" || path == "/i") {
|
||||
if (path == "/contact" || path == "/invitation") {
|
||||
path.removeFirst()
|
||||
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
||||
planAndConnect(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -30,18 +30,9 @@ actor TerminalItems {
|
|||
}
|
||||
}
|
||||
|
||||
func addCommand<R: ChatAPIResult>(_ start: Date, _ cmd: ChatCommand, _ res: APIResult<R>) async {
|
||||
func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async {
|
||||
await add(.cmd(start, cmd))
|
||||
await addResult(res)
|
||||
}
|
||||
|
||||
func addResult<R: ChatAPIResult>(_ res: APIResult<R>) 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)
|
||||
await add(.resp(.now, resp))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +54,9 @@ 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
|
||||
|
@ -73,10 +66,6 @@ class ItemsModel: ObservableObject {
|
|||
private var navigationTimeoutTask: Task<Void, Never>? = nil
|
||||
private var loadChatTask: Task<Void, Never>? = nil
|
||||
|
||||
var lastItemsLoaded: Bool {
|
||||
chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id
|
||||
}
|
||||
|
||||
init() {
|
||||
publisher
|
||||
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
|
||||
|
@ -580,7 +569,7 @@ final class ChatModel: ObservableObject {
|
|||
ci.meta.itemStatus = status
|
||||
}
|
||||
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
||||
im.chatState.itemAdded((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0)
|
||||
im.chatItemsChangesListener.added((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0)
|
||||
im.itemAdded = true
|
||||
ChatItemDummyModel.shared.sendUpdate()
|
||||
return true
|
||||
|
@ -628,7 +617,7 @@ final class ChatModel: ObservableObject {
|
|||
if let i = getChatItemIndex(cItem) {
|
||||
withAnimation {
|
||||
let item = im.reversedChatItems.remove(at: i)
|
||||
im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
||||
im.chatItemsChangesListener.removed([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -716,7 +705,7 @@ final class ChatModel: ObservableObject {
|
|||
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||
withAnimation {
|
||||
im.reversedChatItems.insert(cItem, at: 0)
|
||||
im.chatState.itemAdded((cItem.id, cItem.isRcvNew), 0)
|
||||
im.chatItemsChangesListener.added((cItem.id, cItem.isRcvNew), 0)
|
||||
im.itemAdded = true
|
||||
}
|
||||
return cItem
|
||||
|
@ -750,7 +739,7 @@ final class ChatModel: ObservableObject {
|
|||
markChatItemRead_(i)
|
||||
i += 1
|
||||
}
|
||||
im.chatState.itemsRead(nil, im.reversedChatItems.reversed())
|
||||
im.chatItemsChangesListener.read(nil, im.reversedChatItems.reversed())
|
||||
}
|
||||
}
|
||||
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
|
||||
|
@ -774,7 +763,7 @@ final class ChatModel: ObservableObject {
|
|||
if chatId == cInfo.id {
|
||||
chatItemStatuses = [:]
|
||||
im.reversedChatItems = []
|
||||
im.chatState.clear()
|
||||
im.chatItemsChangesListener.cleared()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -792,7 +781,7 @@ final class ChatModel: ObservableObject {
|
|||
}
|
||||
i += 1
|
||||
}
|
||||
im.chatState.itemsRead(unreadItemIds, im.reversedChatItems.reversed())
|
||||
im.chatItemsChangesListener.read(unreadItemIds, im.reversedChatItems.reversed())
|
||||
}
|
||||
self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead)
|
||||
}
|
||||
|
@ -1152,6 +1141,27 @@ 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
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,7 +19,6 @@ struct SimpleXApp: App {
|
|||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State private var enteredBackgroundAuthenticated: TimeInterval? = nil
|
||||
@State private var appOpenUrlLater: URL?
|
||||
|
||||
init() {
|
||||
DispatchQueue.global(qos: .background).sync {
|
||||
|
@ -43,11 +42,7 @@ struct SimpleXApp: App {
|
|||
.environmentObject(AppTheme.shared)
|
||||
.onOpenURL { url in
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
if AppChatState.shared.value == .active {
|
||||
chatModel.appOpenUrl = url
|
||||
} else {
|
||||
appOpenUrlLater = url
|
||||
}
|
||||
chatModel.appOpenUrl = url
|
||||
}
|
||||
.onAppear() {
|
||||
// Present screen for continue migration if it wasn't finished yet
|
||||
|
@ -98,16 +93,7 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -243,7 +243,7 @@ struct ActiveCallView: View {
|
|||
ChatReceiver.shared.messagesChannel = nil
|
||||
return
|
||||
}
|
||||
if case let .result(.chatItemsStatusesUpdated(_, chatItems)) = msg,
|
||||
if case let .chatItemsStatusesUpdated(_, chatItems) = msg,
|
||||
chatItems.contains(where: { ci in
|
||||
ci.chatInfo.id == call.contact.id &&
|
||||
ci.chatItem.content.isSndCall &&
|
||||
|
@ -467,7 +467,7 @@ struct ActiveCallOverlay: View {
|
|||
.disabled(call.initialCallType == .audio && client.activeCall?.peerHasOldVersion == true)
|
||||
}
|
||||
|
||||
private func flipCameraButton() -> some View {
|
||||
@ViewBuilder 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 {
|
|||
}
|
||||
}
|
||||
|
||||
private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
|
||||
@ViewBuilder private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
|
||||
callButton(imageName, call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2), padding: padding, perform)
|
||||
}
|
||||
|
||||
private func audioDevicePickerButton() -> some View {
|
||||
@ViewBuilder private func audioDevicePickerButton() -> some View {
|
||||
AudioDevicePicker()
|
||||
.opacity(0.8)
|
||||
.scaleEffect(2)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
@preconcurrency import SimpleXChat
|
||||
import SimpleXChat
|
||||
|
||||
func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
|
||||
HStack {
|
||||
|
|
|
@ -50,7 +50,7 @@ struct CICallItemView: View {
|
|||
Image(systemName: "phone.connection").foregroundColor(.green)
|
||||
}
|
||||
|
||||
private func endedCallIcon(_ sent: Bool) -> some View {
|
||||
@ViewBuilder 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 {
|
||||
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!")
|
||||
}
|
||||
})
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "phone.arrow.down.left").foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
allowFeatureToContact(ct, feature, param: setParam ? 86400 : nil)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
featurePreferenceView()
|
||||
}
|
||||
|
|
|
@ -19,42 +19,42 @@ struct CIFileView: View {
|
|||
var body: some View {
|
||||
if smallViewSize != nil {
|
||||
fileIndicator()
|
||||
.simultaneousGesture(TapGesture().onEnded(fileAction))
|
||||
.onTapGesture(perform: fileAction)
|
||||
} else {
|
||||
let metaReserve = edited
|
||||
? " "
|
||||
: " "
|
||||
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)
|
||||
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)
|
||||
}
|
||||
} 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) {
|
||||
|
|
|
@ -84,12 +84,12 @@ struct CIGroupInvitationView: View {
|
|||
}
|
||||
|
||||
if action {
|
||||
v.simultaneousGesture(TapGesture().onEnded {
|
||||
v.onTapGesture {
|
||||
inProgress = true
|
||||
joinGroup(groupInvitation.groupId) {
|
||||
await MainActor.run { inProgress = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
.disabled(inProgress)
|
||||
} else {
|
||||
v
|
||||
|
|
|
@ -31,9 +31,7 @@ struct CIImageView: View {
|
|||
.if(!smallView) { view in
|
||||
view.modifier(PrivacyBlur(blurred: $blurred))
|
||||
}
|
||||
.if(!blurred) { v in
|
||||
v.simultaneousGesture(TapGesture().onEnded { showFullScreenImage = true })
|
||||
}
|
||||
.onTapGesture { showFullScreenImage = true }
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenImage = false
|
||||
}
|
||||
|
@ -45,7 +43,7 @@ struct CIImageView: View {
|
|||
imageView(preview).modifier(PrivacyBlur(blurred: $blurred))
|
||||
}
|
||||
}
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation, .rcvAborted:
|
||||
|
@ -82,7 +80,7 @@ struct CIImageView: View {
|
|||
default: ()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
|
|
|
@ -7,11 +7,10 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIInvalidJSONView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var json: Data?
|
||||
var json: String
|
||||
@State private var showJSON = false
|
||||
|
||||
var body: some View {
|
||||
|
@ -24,16 +23,16 @@ struct CIInvalidJSONView: View {
|
|||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.textSelection(.disabled)
|
||||
.simultaneousGesture(TapGesture().onEnded { showJSON = true })
|
||||
.onTapGesture { showJSON = true }
|
||||
.appSheet(isPresented: $showJSON) {
|
||||
invalidJSONView(dataToString(json))
|
||||
invalidJSONView(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func invalidJSONView(_ json: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Button { // this is used in the sheet, Button works here
|
||||
Button {
|
||||
showShareSheet(items: [json])
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
|
@ -50,6 +49,6 @@ func invalidJSONView(_ json: String) -> some View {
|
|||
|
||||
struct CIInvalidJSONView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CIInvalidJSONView(json: "{}".data(using: .utf8)!)
|
||||
CIInvalidJSONView(json: "{}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,32 +37,10 @@ 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(
|
||||
|
|
|
@ -20,11 +20,11 @@ struct CIMemberCreatedContactView: View {
|
|||
case let .groupRcv(groupMember):
|
||||
if let contactId = groupMember.memberContactId {
|
||||
memberCreatedContactView(openText: "Open")
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
ItemsModel.shared.loadOpenChat("@\(contactId)") {
|
||||
dismissAllSheets(animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
memberCreatedContactView()
|
||||
}
|
||||
|
|
|
@ -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,13 +152,11 @@ 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)
|
||||
|
|
|
@ -68,7 +68,7 @@ struct CIRcvDecryptionError: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func viewBody() -> some View {
|
||||
@ViewBuilder 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)
|
||||
}
|
||||
.simultaneousGesture(TapGesture().onEnded(onClick))
|
||||
.onTapGesture(perform: { 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)
|
||||
}
|
||||
.simultaneousGesture(TapGesture().onEnded(onClick))
|
||||
.onTapGesture(perform: { onClick() })
|
||||
.padding(.vertical, 6)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
|
|
@ -47,57 +47,57 @@ struct CIVideoView: View {
|
|||
let file = chatItem.file
|
||||
ZStack(alignment: smallView ? .topLeading : .center) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
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 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 !smallView {
|
||||
durationProgress()
|
||||
}
|
||||
}
|
||||
if !blurred, let file, showDownloadButton(file.fileStatus) {
|
||||
if !smallView || !file.showStatusIconInSmallView {
|
||||
if !smallView {
|
||||
Button {
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
} else if !file.showStatusIconInSmallView {
|
||||
playPauseIcon("play.fill")
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,26 +151,27 @@ struct CIVideoView: View {
|
|||
ZStack(alignment: .center) {
|
||||
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
|
||||
imageView(defaultPreview)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
decrypt(file: file) {
|
||||
showFullScreenPlayer = urlDecrypted != nil
|
||||
}
|
||||
})
|
||||
}
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenPlayer = false
|
||||
}
|
||||
if !blurred {
|
||||
if !decryptionInProgress {
|
||||
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
decrypt(file: file) {
|
||||
if urlDecrypted != nil {
|
||||
videoPlaying = true
|
||||
player?.play()
|
||||
}
|
||||
Button {
|
||||
decrypt(file: file) {
|
||||
if urlDecrypted != nil {
|
||||
videoPlaying = true
|
||||
player?.play()
|
||||
}
|
||||
})
|
||||
.disabled(!canBePlayed)
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
|
||||
}
|
||||
.disabled(!canBePlayed)
|
||||
} else {
|
||||
videoDecryptionProgress()
|
||||
}
|
||||
|
@ -193,30 +194,29 @@ struct CIVideoView: View {
|
|||
}
|
||||
}
|
||||
.modifier(PrivacyBlur(enabled: !videoPlaying, blurred: $blurred))
|
||||
.if(!blurred) { v in
|
||||
v.simultaneousGesture(TapGesture().onEnded {
|
||||
switch player.timeControlStatus {
|
||||
case .playing:
|
||||
player.pause()
|
||||
videoPlaying = false
|
||||
case .paused:
|
||||
if canBePlayed {
|
||||
showFullScreenPlayer = true
|
||||
}
|
||||
default: ()
|
||||
.onTapGesture {
|
||||
switch player.timeControlStatus {
|
||||
case .playing:
|
||||
player.pause()
|
||||
videoPlaying = false
|
||||
case .paused:
|
||||
if canBePlayed {
|
||||
showFullScreenPlayer = true
|
||||
}
|
||||
})
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenPlayer = false
|
||||
}
|
||||
if !videoPlaying && !blurred {
|
||||
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
m.stopPreviousRecPlay = url
|
||||
player.play()
|
||||
})
|
||||
.disabled(!canBePlayed)
|
||||
Button {
|
||||
m.stopPreviousRecPlay = url
|
||||
player.play()
|
||||
} label: {
|
||||
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
|
||||
}
|
||||
.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 { // this is shown in chat list, where onTapGesture works
|
||||
.onTapGesture {
|
||||
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 { // this is shown in chat list, where onTapGesture works
|
||||
.onTapGesture {
|
||||
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)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
showFileErrorAlert(sndFileError)
|
||||
})
|
||||
}
|
||||
case let .sndWarning(sndFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
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)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
showFileErrorAlert(rcvFileError)
|
||||
})
|
||||
}
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
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 }, // this is used in full screen player, Button works here
|
||||
Button(action: { showFullScreenPlayer = false },
|
||||
label: {
|
||||
Image(systemName: "multiply")
|
||||
.resizable()
|
||||
|
|
|
@ -168,14 +168,14 @@ struct VoiceMessagePlayer: View {
|
|||
case .sndCancelled: playbackButton()
|
||||
case let .sndError(sndFileError):
|
||||
fileStatusIcon("multiply", 14)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
showFileErrorAlert(sndFileError)
|
||||
})
|
||||
}
|
||||
case let .sndWarning(sndFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
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)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
showFileErrorAlert(rcvFileError)
|
||||
})
|
||||
}
|
||||
case let .rcvWarning(rcvFileError):
|
||||
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
showFileErrorAlert(rcvFileError, temporary: true)
|
||||
})
|
||||
}
|
||||
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
}
|
||||
} else {
|
||||
|
@ -255,29 +255,59 @@ struct VoiceMessagePlayer: View {
|
|||
}
|
||||
}
|
||||
|
||||
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:
|
||||
@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 {
|
||||
if let recordingSource = getLoadedFileSource(recordingFile) {
|
||||
startPlayback(recordingSource)
|
||||
}
|
||||
case .playing:
|
||||
} label: {
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
}
|
||||
case .playing:
|
||||
Button {
|
||||
audioPlayer?.pause()
|
||||
playbackState = .paused
|
||||
notifyStateChange()
|
||||
case .paused:
|
||||
} label: {
|
||||
playPauseIcon("pause.fill", theme.colors.primary)
|
||||
}
|
||||
case .paused:
|
||||
Button {
|
||||
audioPlayer?.play()
|
||||
playbackState = .playing
|
||||
notifyStateChange()
|
||||
} label: {
|
||||
playPauseIcon("play.fill", theme.colors.primary)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func playPauseIcon(_ image: String, _ color: Color/* = .accentColor*/) -> some View {
|
||||
|
@ -299,14 +329,28 @@ struct VoiceMessagePlayer: View {
|
|||
}
|
||||
|
||||
private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View {
|
||||
playPauseIcon(icon, theme.colors.primary)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
Task {
|
||||
if let user = chatModel.currentUser {
|
||||
await receiveFile(user: user, fileId: recordingFile.fileId)
|
||||
Group {
|
||||
if sizeMultiplier != 1 {
|
||||
playPauseIcon(icon, theme.colors.primary)
|
||||
.onTapGesture {
|
||||
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() {
|
||||
|
@ -386,7 +430,6 @@ struct VoiceMessagePlayer: View {
|
|||
}
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
func voiceMessageSizeBasedOnSquareSize(_ squareSize: CGFloat) -> CGFloat {
|
||||
let squareToCircleRatio = 0.935
|
||||
return squareSize + squareSize * (1 - squareToCircleRatio)
|
||||
|
@ -403,12 +446,10 @@ 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)"
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ 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 {
|
||||
|
@ -55,7 +57,7 @@ struct FramedItemView: View {
|
|||
|
||||
if let qi = chatItem.quotedItem {
|
||||
ciQuoteView(qi)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) {
|
||||
withAnimation {
|
||||
scrollToItemId(ci.id)
|
||||
|
@ -65,7 +67,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)
|
||||
}
|
||||
|
@ -87,19 +89,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.simultaneousGesture(TapGesture().onEnded {
|
||||
v.onTapGesture {
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text(title),
|
||||
message: Text(text)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
v
|
||||
}
|
||||
|
@ -157,7 +159,7 @@ struct FramedItemView: View {
|
|||
case let .file(text):
|
||||
ciFileView(chatItem, text)
|
||||
case let .report(text, reason):
|
||||
ciMsgContentView(chatItem, txtPrefix: reason.attrString)
|
||||
ciMsgContentView(chatItem, Text(text.isEmpty ? reason.text : "\(reason.text): ").italic().foregroundColor(.red))
|
||||
case let .link(_, preview):
|
||||
CILinkView(linkPreview: preview)
|
||||
ciMsgContentView(chatItem)
|
||||
|
@ -201,7 +203,6 @@ 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):
|
||||
|
@ -243,8 +244,7 @@ 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(backgroundColor)
|
||||
.environment(\.containerBackground, UIColor(backgroundColor))
|
||||
.background(chatItemFrameContextColor(chatItem, theme))
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
} else {
|
||||
|
@ -270,12 +270,14 @@ struct FramedItemView: View {
|
|||
.padding(.top, 6)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
|
||||
private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View {
|
||||
MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, textStyle: .subheadline)
|
||||
.lineLimit(lines)
|
||||
.padding(.bottom, 6)
|
||||
toggleSecrets(qi.formattedText, $showQuoteSecrets,
|
||||
MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, showSecrets: showQuoteSecrets)
|
||||
.lineLimit(lines)
|
||||
.font(.subheadline)
|
||||
.padding(.bottom, 6)
|
||||
)
|
||||
}
|
||||
|
||||
private func ciQuoteIconView(_ image: String) -> some View {
|
||||
|
@ -295,22 +297,21 @@ struct FramedItemView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem, txtPrefix: NSAttributedString? = nil) -> some View {
|
||||
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem, _ txtPrefix: Text? = nil) -> some View {
|
||||
let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text
|
||||
let rtl = isRightToLeft(text)
|
||||
let ft = text == "" ? [] : ci.formattedText
|
||||
let v = MsgContentView(
|
||||
let v = toggleSecrets(ft, $showSecrets, 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)
|
||||
|
@ -350,6 +351,14 @@ struct FramedItemView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func toggleSecrets<V: View>(_ ft: [FormattedText]?, _ showSecrets: Binding<Bool>, _ 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
|
||||
|
|
|
@ -126,7 +126,7 @@ struct FullScreenMediaView: View {
|
|||
.scaledToFit()
|
||||
}
|
||||
}
|
||||
.onTapGesture { showView = false } // this is used in full screen view, onTapGesture works
|
||||
.onTapGesture { showView = false }
|
||||
}
|
||||
|
||||
private func videoView( _ player: AVPlayer, _ url: URL) -> some View {
|
||||
|
|
|
@ -71,7 +71,7 @@ struct CIMsgError: View {
|
|||
.padding(.vertical, 6)
|
||||
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
|
||||
.textSelection(.disabled)
|
||||
.simultaneousGesture(TapGesture().onEnded(onTap))
|
||||
.onTapGesture(perform: onTap)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,74 +11,53 @@ import SimpleXChat
|
|||
|
||||
let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
|
||||
|
||||
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
|
||||
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(verbatim: ".").fontWeight(w)
|
||||
}
|
||||
|
||||
struct MsgContentView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||
@Environment(\.containerBackground) var containerBackground: UIColor
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var text: String
|
||||
var formattedText: [FormattedText]? = nil
|
||||
var textStyle: UIFont.TextStyle
|
||||
var sender: String? = nil
|
||||
var meta: CIMeta? = nil
|
||||
var mentions: [String: CIMention]? = nil
|
||||
var userMemberId: String? = nil
|
||||
var rightToLeft = false
|
||||
var prefix: NSAttributedString? = nil
|
||||
@State private var showSecrets: Set<Int> = []
|
||||
var showSecrets: Bool
|
||||
var prefix: Text? = nil
|
||||
@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 {
|
||||
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()
|
||||
}
|
||||
msgContentView()
|
||||
.onAppear { switchTyping() }
|
||||
.onDisappear(perform: stopTyping)
|
||||
.onChange(of: meta?.isLive, perform: switchTyping)
|
||||
.onChange(of: meta?.recent, perform: switchTyping)
|
||||
} else {
|
||||
v
|
||||
msgContentView()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
typingIdx = (typingIdx + 1) % typingIndicators.count
|
||||
}
|
||||
} else {
|
||||
stopTyping()
|
||||
|
@ -88,276 +67,119 @@ struct MsgContentView: View {
|
|||
private func stopTyping() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
typingIdx = 0
|
||||
}
|
||||
|
||||
@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
|
||||
private func msgContentView() -> Text {
|
||||
var v = messageText(text, formattedText, sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, secondaryColor: theme.colors.secondary, prefix: prefix)
|
||||
if let mt = meta {
|
||||
if mt.isLive {
|
||||
s.append(typingIndicator(mt.recent))
|
||||
v = v + typingIndicator(mt.recent)
|
||||
}
|
||||
t = Text(AttributedString(s)) + reserveSpaceForMeta(mt)
|
||||
} else {
|
||||
t = Text(AttributedString(s))
|
||||
v = v + reserveSpaceForMeta(mt)
|
||||
}
|
||||
return msgTextResultView(r, t, showSecrets: $showSecrets)
|
||||
return v
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
private func typingIndicator(_ recent: Bool) -> NSAttributedString {
|
||||
recent && !typingIndicators.isEmpty
|
||||
? typingIndicators[typingIdx % 4]
|
||||
: noTyping
|
||||
private func typingIndicator(_ recent: Bool) -> Text {
|
||||
return (recent ? typingIndicators[typingIdx] : noTyping)
|
||||
.font(.body.monospaced())
|
||||
.kerning(-2)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||
(rightToLeft ? textNewLine : Text(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding<Set<Int>>? = nil) -> some View {
|
||||
t.if(r.hasSecrets, transform: hiddenSecretsView)
|
||||
.if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets)) }
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding<Set<Int>>? = nil) -> some View {
|
||||
return GeometryReader { g in
|
||||
Rectangle()
|
||||
.fill(Color.clear)
|
||||
.contentShape(Rectangle())
|
||||
.simultaneousGesture(DragGesture(minimumDistance: 0).onEnded { event in
|
||||
let t = event.translation
|
||||
if t.width * t.width + t.height * t.height > 100 { return }
|
||||
let framesetter = CTFramesetterCreateWithAttributedString(s as CFAttributedString)
|
||||
let path = CGPath(rect: CGRect(origin: .zero, size: g.size), transform: nil)
|
||||
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, s.length), path, nil)
|
||||
let point = CGPoint(x: event.location.x, y: g.size.height - event.location.y) // Flip y for UIKit
|
||||
var index: CFIndex?
|
||||
if let lines = CTFrameGetLines(frame) as? [CTLine] {
|
||||
var origins = [CGPoint](repeating: .zero, count: lines.count)
|
||||
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
|
||||
for i in 0 ..< lines.count {
|
||||
let bounds = CTLineGetBoundsWithOptions(lines[i], .useOpticalBounds)
|
||||
if bounds.offsetBy(dx: origins[i].x, dy: origins[i].y).contains(point) {
|
||||
index = CTLineGetStringIndexForPosition(lines[i], point)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if let index, let (url, browser) = attributedStringLink(s, for: index) {
|
||||
if browser {
|
||||
openBrowserAlert(uri: url)
|
||||
} else {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (URL, Bool)? {
|
||||
var linkURL: URL?
|
||||
var browser: Bool = false
|
||||
s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in
|
||||
if index >= range.location && index < range.location + range.length {
|
||||
if let url = attrs[linkAttrKey] as? NSURL {
|
||||
linkURL = url.absoluteURL
|
||||
browser = attrs[webLinkAttrKey] != nil
|
||||
} else if let showSecrets, let i = attrs[secretAttrKey] as? Int {
|
||||
if showSecrets.wrappedValue.contains(i) {
|
||||
showSecrets.wrappedValue.remove(i)
|
||||
} else {
|
||||
showSecrets.wrappedValue.insert(i)
|
||||
}
|
||||
}
|
||||
stop.pointee = true
|
||||
}
|
||||
}
|
||||
return if let linkURL { (linkURL, browser) } else { nil }
|
||||
}
|
||||
}
|
||||
|
||||
func hiddenSecretsView<V: View>(_ v: V) -> some View {
|
||||
v.overlay(
|
||||
GeometryReader { g in
|
||||
let size = (g.size.width + g.size.height) / 1.4142
|
||||
Image("vertical_logo")
|
||||
.resizable(resizingMode: .tile)
|
||||
.frame(width: size, height: size)
|
||||
.rotationEffect(.degrees(45), anchor: .center)
|
||||
.position(x: g.size.width / 2, y: g.size.height / 2)
|
||||
.clipped()
|
||||
.saturation(0.65)
|
||||
.opacity(0.35)
|
||||
}
|
||||
.mask(v)
|
||||
)
|
||||
}
|
||||
|
||||
private let linkAttrKey = NSAttributedString.Key("chat.simplex.app.link")
|
||||
|
||||
private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink")
|
||||
|
||||
private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret")
|
||||
|
||||
typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool)
|
||||
|
||||
func messageText(
|
||||
_ text: String,
|
||||
_ formattedText: [FormattedText]?,
|
||||
textStyle: UIFont.TextStyle = .body,
|
||||
sender: String?,
|
||||
preview: Bool = false,
|
||||
mentions: [String: CIMention]?,
|
||||
userMemberId: String?,
|
||||
showSecrets: Set<Int>?,
|
||||
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))
|
||||
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.append(NSMutableAttributedString(string: text, attributes: plain))
|
||||
res = Text(s)
|
||||
}
|
||||
|
||||
if let i = icon {
|
||||
res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + textSpace + res
|
||||
}
|
||||
|
||||
return (string: res, hasSecrets: hasSecrets, handleTaps: handleTaps)
|
||||
if let p = prefix {
|
||||
res = p + res
|
||||
}
|
||||
|
||||
func linkAttrs() -> [NSAttributedString.Key: Any] {
|
||||
link = link ?? [
|
||||
.font: font,
|
||||
.foregroundColor: uiLinkColor,
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue
|
||||
]
|
||||
return link!
|
||||
if let s = sender {
|
||||
let t = Text(s)
|
||||
return (preview ? t : t.fontWeight(.medium)) + Text(verbatim: ": ") + res
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
private func mentionText(_ name: String) -> String {
|
||||
name.contains(" @") ? "@'\(name)'" : "@\(name)"
|
||||
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
|
||||
}
|
||||
let tName = mentionText(name)
|
||||
return m.memberId == userMemberId ? tName.foregroundColor(.accentColor) : tName
|
||||
} else {
|
||||
return mentionText(memberName)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private func mentionText(_ name: String) -> Text {
|
||||
Text(verbatim: name.contains(" @") ? "@'\(name)'" : "@\(name)").fontWeight(.semibold)
|
||||
}
|
||||
|
||||
private func linkText(_ s: String, _ link: String, _ preview: Bool, prefix: String, color: Color = Color(uiColor: uiLinkColor), uiColor: UIColor = uiLinkColor) -> Text {
|
||||
preview
|
||||
? Text(s).foregroundColor(color).underline(color: color)
|
||||
: Text(AttributedString(s, attributes: AttributeContainer([
|
||||
.link: NSURL(string: prefix + link) as Any,
|
||||
.foregroundColor: uiColor as Any
|
||||
]))).underline()
|
||||
}
|
||||
|
||||
func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String {
|
||||
|
@ -371,9 +193,9 @@ struct MsgContentView_Previews: PreviewProvider {
|
|||
chat: Chat.sampleData,
|
||||
text: chatItem.text,
|
||||
formattedText: chatItem.formattedText,
|
||||
textStyle: .body,
|
||||
sender: chatItem.memberDisplayName,
|
||||
meta: chatItem.meta
|
||||
meta: chatItem.meta,
|
||||
showSecrets: false
|
||||
)
|
||||
.environmentObject(Chat.sampleData)
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ struct ChatItemForwardingView: View {
|
|||
.alert(item: $alert) { $0.alert }
|
||||
}
|
||||
|
||||
private func forwardListView() -> some View {
|
||||
@ViewBuilder private func forwardListView() -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
if !chatsToForwardTo.isEmpty {
|
||||
List {
|
||||
|
|
|
@ -131,9 +131,9 @@ struct ChatItemInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func details() -> some View {
|
||||
@ViewBuilder private func details() -> some View {
|
||||
let meta = ci.meta
|
||||
return VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(title)
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
|
@ -197,7 +197,7 @@ struct ChatItemInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func historyTab() -> some View {
|
||||
@ViewBuilder private func historyTab() -> some View {
|
||||
GeometryReader { g in
|
||||
let maxWidth = (g.size.width - 32) * 0.84
|
||||
ScrollView {
|
||||
|
@ -227,13 +227,12 @@ struct ChatItemInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
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))
|
||||
@ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(backgroundColor)
|
||||
.background(chatItemFrameColor(ci, theme))
|
||||
.modifier(ChatItemClipped())
|
||||
.contextMenu {
|
||||
if itemVersion.msgContent.text != "" {
|
||||
|
@ -258,9 +257,9 @@ struct ChatItemInfoView: View {
|
|||
.frame(maxWidth: maxWidth, alignment: .leading)
|
||||
}
|
||||
|
||||
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil, backgroundColor: UIColor) -> some View {
|
||||
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil) -> some View {
|
||||
if text != "" {
|
||||
TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId, backgroundColor: backgroundColor)
|
||||
TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId)
|
||||
} else {
|
||||
Text("no text")
|
||||
.italic()
|
||||
|
@ -275,16 +274,14 @@ struct ChatItemInfoView: View {
|
|||
var sender: String? = nil
|
||||
var mentions: [String: CIMention]?
|
||||
var userMemberId: String?
|
||||
var backgroundColor: UIColor
|
||||
@State private var showSecrets: Set<Int> = []
|
||||
@State private var showSecrets = false
|
||||
|
||||
var body: some View {
|
||||
let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: backgroundColor)
|
||||
return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets)
|
||||
toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, secondaryColor: theme.colors.secondary))
|
||||
}
|
||||
}
|
||||
|
||||
private func quoteTab(_ qi: CIQuote) -> some View {
|
||||
@ViewBuilder private func quoteTab(_ qi: CIQuote) -> some View {
|
||||
GeometryReader { g in
|
||||
let maxWidth = (g.size.width - 32) * 0.84
|
||||
ScrollView {
|
||||
|
@ -302,10 +299,9 @@ struct ChatItemInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
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))
|
||||
@ViewBuilder private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(quotedMsgFrameColor(qi, theme))
|
||||
|
@ -338,7 +334,7 @@ struct ChatItemInfoView: View {
|
|||
: theme.appColors.receivedMessage
|
||||
}
|
||||
|
||||
private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
|
||||
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
details()
|
||||
|
@ -376,7 +372,7 @@ struct ChatItemInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View {
|
||||
@ViewBuilder private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View {
|
||||
HStack {
|
||||
ChatInfoImage(chat: Chat(chatInfo: forwardedFromItem.chatInfo), size: 48)
|
||||
.padding(.trailing, 6)
|
||||
|
@ -407,7 +403,7 @@ struct ChatItemInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
@ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
details()
|
||||
|
@ -422,7 +418,7 @@ struct ChatItemInfoView: View {
|
|||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
@ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
let mss = membersStatuses(memberDeliveryStatuses)
|
||||
if !mss.isEmpty {
|
||||
|
|
|
@ -18,10 +18,6 @@ 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 }
|
||||
|
@ -31,11 +27,6 @@ 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 {
|
||||
|
|
|
@ -60,8 +60,6 @@ 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)
|
||||
|
@ -106,22 +104,19 @@ func apiLoadMessages(
|
|||
}
|
||||
}
|
||||
case .around:
|
||||
var newSplits: [Int64]
|
||||
let newSplits: [Int64]
|
||||
if openAroundItemId == nil {
|
||||
newItems.append(contentsOf: oldItems)
|
||||
newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed)
|
||||
} else {
|
||||
newSplits = []
|
||||
}
|
||||
let (itemIndex, splitIndex) = indexToInsertAround(chat.chatInfo.chatType, chat.chatItems.last, to: newItems, Set(newSplits))
|
||||
//indexToInsertAroundTest()
|
||||
newItems.insert(contentsOf: chat.chatItems, at: itemIndex)
|
||||
newSplits.insert(chat.chatItems.last!.id, at: splitIndex)
|
||||
// currently, items will always be added on top, which is index 0
|
||||
newItems.insert(contentsOf: chat.chatItems, at: 0)
|
||||
let newReversed: [ChatItem] = newItems.reversed()
|
||||
let orderedSplits = newSplits
|
||||
await MainActor.run {
|
||||
ItemsModel.shared.reversedChatItems = newReversed
|
||||
chatState.splits = orderedSplits
|
||||
chatState.splits = [chat.chatItems.last!.id] + newSplits
|
||||
chatState.unreadAfterItemId = chat.chatItems.last!.id
|
||||
chatState.totalAfter = navInfo.afterTotal
|
||||
chatState.unreadTotal = chat.chatStats.unreadCount
|
||||
|
@ -135,16 +130,14 @@ func apiLoadMessages(
|
|||
// no need to set it, count will be wrong
|
||||
// chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
}
|
||||
PreloadState.shared.clear()
|
||||
}
|
||||
case .last:
|
||||
newItems.append(contentsOf: oldItems)
|
||||
let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, chatState.splits)
|
||||
removeDuplicates(&newItems, chat)
|
||||
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
|
||||
}
|
||||
|
@ -241,14 +234,10 @@ private func removeDuplicatesAndModifySplitsOnAfterPagination(
|
|||
let indexInSplitRanges = splits.firstIndex(of: paginationChatItemId)
|
||||
// Currently, it should always load from split range
|
||||
let loadingFromSplitRange = indexInSplitRanges != nil
|
||||
let topSplits: [Int64]
|
||||
var splitsToMerge: [Int64]
|
||||
if let indexInSplitRanges, loadingFromSplitRange && indexInSplitRanges + 1 <= splits.count {
|
||||
splitsToMerge = Array(splits[indexInSplitRanges + 1 ..< splits.count])
|
||||
topSplits = Array(splits[0 ..< indexInSplitRanges + 1])
|
||||
var splitsToMerge: [Int64] = if let indexInSplitRanges, loadingFromSplitRange && indexInSplitRanges + 1 <= splits.count {
|
||||
Array(splits[indexInSplitRanges + 1 ..< splits.count])
|
||||
} else {
|
||||
splitsToMerge = []
|
||||
topSplits = []
|
||||
[]
|
||||
}
|
||||
newItems.removeAll(where: { new in
|
||||
let duplicate = newIds.contains(new.id)
|
||||
|
@ -268,8 +257,8 @@ private func removeDuplicatesAndModifySplitsOnAfterPagination(
|
|||
})
|
||||
var newSplits: [Int64] = []
|
||||
if firstItemIdBelowAllSplits != nil {
|
||||
// no splits below anymore, all were merged with bottom items
|
||||
newSplits = topSplits
|
||||
// no splits anymore, all were merged with bottom items
|
||||
newSplits = []
|
||||
} else {
|
||||
if !splitsToRemove.isEmpty {
|
||||
var new = splits
|
||||
|
@ -331,28 +320,6 @@ 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<Int64>, Int) {
|
||||
var unreadInLoaded = 0
|
||||
|
@ -373,139 +340,3 @@ private func removeDuplicates(_ newItems: inout [ChatItem], _ chat: Chat) {
|
|||
let (newIds, _) = mapItemsToIds(chat.chatItems)
|
||||
newItems.removeAll { newIds.contains($0.id) }
|
||||
}
|
||||
|
||||
private typealias SameTimeItem = (index: Int, item: ChatItem)
|
||||
|
||||
// return (item index, split index)
|
||||
private func indexToInsertAround(_ chatType: ChatType, _ lastNew: ChatItem?, to: [ChatItem], _ splits: Set<Int64>) -> (Int, Int) {
|
||||
guard to.count > 0, let lastNew = lastNew else { return (0, 0) }
|
||||
// group sorting: item_ts, item_id
|
||||
// everything else: created_at, item_id
|
||||
let compareByTimeTs = chatType == .group
|
||||
// in case several items have the same time as another item in the `to` array
|
||||
var sameTime: [SameTimeItem] = []
|
||||
|
||||
// trying to find new split index for item looks difficult but allows to not use one more loop.
|
||||
// The idea is to memorize how many splits were till any index (map number of splits until index)
|
||||
// and use resulting itemIndex to decide new split index position.
|
||||
// Because of the possibility to have many items with the same timestamp, it's possible to see `itemIndex < || == || > i`.
|
||||
var splitsTillIndex: [Int] = []
|
||||
var splitsPerPrevIndex = 0
|
||||
|
||||
for i in 0 ..< to.count {
|
||||
let item = to[i]
|
||||
|
||||
splitsPerPrevIndex = splits.contains(item.id) ? splitsPerPrevIndex + 1 : splitsPerPrevIndex
|
||||
splitsTillIndex.append(splitsPerPrevIndex)
|
||||
|
||||
let itemIsNewer = (compareByTimeTs ? item.meta.itemTs > lastNew.meta.itemTs : item.meta.createdAt > lastNew.meta.createdAt)
|
||||
if itemIsNewer || i + 1 == to.count {
|
||||
if (compareByTimeTs ? lastNew.meta.itemTs == item.meta.itemTs : lastNew.meta.createdAt == item.meta.createdAt) {
|
||||
sameTime.append((i, item))
|
||||
}
|
||||
// time to stop the loop. Item is newer or it's the last item in `to` array, taking previous items and checking position inside them
|
||||
let itemIndex: Int
|
||||
if sameTime.count > 1, let first = sameTime.sorted(by: { prev, next in prev.item.meta.itemId < next.item.id }).first(where: { same in same.item.id > lastNew.id }) {
|
||||
itemIndex = first.index
|
||||
} else if sameTime.count == 1 {
|
||||
itemIndex = sameTime[0].item.id > lastNew.id ? sameTime[0].index : sameTime[0].index + 1
|
||||
} else {
|
||||
itemIndex = itemIsNewer ? i : i + 1
|
||||
}
|
||||
let splitIndex = splitsTillIndex[min(itemIndex, splitsTillIndex.count - 1)]
|
||||
let prevItemSplitIndex = itemIndex == 0 ? 0 : splitsTillIndex[min(itemIndex - 1, splitsTillIndex.count - 1)]
|
||||
return (itemIndex, splitIndex == prevItemSplitIndex ? splitIndex : prevItemSplitIndex)
|
||||
}
|
||||
|
||||
if (compareByTimeTs ? lastNew.meta.itemTs == item.meta.itemTs : lastNew.meta.createdAt == item.meta.createdAt) {
|
||||
sameTime.append(SameTimeItem(index: i, item: item))
|
||||
} else {
|
||||
sameTime = []
|
||||
}
|
||||
}
|
||||
// shouldn't be here
|
||||
return (to.count, splits.count)
|
||||
}
|
||||
|
||||
private func indexToInsertAroundTest() {
|
||||
func assert(_ one: (Int, Int), _ two: (Int, Int)) {
|
||||
if one != two {
|
||||
logger.debug("\(String(describing: one)) != \(String(describing: two))")
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
let itemsToInsert = [ChatItem.getSample(3, .groupSnd, Date.init(timeIntervalSince1970: 3), "")]
|
||||
let items1 = [
|
||||
ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""),
|
||||
ChatItem.getSample(1, .groupSnd, Date.init(timeIntervalSince1970: 1), ""),
|
||||
ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 2), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items1, Set([1])), (3, 1))
|
||||
|
||||
let items2 = [
|
||||
ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""),
|
||||
ChatItem.getSample(1, .groupSnd, Date.init(timeIntervalSince1970: 1), ""),
|
||||
ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 3), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items2, Set([2])), (3, 1))
|
||||
|
||||
let items3 = [
|
||||
ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""),
|
||||
ChatItem.getSample(1, .groupSnd, Date.init(timeIntervalSince1970: 3), ""),
|
||||
ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 3), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items3, Set([1])), (3, 1))
|
||||
|
||||
let items4 = [
|
||||
ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""),
|
||||
ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), ""),
|
||||
ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 3), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items4, Set([4])), (1, 0))
|
||||
|
||||
let items5 = [
|
||||
ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""),
|
||||
ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 3), ""),
|
||||
ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items5, Set([2])), (2, 1))
|
||||
|
||||
let items6 = [
|
||||
ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 4), ""),
|
||||
ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 4), ""),
|
||||
ChatItem.getSample(6, .groupSnd, Date.init(timeIntervalSince1970: 4), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items6, Set([5])), (0, 0))
|
||||
|
||||
let items7 = [
|
||||
ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 4), ""),
|
||||
ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 4), ""),
|
||||
ChatItem.getSample(6, .groupSnd, Date.init(timeIntervalSince1970: 4), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, nil, to: items7, Set([6])), (0, 0))
|
||||
|
||||
let items8 = [
|
||||
ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 4), ""),
|
||||
ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), ""),
|
||||
ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 4), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items8, Set([2])), (0, 0))
|
||||
|
||||
let items9 = [
|
||||
ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 3), ""),
|
||||
ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), ""),
|
||||
ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 4), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items9, Set([5])), (1, 0))
|
||||
|
||||
let items10 = [
|
||||
ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), ""),
|
||||
ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 3), ""),
|
||||
ChatItem.getSample(6, .groupSnd, Date.init(timeIntervalSince1970: 4), "")
|
||||
]
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items10, Set([4])), (0, 0))
|
||||
|
||||
let items11: [ChatItem] = []
|
||||
assert(indexToInsertAround(.group, itemsToInsert.last, to: items11, Set([])), (0, 0))
|
||||
}
|
||||
|
|
|
@ -321,101 +321,6 @@ class ActiveChatState {
|
|||
unreadAfter = 0
|
||||
unreadAfterNewestLoaded = 0
|
||||
}
|
||||
|
||||
func itemsRead(_ itemIds: Set<Int64>?, _ newItems: [ChatItem]) {
|
||||
guard let itemIds else {
|
||||
// special case when the whole chat became read
|
||||
unreadTotal = 0
|
||||
unreadAfter = 0
|
||||
return
|
||||
}
|
||||
var unreadAfterItemIndex: Int = -1
|
||||
// since it's more often that the newest items become read, it's logical to loop from the end of the list to finish it faster
|
||||
var i = newItems.count - 1
|
||||
var ids = itemIds
|
||||
// intermediate variables to prevent re-setting state value a lot of times without reason
|
||||
var newUnreadTotal = unreadTotal
|
||||
var newUnreadAfter = unreadAfter
|
||||
while i >= 0 {
|
||||
let item = newItems[i]
|
||||
if item.id == unreadAfterItemId {
|
||||
unreadAfterItemIndex = i
|
||||
}
|
||||
if ids.contains(item.id) {
|
||||
// was unread, now this item is read
|
||||
if (unreadAfterItemIndex == -1) {
|
||||
newUnreadAfter -= 1
|
||||
}
|
||||
newUnreadTotal -= 1
|
||||
ids.remove(item.id)
|
||||
if ids.isEmpty {
|
||||
break
|
||||
}
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
unreadTotal = newUnreadTotal
|
||||
unreadAfter = newUnreadAfter
|
||||
}
|
||||
|
||||
func itemAdded(_ item: (Int64, Bool), _ index: Int) {
|
||||
if item.1 {
|
||||
unreadAfter += 1
|
||||
unreadTotal += 1
|
||||
}
|
||||
}
|
||||
|
||||
func itemsRemoved(_ itemIds: [(Int64, Int, Bool)], _ newItems: [ChatItem]) {
|
||||
var newSplits: [Int64] = []
|
||||
for split in splits {
|
||||
let index = itemIds.firstIndex(where: { (delId, _, _) in delId == split })
|
||||
// deleted the item that was right before the split between items, find newer item so it will act like the split
|
||||
if let index {
|
||||
let idx = itemIds[index].1 - itemIds.filter { (_, delIndex, _) in delIndex <= index }.count
|
||||
let newSplit = newItems.count > idx && idx >= 0 ? newItems[idx].id : nil
|
||||
// it the whole section is gone and splits overlap, don't add it at all
|
||||
if let newSplit, !newSplits.contains(newSplit) {
|
||||
newSplits.append(newSplit)
|
||||
}
|
||||
} else {
|
||||
newSplits.append(split)
|
||||
}
|
||||
}
|
||||
splits = newSplits
|
||||
|
||||
let index = itemIds.firstIndex(where: { (delId, _, _) in delId == unreadAfterItemId })
|
||||
// unread after item was removed
|
||||
if let index {
|
||||
let idx = itemIds[index].1 - itemIds.filter { (_, delIndex, _) in delIndex <= index }.count
|
||||
var newUnreadAfterItemId = newItems.count > idx && idx >= 0 ? newItems[idx].id : nil
|
||||
let newUnreadAfterItemWasNull = newUnreadAfterItemId == nil
|
||||
if newUnreadAfterItemId == nil {
|
||||
// everything on top (including unread after item) were deleted, take top item as unread after id
|
||||
newUnreadAfterItemId = newItems.first?.id
|
||||
}
|
||||
if let newUnreadAfterItemId {
|
||||
unreadAfterItemId = newUnreadAfterItemId
|
||||
totalAfter -= itemIds.filter { (_, delIndex, _) in delIndex > index }.count
|
||||
unreadTotal -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex <= index && isRcvNew }.count
|
||||
unreadAfter -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex > index && isRcvNew }.count
|
||||
if newUnreadAfterItemWasNull {
|
||||
// since the unread after item was moved one item after initial position, adjust counters accordingly
|
||||
if newItems.first?.isRcvNew == true {
|
||||
unreadTotal += 1
|
||||
unreadAfter -= 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// all items were deleted, 0 items in chatItems
|
||||
unreadAfterItemId = -1
|
||||
totalAfter = 0
|
||||
unreadTotal = 0
|
||||
unreadAfter = 0
|
||||
}
|
||||
} else {
|
||||
totalAfter -= itemIds.count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BoxedValue<T: Hashable>: Equatable, Hashable {
|
||||
|
@ -454,3 +359,101 @@ func visibleItemIndexesNonReversed(_ listState: EndlessScrollView<MergedItem>.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<Int64>?, _ 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() }
|
||||
}
|
||||
|
|
|
@ -9,23 +9,25 @@
|
|||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Binding<Bool>, _ chat: Chat) async {
|
||||
await MainActor.run {
|
||||
loadingMoreItems.wrappedValue = true
|
||||
loadingBottomItems.wrappedValue = true
|
||||
func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Binding<Bool>, _ chat: Chat) {
|
||||
if ItemsModel.shared.chatState.totalAfter == 0 {
|
||||
return
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 500_000000)
|
||||
if ChatModel.shared.chatId != chat.chatInfo.id {
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,12 +36,6 @@ 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(
|
||||
|
@ -47,41 +43,26 @@ func preloadIfNeeded(
|
|||
_ ignoreLoadingRequests: Binding<Int64?>,
|
||||
_ listState: EndlessScrollView<MergedItem>.ListState,
|
||||
_ mergedItems: BoxedValue<MergedItems>,
|
||||
loadItems: @escaping (Bool, ChatPagination) async -> Bool,
|
||||
loadLastItems: @escaping () async -> Void
|
||||
loadItems: @escaping (Bool, ChatPagination) async -> Bool
|
||||
) {
|
||||
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
|
||||
}
|
||||
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()
|
||||
}
|
||||
state.prevFirstVisible = listState.firstVisibleItemId as! Int64
|
||||
state.prevItemsCount = mergedItems.boxedValue.indexInParentItems.count
|
||||
state.preloading = true
|
||||
let allowLoadMore = allowLoadMoreItems.wrappedValue
|
||||
Task {
|
||||
defer {
|
||||
state.preloading = false
|
||||
}
|
||||
} else if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded {
|
||||
state.preloading = true
|
||||
Task {
|
||||
defer { state.preloading = false }
|
||||
await loadLastItems()
|
||||
await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in
|
||||
await loadItems(false, pagination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +105,6 @@ 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
|
||||
}
|
||||
|
|
|
@ -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: CreatedConnLink?
|
||||
@State private var groupLink: String?
|
||||
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
||||
@State private var forwardedChatItems: [ChatItem] = []
|
||||
@State private var selectedChatItems: Set<Int64>? = nil
|
||||
|
@ -71,9 +71,10 @@ struct ChatView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var viewBody: some View {
|
||||
let cInfo = chat.chatInfo
|
||||
return ZStack {
|
||||
ZStack {
|
||||
let wallpaperImage = theme.wallpaper.type.image
|
||||
let wallpaperType = theme.wallpaper.type
|
||||
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
|
||||
|
@ -90,32 +91,18 @@ 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, 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)
|
||||
}
|
||||
)
|
||||
FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel)
|
||||
}
|
||||
connectingText()
|
||||
if selectedChatItems == nil {
|
||||
let reason = chat.chatInfo.userCantSendReason
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: $keyboardVisible,
|
||||
keyboardHiddenDate: $keyboardHiddenDate,
|
||||
selectedRange: $selectedRange,
|
||||
disabledText: reason?.composeLabel
|
||||
selectedRange: $selectedRange
|
||||
)
|
||||
.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,
|
||||
|
@ -275,6 +262,7 @@ 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
|
||||
|
@ -290,7 +278,7 @@ struct ChatView: View {
|
|||
if chatModel.chatId == nil {
|
||||
chatModel.chatItemStatuses = [:]
|
||||
ItemsModel.shared.reversedChatItems = []
|
||||
ItemsModel.shared.chatState.clear()
|
||||
ItemsModel.shared.chatItemsChangesListener.cleared()
|
||||
chatModel.groupMembers = []
|
||||
chatModel.groupMembersIndexes.removeAll()
|
||||
chatModel.membersLoaded = false
|
||||
|
@ -596,6 +584,7 @@ struct ChatView: View {
|
|||
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
|
||||
|
@ -640,6 +629,7 @@ struct ChatView: View {
|
|||
if let unreadIndex {
|
||||
scrollView.scrollToItem(unreadIndex)
|
||||
}
|
||||
loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
allowLoadMoreItems = true
|
||||
}
|
||||
|
@ -657,8 +647,10 @@ struct ChatView: View {
|
|||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -739,7 +731,6 @@ struct ChatView: View {
|
|||
let theme: AppTheme
|
||||
let scrollView: EndlessScrollView<MergedItem>
|
||||
let chat: Chat
|
||||
@Binding var loadingMoreItems: Bool
|
||||
@Binding var loadingTopItems: Bool
|
||||
@Binding var requestedTopScroll: Bool
|
||||
@Binding var loadingBottomItems: Bool
|
||||
|
@ -747,7 +738,6 @@ struct ChatView: View {
|
|||
@Binding var animatedScrollingInProgress: Bool
|
||||
let listState: EndlessScrollView<MergedItem>.ListState
|
||||
@ObservedObject var model: FloatingButtonModel
|
||||
let reloadItems: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
|
@ -805,7 +795,7 @@ struct ChatView: View {
|
|||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if loadingBottomItems || !ItemsModel.shared.lastItemsLoaded {
|
||||
if loadingBottomItems {
|
||||
requestedTopScroll = false
|
||||
requestedBottomScroll = true
|
||||
} else {
|
||||
|
@ -825,7 +815,7 @@ struct ChatView: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: loadingBottomItems) { loading in
|
||||
if !loading && requestedBottomScroll && ItemsModel.shared.lastItemsLoaded {
|
||||
if !loading && requestedBottomScroll {
|
||||
requestedBottomScroll = false
|
||||
scrollToBottom()
|
||||
}
|
||||
|
@ -834,25 +824,15 @@ struct ChatView: View {
|
|||
}
|
||||
|
||||
private func scrollToTopUnread() {
|
||||
Task {
|
||||
if !ItemsModel.shared.chatState.splits.isEmpty {
|
||||
await MainActor.run { loadingMoreItems = true }
|
||||
await loadChat(chatId: chat.id, openAroundItemId: nil, clearItems: false)
|
||||
await MainActor.run { reloadItems() }
|
||||
if let index = listState.items.lastIndex(where: { $0.hasUnread() }) {
|
||||
await MainActor.run { animatedScrollingInProgress = true }
|
||||
await scrollView.scrollToItemAnimated(index)
|
||||
await MainActor.run { animatedScrollingInProgress = false }
|
||||
}
|
||||
await MainActor.run { loadingMoreItems = false }
|
||||
} else if let index = listState.items.lastIndex(where: { $0.hasUnread() }) {
|
||||
await MainActor.run { animatedScrollingInProgress = true }
|
||||
// scroll to the top unread item
|
||||
if let index = listState.items.lastIndex(where: { $0.hasUnread() }) {
|
||||
animatedScrollingInProgress = true
|
||||
// scroll to the top unread item
|
||||
Task {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1167,11 +1147,6 @@ struct ChatView: View {
|
|||
} else {
|
||||
await loadChatItems(chat, pagination)
|
||||
}
|
||||
},
|
||||
loadLastItems: {
|
||||
if !loadingMoreItems {
|
||||
await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -1272,11 +1247,18 @@ struct ChatView: View {
|
|||
nil
|
||||
}
|
||||
let showAvatar = shouldShowAvatar(item, listItem.nextItem)
|
||||
let itemSeparation: ItemSeparation
|
||||
let single = switch merged {
|
||||
case .single: true
|
||||
default: false
|
||||
}
|
||||
let itemSeparation = getItemSeparation(item, single || revealed ? listItem.prevItem: nil)
|
||||
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)
|
||||
}
|
||||
return VStack(spacing: 0) {
|
||||
if let last {
|
||||
DateSeparator(date: last.meta.itemTs).padding(8)
|
||||
|
@ -1286,10 +1268,10 @@ struct ChatView: View {
|
|||
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
let checked = selected.contains(chatItem.id)
|
||||
selectUnselectChatItem(select: !checked, chatItem)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if let date = itemSeparation.date {
|
||||
|
@ -1478,7 +1460,7 @@ struct ChatView: View {
|
|||
}
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
if let mem = m.getGroupMember(member.groupMemberId) {
|
||||
selectedMember = mem
|
||||
} else {
|
||||
|
@ -1487,7 +1469,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 }
|
||||
}
|
||||
|
@ -1537,9 +1519,9 @@ struct ChatView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
|
||||
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
|
||||
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||
return VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||
VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||
HStack {
|
||||
if ci.chatDir.sent {
|
||||
goToItemButton(true)
|
||||
|
@ -1631,9 +1613,9 @@ struct ChatView: View {
|
|||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 4)
|
||||
.if(chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted)) { v in
|
||||
v.simultaneousGesture(TapGesture().onEnded {
|
||||
v.onTapGesture {
|
||||
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
|
||||
})
|
||||
}
|
||||
}
|
||||
switch chat.chatInfo {
|
||||
case let .group(groupInfo):
|
||||
|
@ -2223,12 +2205,15 @@ struct ChatView: View {
|
|||
}
|
||||
|
||||
func goToItemInnerButton(_ alignStart: Bool, _ image: String, touchInProgress: Bool, _ onClick: @escaping () -> Void) -> some View {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
.frame(width: 13, height: 13)
|
||||
.padding([alignStart ? .trailing : .leading], 10)
|
||||
.tint(theme.colors.secondary.opacity(touchInProgress ? 1.0 : 0.4))
|
||||
.simultaneousGesture(TapGesture().onEnded(onClick))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
@ -18,7 +18,7 @@ struct ComposeLinkView: View {
|
|||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
if let linkPreview {
|
||||
if let linkPreview = linkPreview {
|
||||
linkPreviewView(linkPreview)
|
||||
} else {
|
||||
ProgressView()
|
||||
|
|
|
@ -327,7 +327,6 @@ 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
|
||||
|
@ -392,7 +391,7 @@ struct ComposeView: View {
|
|||
Image(systemName: "paperclip")
|
||||
.resizable()
|
||||
}
|
||||
.disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled || (chat.chatInfo.contact?.nextSendGrpInv ?? false))
|
||||
.disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false))
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.leading, 12)
|
||||
|
@ -442,13 +441,19 @@ struct ComposeView: View {
|
|||
: theme.colors.primary
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.disabled(!chat.chatInfo.sendMsgEnabled)
|
||||
.disabled(!chat.userCanSend)
|
||||
|
||||
if let disabledText {
|
||||
Text(disabledText)
|
||||
if chat.userIsObserver {
|
||||
Text("you are observer")
|
||||
.italic()
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "You can't send messages!",
|
||||
message: "Please contact group admin."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -474,8 +479,8 @@ struct ComposeView: View {
|
|||
hasSimplexLink = false
|
||||
}
|
||||
}
|
||||
.onChange(of: chat.chatInfo.sendMsgEnabled) { sendEnabled in
|
||||
if !sendEnabled {
|
||||
.onChange(of: chat.userCanSend) { canSend in
|
||||
if !canSend {
|
||||
cancelCurrentVoiceRecording()
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
|
@ -1249,14 +1254,11 @@ struct ComposeView: View {
|
|||
if pendingLinkUrl == url {
|
||||
composeState = composeState.copy(preview: .linkPreview(linkPreview: nil))
|
||||
getLinkPreview(url: url) { linkPreview in
|
||||
if let linkPreview, pendingLinkUrl == url {
|
||||
if let linkPreview = linkPreview,
|
||||
pendingLinkUrl == url {
|
||||
composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview))
|
||||
} else {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
composeState = composeState.copy(preview: .noPreview)
|
||||
}
|
||||
pendingLinkUrl = nil
|
||||
}
|
||||
pendingLinkUrl = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,10 +70,8 @@ struct ContextItemView: View {
|
|||
.lineLimit(lines)
|
||||
}
|
||||
|
||||
private func contextMsgPreview(_ contextItem: ChatItem) -> some View {
|
||||
let r = messageText(contextItem.text, contextItem.formattedText, sender: nil, preview: true, mentions: contextItem.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(background))
|
||||
let t = attachment() + Text(AttributedString(r.string))
|
||||
return t.if(r.hasSecrets, transform: hiddenSecretsView)
|
||||
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)
|
||||
|
||||
func attachment() -> Text {
|
||||
let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) {
|
||||
|
|
|
@ -15,7 +15,6 @@ 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
|
||||
|
@ -256,7 +255,6 @@ struct SendMessageView: View {
|
|||
}
|
||||
|
||||
private struct RecordVoiceMessageButton: View {
|
||||
@Environment(\.isEnabled) var isEnabled
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var startVoiceMessageRecording: (() -> Void)?
|
||||
var finishVoiceMessageRecording: (() -> Void)?
|
||||
|
@ -265,12 +263,13 @@ struct SendMessageView: View {
|
|||
@State private var pressed: TimeInterval? = nil
|
||||
|
||||
var body: some View {
|
||||
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)
|
||||
Button(action: {}) {
|
||||
Image(systemName: "mic.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.disabled(disabled)
|
||||
.frame(width: 31, height: 31)
|
||||
.padding([.bottom, .trailing], 4)
|
||||
|
@ -280,7 +279,9 @@ struct SendMessageView: View {
|
|||
pressed = ProcessInfo.processInfo.systemUptime
|
||||
startVoiceMessageRecording?()
|
||||
} else {
|
||||
if let pressed, ProcessInfo.processInfo.systemUptime - pressed >= 1 {
|
||||
let now = ProcessInfo.processInfo.systemUptime
|
||||
if let pressed = pressed,
|
||||
now - pressed >= 1 {
|
||||
finishVoiceMessageRecording?()
|
||||
}
|
||||
holdingVMR = false
|
||||
|
@ -354,7 +355,7 @@ struct SendMessageView: View {
|
|||
Image(systemName: "bolt.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(isEnabled ? theme.colors.primary : theme.colors.secondary)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
|
|
|
@ -171,9 +171,6 @@ class EndlessScrollView<ScrollItem>: 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
|
||||
|
@ -287,8 +284,7 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
|
||||
func updateItems(_ items: [ScrollItem], _ forceReloadVisible: Bool = false) {
|
||||
if !Thread.isMainThread {
|
||||
logger.error("Use main thread to update items")
|
||||
return
|
||||
fatalError("Use main thread to update items")
|
||||
}
|
||||
if bounds.height == 0 {
|
||||
self.listState.items = items
|
||||
|
@ -306,7 +302,6 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
if items.isEmpty {
|
||||
listState.visibleItems.forEach { item in item.view.removeFromSuperview() }
|
||||
listState.visibleItems = []
|
||||
listState.itemsCanCoverScreen = false
|
||||
listState.firstVisibleItemId = EndlessScrollView<ScrollItem>.DEFAULT_ITEM_ID
|
||||
listState.firstVisibleItemIndex = 0
|
||||
listState.firstVisibleItemOffset = -insetTop
|
||||
|
@ -327,7 +322,6 @@ class EndlessScrollView<ScrollItem>: 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
|
||||
|
@ -345,11 +339,7 @@ class EndlessScrollView<ScrollItem>: 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 {
|
||||
|
@ -399,7 +389,6 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
addSubview(vis.view)
|
||||
}
|
||||
newVisible.append(vis)
|
||||
visibleItemsHeight += vis.view.frame.height
|
||||
nextOffsetY = vis.view.frame.origin.y
|
||||
} else {
|
||||
let vis: VisibleItem
|
||||
|
@ -417,7 +406,6 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
addSubview(vis.view)
|
||||
}
|
||||
newVisible.append(vis)
|
||||
visibleItemsHeight += vis.view.frame.height
|
||||
}
|
||||
if abs(nextOffsetY) < contentOffsetY && !allowOneMore {
|
||||
break
|
||||
|
@ -447,7 +435,6 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
}
|
||||
offset += vis.view.frame.height
|
||||
newVisible.insert(vis, at: 0)
|
||||
visibleItemsHeight += vis.view.frame.height
|
||||
if offset >= contentOffsetY + bounds.height {
|
||||
break
|
||||
}
|
||||
|
@ -463,15 +450,11 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
|
|||
prevProcessedOffset = contentOffsetY
|
||||
|
||||
listState.visibleItems = newVisible
|
||||
// bottom drawing starts from 0 until top visible area at least (bound.height - insetTop) or above top bar (bounds.height).
|
||||
// For visible items to preserve offset after adding more items having such height is enough
|
||||
listState.itemsCanCoverScreen = visibleItemsHeight >= bounds.height - insetTop
|
||||
listState.items = items
|
||||
|
||||
listState.firstVisibleItemId = listState.visibleItems.first?.item.id ?? EndlessScrollView<ScrollItem>.DEFAULT_ITEM_ID
|
||||
listState.firstVisibleItemIndex = listState.visibleItems.first?.index ?? 0
|
||||
listState.firstVisibleItemOffset = listState.visibleItems.first?.offset ?? -insetTop
|
||||
// updating the items with the last step in order to call listener with fully updated state
|
||||
listState.items = items
|
||||
|
||||
estimatedContentHeight.update(contentOffset, listState, averageItemHeight, itemsCountChanged)
|
||||
scrollBarView.contentSize = CGSizeMake(bounds.width, estimatedContentHeight.virtualOverscrolledHeight)
|
||||
|
|
|
@ -145,9 +145,9 @@ struct AddGroupMembersViewCommon: View {
|
|||
return dummy
|
||||
}()
|
||||
|
||||
private func inviteMembersButton() -> some View {
|
||||
@ViewBuilder private func inviteMembersButton() -> some View {
|
||||
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat"
|
||||
return Button {
|
||||
Button {
|
||||
inviteMembers()
|
||||
} label: {
|
||||
HStack {
|
||||
|
|
|
@ -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: CreatedConnLink?
|
||||
@State private var groupLink: String?
|
||||
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
||||
@State private var groupLinkNavLinkActive: Bool = false
|
||||
@State private var addMembersNavLinkActive: Bool = false
|
||||
|
@ -144,9 +144,17 @@ struct GroupChatInfoView: View {
|
|||
let filteredMembers = s == ""
|
||||
? members
|
||||
: members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
|
||||
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert)
|
||||
MemberRowView(groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert)
|
||||
ForEach(filteredMembers) { member in
|
||||
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, alert: $alert)
|
||||
ZStack {
|
||||
NavigationLink {
|
||||
memberInfoView(member)
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.opacity(0)
|
||||
MemberRowView(groupInfo: groupInfo, groupMember: member, alert: $alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,9 +292,9 @@ struct GroupChatInfoView: View {
|
|||
.disabled(!groupInfo.ready || chat.chatItems.isEmpty)
|
||||
}
|
||||
|
||||
private func addMembersActionButton(width: CGFloat) -> some View {
|
||||
ZStack {
|
||||
if chat.chatInfo.incognito {
|
||||
@ViewBuilder private func addMembersActionButton(width: CGFloat) -> some View {
|
||||
if chat.chatInfo.incognito {
|
||||
ZStack {
|
||||
InfoViewButton(image: "link.badge.plus", title: "invite", width: width) {
|
||||
groupLinkNavLinkActive = true
|
||||
}
|
||||
|
@ -298,7 +306,10 @@ struct GroupChatInfoView: View {
|
|||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
} else {
|
||||
}
|
||||
.disabled(!groupInfo.ready)
|
||||
} else {
|
||||
ZStack {
|
||||
InfoViewButton(image: "person.fill.badge.plus", title: "invite", width: width) {
|
||||
addMembersNavLinkActive = true
|
||||
}
|
||||
|
@ -311,8 +322,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 {
|
||||
|
@ -350,7 +361,6 @@ struct GroupChatInfoView: View {
|
|||
}
|
||||
|
||||
private struct MemberRowView: View {
|
||||
var chat: Chat
|
||||
var groupInfo: GroupInfo
|
||||
@ObservedObject var groupMember: GMember
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
|
@ -359,7 +369,7 @@ struct GroupChatInfoView: View {
|
|||
|
||||
var body: some View {
|
||||
let member = groupMember.wrapped
|
||||
let v1 = HStack{
|
||||
let v = HStack{
|
||||
MemberProfileImage(member, size: 38)
|
||||
.padding(.trailing, 2)
|
||||
// TODO server connection status
|
||||
|
@ -376,20 +386,6 @@ 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 {
|
||||
|
@ -414,11 +410,6 @@ 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"
|
||||
|
@ -497,6 +488,11 @@ 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 {
|
||||
|
@ -573,9 +569,9 @@ struct GroupChatInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func leaveGroupButton() -> some View {
|
||||
@ViewBuilder private func leaveGroupButton() -> some View {
|
||||
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat"
|
||||
return Button(role: .destructive) {
|
||||
Button(role: .destructive) {
|
||||
alert = .leaveGroupAlert
|
||||
} label: {
|
||||
Label(label, systemImage: "rectangle.portrait.and.arrow.right")
|
||||
|
|
|
@ -10,14 +10,12 @@ import SwiftUI
|
|||
import SimpleXChat
|
||||
|
||||
struct GroupLinkView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var groupId: Int64
|
||||
@Binding var groupLink: CreatedConnLink?
|
||||
@Binding var groupLink: String?
|
||||
@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
|
||||
|
@ -71,10 +69,10 @@ struct GroupLinkView: View {
|
|||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
SimpleXCreatedLinkQRCode(link: groupLink, short: $showShortLink)
|
||||
.id("simplex-qrcode-view-for-\(groupLink.simplexChatUri(short: showShortLink))")
|
||||
SimpleXLinkQRCode(uri: groupLink)
|
||||
.id("simplex-qrcode-view-for-\(groupLink)")
|
||||
Button {
|
||||
showShareSheet(items: [groupLink.simplexChatUri(short: showShortLink)])
|
||||
showShareSheet(items: [simplexChatLink(groupLink)])
|
||||
} label: {
|
||||
Label("Share link", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
|
@ -95,10 +93,6 @@ 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 {
|
||||
|
@ -164,8 +158,8 @@ struct GroupLinkView: View {
|
|||
|
||||
struct GroupLinkView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@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
|
||||
@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
|
||||
|
||||
return Group {
|
||||
GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member))
|
||||
|
|
|
@ -196,9 +196,7 @@ struct GroupMentionsView: View {
|
|||
newName = composeState.mentionMemberName(member.wrapped.memberProfile.displayName)
|
||||
}
|
||||
mentions[newName] = CIMention(groupMember: member.wrapped)
|
||||
var msgMention = newName.contains(" ") || newName.last?.isPunctuation == true
|
||||
? "@'\(newName)'"
|
||||
: "@\(newName)"
|
||||
var msgMention = "@" + (newName.contains(" ") ? "'\(newName)'" : newName)
|
||||
var newPos = r.location + msgMention.count
|
||||
let newMsgLength = composeState.message.count + msgMention.count - r.length
|
||||
print(newPos)
|
||||
|
|
|
@ -18,7 +18,6 @@ struct GroupWelcomeView: View {
|
|||
@State private var editMode = true
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State private var showSaveDialog = false
|
||||
@State private var showSecrets: Set<Int> = []
|
||||
|
||||
let maxByteCount = 1200
|
||||
|
||||
|
@ -59,8 +58,7 @@ struct GroupWelcomeView: View {
|
|||
}
|
||||
|
||||
private func textPreview() -> some View {
|
||||
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)
|
||||
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, mentions: nil, userMemberId: nil, showSecrets: false, secondaryColor: theme.colors.secondary)
|
||||
.frame(minHeight: 130, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
|
|
@ -90,11 +90,11 @@ struct ChatListNavLink: View {
|
|||
.actionSheet(item: $actionSheet) { $0.actionSheet }
|
||||
}
|
||||
|
||||
private func contactNavLink(_ contact: Contact) -> some View {
|
||||
@ViewBuilder 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))
|
||||
.frameCompat(height: dynamicRowHeight)
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button {
|
||||
deleteContactDialog(
|
||||
|
@ -121,7 +121,6 @@ struct ChatListNavLink: View {
|
|||
selection: $chatModel.chatId,
|
||||
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }
|
||||
)
|
||||
.frameCompat(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
toggleFavoriteButton()
|
||||
|
@ -146,6 +145,7 @@ 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)
|
||||
.frameCompat(height: dynamicRowHeight)
|
||||
.frame(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))
|
||||
.frameCompat(height: dynamicRowHeight)
|
||||
.frame(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
|
||||
)
|
||||
.frameCompat(height: dynamicRowHeight)
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
toggleFavoriteButton()
|
||||
|
@ -243,14 +243,14 @@ struct ChatListNavLink: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View {
|
||||
@ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View {
|
||||
NavLinkPlain(
|
||||
chatId: chat.chatInfo.id,
|
||||
selection: $chatModel.chatId,
|
||||
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
|
||||
disabled: !noteFolder.ready
|
||||
)
|
||||
.frameCompat(height: dynamicRowHeight)
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
}
|
||||
|
@ -433,7 +433,6 @@ 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) }
|
||||
|
@ -452,6 +451,7 @@ struct ChatListNavLink: View {
|
|||
}
|
||||
.tint(.red)
|
||||
}
|
||||
.frame(height: dynamicRowHeight)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { showContactRequestDialog = true }
|
||||
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
|
||||
|
@ -463,7 +463,6 @@ 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
|
||||
|
@ -481,6 +480,7 @@ 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: Data?) -> some View {
|
||||
private func invalidJSONPreview(_ json: String) -> some View {
|
||||
Text("invalid chat data")
|
||||
.foregroundColor(.red)
|
||||
.padding(4)
|
||||
.frameCompat(height: dynamicRowHeight)
|
||||
.frame(height: dynamicRowHeight)
|
||||
.onTapGesture { showInvalidJSON = true }
|
||||
.appSheet(isPresented: $showInvalidJSON) {
|
||||
invalidJSONView(dataToString(json))
|
||||
invalidJSONView(json)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
}
|
||||
}
|
||||
|
@ -603,24 +603,6 @@ struct ChatListNavLink: View {
|
|||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
@inline(__always)
|
||||
@ViewBuilder fileprivate func frameCompat(height: CGFloat) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
self.frame(height: height)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
.padding(.leading, 16)
|
||||
self
|
||||
.frame(height: height)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert {
|
||||
Alert(
|
||||
title: Text("Reject contact request"),
|
||||
|
@ -707,7 +689,7 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) {
|
|||
}
|
||||
|
||||
func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert {
|
||||
if let r = error as? ChatError,
|
||||
if let r = error as? ChatResponse,
|
||||
let alert = getNetworkErrorAlert(r) {
|
||||
return alert
|
||||
} else {
|
||||
|
|
|
@ -335,9 +335,9 @@ struct ChatListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var chatList: some View {
|
||||
@ViewBuilder private var chatList: some View {
|
||||
let cs = filteredChats()
|
||||
return ZStack {
|
||||
ZStack {
|
||||
ScrollViewReader { scrollProxy in
|
||||
List {
|
||||
if !chatModel.chats.isEmpty {
|
||||
|
@ -367,7 +367,13 @@ struct ChatListView: View {
|
|||
.offset(x: -8)
|
||||
} else {
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat, parentSheet: $sheet)
|
||||
VStack(spacing: .zero) {
|
||||
Divider()
|
||||
.padding(.leading, 16)
|
||||
ChatListNavLink(chat: chat, parentSheet: $sheet)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets())
|
||||
|
@ -798,7 +804,7 @@ struct TagsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func expandedPresetTagsFiltersView() -> some View {
|
||||
@ViewBuilder private func expandedPresetTagsFiltersView() -> some View {
|
||||
ForEach(PresetTag.allCases, id: \.id) { tag in
|
||||
if (chatTagsModel.presetTags[tag] ?? 0) > 0 {
|
||||
expandedTagFilterView(tag)
|
||||
|
|
|
@ -187,14 +187,13 @@ struct ChatPreviewView: View {
|
|||
.kerning(-2)
|
||||
}
|
||||
|
||||
private func chatPreviewLayout(_ text: Text?, draft: Bool = false, hasFilePreview: Bool = false, hasSecrets: Bool) -> some View {
|
||||
private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: Bool = false) -> 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))
|
||||
|
@ -260,13 +259,11 @@ struct ChatPreviewView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func messageDraft(_ draft: ComposeState) -> (Text, Bool) {
|
||||
private func messageDraft(_ draft: ComposeState) -> Text {
|
||||
let msg = draft.message
|
||||
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)
|
||||
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)
|
||||
|
||||
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
|
||||
Text(Image(systemName: s)).foregroundColor(color) + textSpace
|
||||
|
@ -282,11 +279,10 @@ struct ChatPreviewView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func chatItemPreview(_ cItem: ChatItem) -> (Text, Bool) {
|
||||
func chatItemPreview(_ cItem: ChatItem) -> Text {
|
||||
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
|
||||
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
||||
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)
|
||||
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())
|
||||
|
||||
// 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
|
||||
|
@ -313,21 +309,19 @@ struct ChatPreviewView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func prefix() -> NSAttributedString? {
|
||||
func prefix() -> Text {
|
||||
switch cItem.content.msgContent {
|
||||
case let .report(_, reason): reason.attrString
|
||||
default: nil
|
||||
case let .report(_, reason): return Text(!itemText.isEmpty ? "\(reason.text): " : reason.text).italic().foregroundColor(Color.red)
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View {
|
||||
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
|
||||
let (t, hasSecrets) = messageDraft(draft)
|
||||
chatPreviewLayout(t, draft: true, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets)
|
||||
chatPreviewLayout(messageDraft(draft), draft: true, hasFilePreview)
|
||||
} else if let cItem = cItem {
|
||||
let (t, hasSecrets) = chatItemPreview(cItem)
|
||||
chatPreviewLayout(itemStatusMark(cItem) + t, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets)
|
||||
chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem), hasFilePreview)
|
||||
} else {
|
||||
switch (chat.chatInfo) {
|
||||
case let .direct(contact):
|
||||
|
@ -354,6 +348,7 @@ 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):
|
||||
|
@ -375,7 +370,17 @@ struct ChatPreviewView: View {
|
|||
.cornerRadius(8)
|
||||
}
|
||||
.onTapGesture {
|
||||
openBrowserAlert(uri: preview.uri)
|
||||
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) })
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .image(_, image):
|
||||
|
@ -405,7 +410,7 @@ struct ChatPreviewView: View {
|
|||
: chatPreviewInfoText("you are invited to group")
|
||||
}
|
||||
|
||||
private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
|
||||
@ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
|
||||
Text(text)
|
||||
.frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
||||
.padding([.leading, .trailing], 8)
|
||||
|
@ -485,7 +490,7 @@ struct ChatPreviewView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func groupReportsIcon(size: CGFloat) -> some View {
|
||||
@ViewBuilder func groupReportsIcon(size: CGFloat) -> some View {
|
||||
Image(systemName: "flag")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
|
|
|
@ -14,7 +14,6 @@ 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
|
||||
|
@ -62,19 +61,14 @@ struct ContactConnectionInfo: View {
|
|||
}
|
||||
|
||||
if contactConnection.initiated,
|
||||
let connLinkInv = contactConnection.connLinkInv {
|
||||
SimpleXCreatedLinkQRCode(link: connLinkInv, short: $showShortLink)
|
||||
.id("simplex-invitation-qrcode-\(connLinkInv.simplexChatUri(short: showShortLink))")
|
||||
let connReqInv = contactConnection.connReqInv {
|
||||
SimpleXLinkQRCode(uri: simplexChatLink(connReqInv))
|
||||
incognitoEnabled()
|
||||
shareLinkButton(connLinkInv, short: showShortLink)
|
||||
oneTimeLinkLearnMoreButton()
|
||||
shareLinkButton(connReqInv, theme.colors.secondary)
|
||||
oneTimeLinkLearnMoreButton(theme.colors.secondary)
|
||||
} else {
|
||||
incognitoEnabled()
|
||||
oneTimeLinkLearnMoreButton()
|
||||
}
|
||||
} header: {
|
||||
if let connLinkInv = contactConnection.connLinkInv, connLinkInv.connShortLink != nil {
|
||||
ToggleShortLinkHeader(text: Text(""), link: connLinkInv, short: $showShortLink)
|
||||
oneTimeLinkLearnMoreButton(theme.colors.secondary)
|
||||
}
|
||||
} footer: {
|
||||
sharedProfileInfo(contactConnection.incognito)
|
||||
|
@ -173,22 +167,26 @@ struct ContactConnectionInfo: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func shareLinkButton(_ connLinkInvitation: CreatedConnLink, short: Bool) -> some View {
|
||||
private func shareLinkButton(_ connReqInvitation: String, _ secondaryColor: Color) -> some View {
|
||||
Button {
|
||||
showShareSheet(items: [connLinkInvitation.simplexChatUri(short: short)])
|
||||
showShareSheet(items: [simplexChatLink(connReqInvitation)])
|
||||
} label: {
|
||||
Label("Share 1-time link", systemImage: "square.and.arrow.up")
|
||||
settingsRow("square.and.arrow.up", color: secondaryColor) {
|
||||
Text("Share 1-time link")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func oneTimeLinkLearnMoreButton() -> some View {
|
||||
private func oneTimeLinkLearnMoreButton(_ secondaryColor: Color) -> some View {
|
||||
NavigationLink {
|
||||
AddContactLearnMore(showTitle: false)
|
||||
.navigationTitle("One-time invitation link")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Learn more", systemImage: "info.circle")
|
||||
settingsRow("info.circle", color: secondaryColor) {
|
||||
Text("Learn more")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -245,7 +245,7 @@ struct ServersSummaryView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func smpServersListView(
|
||||
@ViewBuilder 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
|
||||
}
|
||||
return Section {
|
||||
Section {
|
||||
ForEach(sortedServers) { server in
|
||||
smpServerView(server, statsStartedAt)
|
||||
}
|
||||
|
@ -318,14 +318,14 @@ struct ServersSummaryView: View {
|
|||
return onionHosts == .require ? .indigo : .accentColor
|
||||
}
|
||||
|
||||
private func xftpServersListView(
|
||||
@ViewBuilder private func xftpServersListView(
|
||||
_ servers: [XFTPServerSummary],
|
||||
_ statsStartedAt: Date,
|
||||
_ header: LocalizedStringKey? = nil,
|
||||
_ footer: LocalizedStringKey? = nil
|
||||
) -> some View {
|
||||
let sortedServers = servers.sorted { serverAddress($0.xftpServer) < serverAddress($1.xftpServer) }
|
||||
return Section {
|
||||
Section {
|
||||
ForEach(sortedServers) { server in
|
||||
xftpServerView(server, statsStartedAt)
|
||||
}
|
||||
|
|
|
@ -138,7 +138,7 @@ struct TagListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func radioButton(selected: Bool) -> some View {
|
||||
@ViewBuilder private func radioButton(selected: Bool) -> some View {
|
||||
Image(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel))
|
||||
|
|
|
@ -140,9 +140,9 @@ struct ContactListNavLink: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func previewTitle(_ contact: Contact, titleColor: Color) -> some View {
|
||||
@ViewBuilder private func previewTitle(_ contact: Contact, titleColor: Color) -> some View {
|
||||
let t = Text(chat.chatInfo.chatViewName).foregroundColor(titleColor)
|
||||
return (
|
||||
(
|
||||
contact.verified == true
|
||||
? verifiedIcon + t
|
||||
: t
|
||||
|
|
|
@ -173,7 +173,7 @@ struct DatabaseEncryptionView: View {
|
|||
}
|
||||
return true
|
||||
} catch let error {
|
||||
if case .errorDatabase(.errorExport(.errorNotADatabase)) = error as? ChatError {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
await operationEnded(.currentPassphraseError)
|
||||
} else {
|
||||
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
||||
|
|
|
@ -28,7 +28,7 @@ struct DatabaseErrorView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func databaseErrorView() -> some View {
|
||||
@ViewBuilder private func databaseErrorView() -> some View {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
switch status {
|
||||
case let .errorNotADatabase(dbFile):
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
@inline(__always)
|
||||
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
|
||||
if condition {
|
||||
transform(self)
|
||||
|
@ -37,9 +36,9 @@ struct PrivacyBlur: ViewModifier {
|
|||
.overlay {
|
||||
if (blurred && enabled) {
|
||||
Color.clear.contentShape(Rectangle())
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
.onTapGesture {
|
||||
blurred = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .chatViewWillBeginScrolling)) { _ in
|
||||
|
|
|
@ -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.chatState.clear()
|
||||
ItemsModel.shared.chatItemsChangesListener.cleared()
|
||||
m.updateChats([])
|
||||
m.users = []
|
||||
_ = kcAppPassword.set(password)
|
||||
|
|
|
@ -28,7 +28,7 @@ struct PasscodeEntry: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func passwordView() -> some View {
|
||||
@ViewBuilder private func passwordView() -> some View {
|
||||
Text(
|
||||
password == ""
|
||||
? " "
|
||||
|
|
|
@ -520,15 +520,15 @@ struct MigrateFromDevice: View {
|
|||
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
|
||||
await MainActor.run {
|
||||
switch msg {
|
||||
case let .result(.sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize)):
|
||||
case let .sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize):
|
||||
if case let .uploadProgress(uploaded, total, _, _, _) = migrationState, uploaded != total {
|
||||
migrationState = .uploadProgress(uploadedBytes: sentSize, totalBytes: totalSize, fileId: fileTransferMeta.fileId, archivePath: archivePath, ctrl: ctrl)
|
||||
}
|
||||
case .result(.sndFileRedirectStartXFTP):
|
||||
case .sndFileRedirectStartXFTP:
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
migrationState = .linkCreation
|
||||
}
|
||||
case let .result(.sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs)):
|
||||
case let .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 .result(.sndFileError):
|
||||
case .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 .errorDatabase(.errorOpen(.errorNotADatabase)) = error as? ChatError {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse {
|
||||
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: (APIResult<ChatEvent>) async -> Void
|
||||
let processReceivedMsg: (ChatResponse) async -> Void
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (APIResult<ChatEvent>) async -> Void) {
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatResponse) 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: APIResult<ChatEvent> = await chatRecvMsg(ctrl) {
|
||||
if let msg = await chatRecvMsg(ctrl) {
|
||||
Task {
|
||||
await TerminalItems.shared.addResult(msg)
|
||||
await TerminalItems.shared.add(.resp(.now, msg))
|
||||
}
|
||||
logger.debug("processReceivedMsg: \(msg.responseType)")
|
||||
await processReceivedMsg(msg)
|
||||
|
|
|
@ -496,10 +496,10 @@ struct MigrateToDevice: View {
|
|||
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
|
||||
await MainActor.run {
|
||||
switch msg {
|
||||
case let .result(.rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer)):
|
||||
case let .rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer):
|
||||
migrationState = .downloadProgress(downloadedBytes: receivedSize, totalBytes: totalSize, fileId: rcvFileTransfer.fileId, link: link, archivePath: archivePath, ctrl: ctrl)
|
||||
MigrationToDeviceState.save(.downloadProgress(link: link, archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
|
||||
case .result(.rcvStandaloneFileComplete):
|
||||
case .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 .result(.rcvFileError):
|
||||
case .rcvFileError:
|
||||
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
|
||||
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||
case .error(.error(.noRcvFileUser)):
|
||||
case .chatError(_, .error(.noRcvFileUser)):
|
||||
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
|
||||
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||
default:
|
||||
|
@ -751,11 +751,11 @@ private func progressView() -> some View {
|
|||
private class MigrationChatReceiver {
|
||||
let ctrl: chat_ctrl
|
||||
let databaseUrl: URL
|
||||
let processReceivedMsg: (APIResult<ChatEvent>) async -> Void
|
||||
let processReceivedMsg: (ChatResponse) async -> Void
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (APIResult<ChatEvent>) async -> Void) {
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatResponse) async -> Void) {
|
||||
self.ctrl = ctrl
|
||||
self.databaseUrl = databaseUrl
|
||||
self.processReceivedMsg = processReceivedMsg
|
||||
|
@ -772,7 +772,7 @@ private class MigrationChatReceiver {
|
|||
// TODO use function that has timeout
|
||||
if let msg = await chatRecvMsg(ctrl) {
|
||||
Task {
|
||||
await TerminalItems.shared.addResult(msg)
|
||||
await TerminalItems.shared.add(.resp(.now, msg))
|
||||
}
|
||||
logger.debug("processReceivedMsg: \(msg.responseType)")
|
||||
await processReceivedMsg(msg)
|
||||
|
|
|
@ -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: CreatedConnLink?
|
||||
@State private var groupLink: String?
|
||||
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -85,7 +85,7 @@ struct NewChatSheet: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func viewBody(_ showArchive: Bool) -> some View {
|
||||
@ViewBuilder private func viewBody(_ showArchive: Bool) -> some View {
|
||||
List {
|
||||
HStack {
|
||||
ContactsListSearchBar(
|
||||
|
@ -258,7 +258,7 @@ struct ContactsList: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func noResultSection(text: String) -> some View {
|
||||
@ViewBuilder private func noResultSection(text: String) -> some View {
|
||||
Section {
|
||||
Text(text)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
|
|
|
@ -81,8 +81,7 @@ struct NewChatView: View {
|
|||
@State var selection: NewChatOption
|
||||
@State var showQRCodeScanner = false
|
||||
@State private var invitationUsed: Bool = false
|
||||
@State private var connLinkInvitation: CreatedConnLink = CreatedConnLink(connFullLink: "", connShortLink: nil)
|
||||
@State private var showShortLink = true
|
||||
@State private var connReqInvitation: String = ""
|
||||
@State private var creatingConnReq = false
|
||||
@State var choosingProfile = false
|
||||
@State private var pastedLink: String = ""
|
||||
|
@ -175,12 +174,11 @@ struct NewChatView: View {
|
|||
|
||||
private func prepareAndInviteView() -> some View {
|
||||
ZStack { // ZStack is needed for views to not make transitions between each other
|
||||
if connLinkInvitation.connFullLink != "" {
|
||||
if connReqInvitation != "" {
|
||||
InviteView(
|
||||
invitationUsed: $invitationUsed,
|
||||
contactConnection: $contactConnection,
|
||||
connLinkInvitation: $connLinkInvitation,
|
||||
showShortLink: $showShortLink,
|
||||
connReqInvitation: $connReqInvitation,
|
||||
choosingProfile: $choosingProfile
|
||||
)
|
||||
} else if creatingConnReq {
|
||||
|
@ -192,16 +190,16 @@ struct NewChatView: View {
|
|||
}
|
||||
|
||||
private func createInvitation() {
|
||||
if connLinkInvitation.connFullLink == "" && contactConnection == nil && !creatingConnReq {
|
||||
if connReqInvitation == "" && contactConnection == nil && !creatingConnReq {
|
||||
creatingConnReq = true
|
||||
Task {
|
||||
_ = try? await Task.sleep(nanoseconds: 250_000000)
|
||||
let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get())
|
||||
if let (connLink, pcc) = r {
|
||||
if let (connReq, pcc) = r {
|
||||
await MainActor.run {
|
||||
m.updateContactConnection(pcc)
|
||||
m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false)
|
||||
connLinkInvitation = connLink
|
||||
connReqInvitation = connReq
|
||||
contactConnection = pcc
|
||||
}
|
||||
} else {
|
||||
|
@ -245,8 +243,7 @@ private struct InviteView: View {
|
|||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var invitationUsed: Bool
|
||||
@Binding var contactConnection: PendingContactConnection?
|
||||
@Binding var connLinkInvitation: CreatedConnLink
|
||||
@Binding var showShortLink: Bool
|
||||
@Binding var connReqInvitation: String
|
||||
@Binding var choosingProfile: Bool
|
||||
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
|
@ -264,7 +261,7 @@ private struct InviteView: View {
|
|||
NavigationLink {
|
||||
ActiveProfilePicker(
|
||||
contactConnection: $contactConnection,
|
||||
connLinkInvitation: $connLinkInvitation,
|
||||
connReqInvitation: $connReqInvitation,
|
||||
incognitoEnabled: $incognitoDefault,
|
||||
choosingProfile: $choosingProfile,
|
||||
selectedProfile: selectedProfile
|
||||
|
@ -299,7 +296,7 @@ private struct InviteView: View {
|
|||
|
||||
private func shareLinkView() -> some View {
|
||||
HStack {
|
||||
let link = connLinkInvitation.simplexChatUri(short: showShortLink)
|
||||
let link = simplexChatLink(connReqInvitation)
|
||||
linkTextView(link)
|
||||
Button {
|
||||
showShareSheet(items: [link])
|
||||
|
@ -313,9 +310,9 @@ private struct InviteView: View {
|
|||
}
|
||||
|
||||
private func qrCodeView() -> some View {
|
||||
Section {
|
||||
SimpleXCreatedLinkQRCode(link: connLinkInvitation, short: $showShortLink, onShare: setInvitationUsed)
|
||||
.id("simplex-qrcode-view-for-\(connLinkInvitation.simplexChatUri(short: showShortLink))")
|
||||
Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) {
|
||||
SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed)
|
||||
.id("simplex-qrcode-view-for-\(connReqInvitation)")
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
|
@ -325,8 +322,6 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -348,7 +343,7 @@ private struct ActiveProfilePicker: View {
|
|||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var contactConnection: PendingContactConnection?
|
||||
@Binding var connLinkInvitation: CreatedConnLink
|
||||
@Binding var connReqInvitation: String
|
||||
@Binding var incognitoEnabled: Bool
|
||||
@Binding var choosingProfile: Bool
|
||||
@State private var alert: SomeAlert?
|
||||
|
@ -420,11 +415,12 @@ 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
|
||||
connLinkInvitation = conn.connLinkInv ?? CreatedConnLink(connFullLink: "", connShortLink: nil)
|
||||
connReqInvitation = conn.connReqInv ?? ""
|
||||
incognitoEnabled = false
|
||||
chatModel.updateContactConnection(conn)
|
||||
}
|
||||
|
@ -506,7 +502,7 @@ private struct ActiveProfilePicker: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func profilerPickerUserOption(_ user: User) -> some View {
|
||||
@ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View {
|
||||
Button {
|
||||
if selectedProfile == user && incognitoEnabled {
|
||||
incognitoEnabled = false
|
||||
|
@ -840,25 +836,23 @@ func sharedProfileInfo(_ incognito: Bool) -> Text {
|
|||
}
|
||||
|
||||
enum PlanAndConnectAlert: Identifiable {
|
||||
case ownInvitationLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case invitationLinkConnecting(connectionLink: CreatedConnLink)
|
||||
case ownContactAddressConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case contactAddressConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConnecting(connectionLink: CreatedConnLink, groupInfo: GroupInfo?)
|
||||
case error(shortOrFullLink: String, alert: Alert)
|
||||
case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case invitationLinkConnecting(connectionLink: String)
|
||||
case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink.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)"
|
||||
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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -941,22 +935,21 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (
|
|||
dismissButton: .default(Text("OK")) { cleanup?() }
|
||||
)
|
||||
}
|
||||
case let .error(_, alert): return alert
|
||||
}
|
||||
}
|
||||
|
||||
enum PlanAndConnectActionSheet: Identifiable {
|
||||
case askCurrentOrIncognitoProfile(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
|
||||
case askCurrentOrIncognitoProfileDestructive(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
|
||||
case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
|
||||
case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
|
||||
case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact)
|
||||
case ownGroupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
|
||||
case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink.connFullLink)"
|
||||
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink.connFullLink)"
|
||||
case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
|
||||
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)"
|
||||
case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)"
|
||||
case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink.connFullLink)"
|
||||
case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1015,7 +1008,7 @@ func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool
|
|||
}
|
||||
|
||||
func planAndConnect(
|
||||
_ shortOrFullLink: String,
|
||||
_ connectionLink: String,
|
||||
showAlert: @escaping (PlanAndConnectAlert) -> Void,
|
||||
showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
|
||||
dismiss: Bool,
|
||||
|
@ -1025,8 +1018,8 @@ func planAndConnect(
|
|||
filterKnownGroup: ((GroupInfo) -> Void)? = nil
|
||||
) {
|
||||
Task {
|
||||
let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink)
|
||||
if let (connectionLink, connectionPlan) = result {
|
||||
do {
|
||||
let connectionPlan = try await apiConnectPlan(connReq: connectionLink)
|
||||
switch connectionPlan {
|
||||
case let .invitationLink(ilp):
|
||||
switch ilp {
|
||||
|
@ -1035,40 +1028,32 @@ func planAndConnect(
|
|||
if let incognito = incognito {
|
||||
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
|
||||
} else {
|
||||
await MainActor.run {
|
||||
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
|
||||
}
|
||||
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
|
||||
}
|
||||
case .ownLink:
|
||||
logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let incognito = incognito {
|
||||
showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
|
||||
}
|
||||
if let incognito = incognito {
|
||||
showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
|
||||
}
|
||||
case let .connecting(contact_):
|
||||
logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let contact = contact_ {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
|
||||
}
|
||||
} else {
|
||||
showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
|
||||
}
|
||||
}
|
||||
case let .known(contact):
|
||||
logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let contact = contact_ {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
|
||||
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
|
||||
}
|
||||
} else {
|
||||
showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
|
||||
}
|
||||
case let .known(contact):
|
||||
logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
|
||||
}
|
||||
}
|
||||
case let .contactAddress(cap):
|
||||
|
@ -1078,109 +1063,83 @@ func planAndConnect(
|
|||
if let incognito = incognito {
|
||||
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
|
||||
} else {
|
||||
await MainActor.run {
|
||||
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
|
||||
}
|
||||
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
|
||||
}
|
||||
case .ownLink:
|
||||
logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let incognito = incognito {
|
||||
showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
|
||||
}
|
||||
if let incognito = incognito {
|
||||
showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
|
||||
}
|
||||
case .connectingConfirmReconnect:
|
||||
logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let incognito = incognito {
|
||||
showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
|
||||
}
|
||||
if let incognito = incognito {
|
||||
showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
|
||||
}
|
||||
case let .connectingProhibit(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
|
||||
}
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
|
||||
}
|
||||
case let .known(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
|
||||
}
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
|
||||
}
|
||||
case let .contactViaAddress(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")")
|
||||
if let incognito = incognito {
|
||||
connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
|
||||
} else {
|
||||
await MainActor.run {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact))
|
||||
}
|
||||
showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact))
|
||||
}
|
||||
}
|
||||
case let .groupLink(glp):
|
||||
switch glp {
|
||||
case .ok:
|
||||
await MainActor.run {
|
||||
if let incognito = incognito {
|
||||
showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
|
||||
}
|
||||
if let incognito = incognito {
|
||||
showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
|
||||
}
|
||||
case let .ownLink(groupInfo):
|
||||
logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
}
|
||||
showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
}
|
||||
showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
|
||||
case .connectingConfirmReconnect:
|
||||
logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let incognito = incognito {
|
||||
showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
|
||||
}
|
||||
if let incognito = incognito {
|
||||
showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
|
||||
}
|
||||
case let .connectingProhibit(groupInfo_):
|
||||
logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
|
||||
}
|
||||
showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
|
||||
case let .known(groupInfo):
|
||||
logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
} else {
|
||||
openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
|
||||
}
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
} else {
|
||||
openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
|
||||
}
|
||||
}
|
||||
case let .error(chatError):
|
||||
logger.debug("planAndConnect, .error \(chatErrorString(chatError))")
|
||||
if let incognito = incognito {
|
||||
connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
|
||||
} else {
|
||||
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
|
||||
}
|
||||
}
|
||||
} else if let alert {
|
||||
await MainActor.run {
|
||||
showAlert(.error(shortOrFullLink: shortOrFullLink, alert: alert))
|
||||
} 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1202,22 +1161,22 @@ private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incogn
|
|||
}
|
||||
|
||||
private func connectViaLink(
|
||||
_ connectionLink: CreatedConnLink,
|
||||
_ connectionLink: String,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
dismiss: Bool,
|
||||
incognito: Bool,
|
||||
cleanup: (() -> Void)?
|
||||
) {
|
||||
Task {
|
||||
if let (connReqType, pcc) = await apiConnect(incognito: incognito, connLink: connectionLink) {
|
||||
if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) {
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateContactConnection(pcc)
|
||||
}
|
||||
let crt: ConnReqType
|
||||
crt = if let plan = connectionPlan {
|
||||
planToConnReqType(plan) ?? connReqType
|
||||
if let plan = connectionPlan {
|
||||
crt = planToConnReqType(plan)
|
||||
} else {
|
||||
connReqType
|
||||
crt = connReqType
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if dismiss {
|
||||
|
@ -1240,35 +1199,43 @@ private func connectViaLink(
|
|||
}
|
||||
|
||||
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||
let m = ChatModel.shared
|
||||
if let c = m.getContactChat(contact.contactId) {
|
||||
if dismiss {
|
||||
dismissAllSheets(animated: true) {
|
||||
ItemsModel.shared.loadOpenChat(c.id) {
|
||||
showAlreadyExistsAlert?()
|
||||
Task {
|
||||
let m = ChatModel.shared
|
||||
if let c = m.getContactChat(contact.contactId) {
|
||||
DispatchQueue.main.async {
|
||||
if dismiss {
|
||||
dismissAllSheets(animated: true) {
|
||||
ItemsModel.shared.loadOpenChat(c.id) {
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ItemsModel.shared.loadOpenChat(c.id) {
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ItemsModel.shared.loadOpenChat(c.id) {
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||
let m = ChatModel.shared
|
||||
if let g = m.getGroupChat(groupInfo.groupId) {
|
||||
if dismiss {
|
||||
dismissAllSheets(animated: true) {
|
||||
ItemsModel.shared.loadOpenChat(g.id) {
|
||||
showAlreadyExistsAlert?()
|
||||
Task {
|
||||
let m = ChatModel.shared
|
||||
if let g = m.getGroupChat(groupInfo.groupId) {
|
||||
DispatchQueue.main.async {
|
||||
if dismiss {
|
||||
dismissAllSheets(animated: true) {
|
||||
ItemsModel.shared.loadOpenChat(g.id) {
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ItemsModel.shared.loadOpenChat(g.id) {
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ItemsModel.shared.loadOpenChat(g.id) {
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1306,12 +1273,11 @@ enum ConnReqType: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType? {
|
||||
private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType {
|
||||
switch connectionPlan {
|
||||
case .invitationLink: .invitation
|
||||
case .contactAddress: .contact
|
||||
case .groupLink: .groupLink
|
||||
case .error: nil
|
||||
case .invitationLink: return .invitation
|
||||
case .contactAddress: return .contact
|
||||
case .groupLink: return .groupLink
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import SwiftUI
|
||||
import CoreImage.CIFilterBuiltins
|
||||
import SimpleXChat
|
||||
|
||||
struct MutableQRCode: View {
|
||||
@Binding var uri: String
|
||||
|
@ -21,16 +20,6 @@ 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
|
||||
|
@ -42,6 +31,12 @@ 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
|
||||
|
|
|
@ -43,23 +43,26 @@ struct OnboardingButtonStyle: ButtonStyle {
|
|||
}
|
||||
}
|
||||
|
||||
private enum OnboardingConditionsViewSheet: Identifiable {
|
||||
private enum ChooseServerOperatorsSheet: Identifiable {
|
||||
case showInfo
|
||||
case showConditions
|
||||
case configureOperators
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .showInfo: return "showInfo"
|
||||
case .showConditions: return "showConditions"
|
||||
case .configureOperators: return "configureOperators"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingConditionsView: View {
|
||||
struct ChooseServerOperators: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var onboarding: Bool
|
||||
@State private var serverOperators: [ServerOperator] = []
|
||||
@State private var selectedOperatorIds = Set<Int64>()
|
||||
@State private var sheetItem: OnboardingConditionsViewSheet? = nil
|
||||
@State private var sheetItem: ChooseServerOperatorsSheet? = nil
|
||||
@State private var notificationsModeNavLinkActive = false
|
||||
@State private var justOpened = true
|
||||
|
||||
|
@ -67,47 +70,83 @@ struct OnboardingConditionsView: View {
|
|||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
let v = ScrollView {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Conditions of use")
|
||||
let title = Text("Server operators")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 25)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Private chats, groups and your contacts are not accessible to server operators.")
|
||||
.lineSpacing(2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("""
|
||||
By using SimpleX Chat you agree to:
|
||||
- send only legal content in public groups.
|
||||
- respect other users – no spam.
|
||||
""")
|
||||
.lineSpacing(2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Button("Privacy policy and conditions of use.") {
|
||||
sheetItem = .showConditions
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if onboarding {
|
||||
title.padding(.top, 25)
|
||||
} else {
|
||||
title
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
|
||||
infoText()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 12) {
|
||||
acceptConditionsButton()
|
||||
|
||||
Button("Configure server operators") {
|
||||
sheetItem = .configureOperators
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
}
|
||||
.frame(minHeight: 40)
|
||||
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)
|
||||
}
|
||||
}
|
||||
.padding(25)
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
.onAppear {
|
||||
|
@ -119,28 +158,127 @@ struct OnboardingConditionsView: View {
|
|||
}
|
||||
.sheet(item: $sheetItem) { item in
|
||||
switch item {
|
||||
case .showInfo:
|
||||
ChooseServerOperatorsInfoView()
|
||||
case .showConditions:
|
||||
SimpleConditionsView()
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
case .configureOperators:
|
||||
ChooseServerOperators(serverOperators: serverOperators, selectedOperatorIds: $selectedOperatorIds)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
if #available(iOS 16.4, *) {
|
||||
v.scrollBounceBehavior(.basedOnSize)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.navigationBarHidden(true) // necessary on iOS 15
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
private func continueToNextStep() {
|
||||
onboardingStageDefault.set(.step4_SetNotificationsMode)
|
||||
notificationsModeNavLinkActive = true
|
||||
if onboarding {
|
||||
onboardingStageDefault.set(.step4_SetNotificationsMode)
|
||||
notificationsModeNavLinkActive = true
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View {
|
||||
|
@ -163,6 +301,25 @@ struct OnboardingConditionsView: 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 {
|
||||
|
@ -196,10 +353,9 @@ struct OnboardingConditionsView: View {
|
|||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Accept")
|
||||
Text("Accept conditions")
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
|
||||
.disabled(selectedOperatorIds.isEmpty)
|
||||
.buttonStyle(OnboardingButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,126 +390,6 @@ struct OnboardingConditionsView: 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<Int64>
|
||||
@State private var sheetItem: ChooseServerOperatorsSheet? = nil
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Server operators")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 25)
|
||||
|
||||
infoText()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Spacer()
|
||||
|
||||
ForEach(serverOperators) { srvOperator in
|
||||
operatorCheckView(srvOperator)
|
||||
}
|
||||
VStack {
|
||||
Text("SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.").padding(.bottom, 8)
|
||||
Text("You can configure servers via settings.")
|
||||
}
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 8) {
|
||||
setOperatorsButton()
|
||||
onboardingButtonPlaceholder()
|
||||
}
|
||||
}
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
.sheet(item: $sheetItem) { item in
|
||||
switch item {
|
||||
case .showInfo:
|
||||
ChooseServerOperatorsInfoView()
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.padding(25)
|
||||
.interactiveDismissDisabled(selectedOperatorIds.isEmpty)
|
||||
}
|
||||
|
||||
private func infoText() -> some View {
|
||||
Button {
|
||||
sheetItem = .showInfo
|
||||
} label: {
|
||||
Label("How it helps privacy", systemImage: "info.circle")
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
|
||||
private func operatorCheckView(_ serverOperator: ServerOperator) -> some View {
|
||||
let checked = selectedOperatorIds.contains(serverOperator.operatorId)
|
||||
let icon = checked ? "checkmark.circle.fill" : "circle"
|
||||
let iconColor = checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
|
||||
return HStack(spacing: 10) {
|
||||
Image(serverOperator.largeLogo(colorScheme))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(height: 48)
|
||||
Spacer()
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 26, height: 26)
|
||||
.foregroundColor(iconColor)
|
||||
}
|
||||
.background(theme.colors.background)
|
||||
.padding()
|
||||
.clipShape(RoundedRectangle(cornerRadius: 18))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||
)
|
||||
.padding(.horizontal, 2)
|
||||
.onTapGesture {
|
||||
if checked {
|
||||
selectedOperatorIds.remove(serverOperator.operatorId)
|
||||
} else {
|
||||
selectedOperatorIds.insert(serverOperator.operatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setOperatorsButton() -> some View {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("OK")
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
|
||||
.disabled(selectedOperatorIds.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")!
|
||||
|
||||
struct ChooseServerOperatorsInfoView: View {
|
||||
|
@ -408,5 +444,5 @@ struct ChooseServerOperatorsInfoView: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
OnboardingConditionsView()
|
||||
ChooseServerOperators(onboarding: true)
|
||||
}
|
||||
|
|
|
@ -62,7 +62,8 @@ struct CreateProfile: View {
|
|||
.frame(height: 20)
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Your profile is stored on your device and only shared with your contacts.")
|
||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||
Text("The profile is only shared with your contacts.")
|
||||
}
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
@ -117,22 +118,25 @@ struct CreateFirstProfile: View {
|
|||
@State private var nextStepNavLinkActive = false
|
||||
|
||||
var body: some View {
|
||||
let v = VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
Text("Create profile")
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("Create your profile")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("Your profile is stored on your device and only shared with your contacts.")
|
||||
|
||||
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.")
|
||||
.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)
|
||||
|
@ -170,23 +174,12 @@ struct CreateFirstProfile: View {
|
|||
}
|
||||
}
|
||||
.onAppear() {
|
||||
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
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
@ -214,7 +207,7 @@ struct CreateFirstProfile: View {
|
|||
}
|
||||
|
||||
private func nextStepDestinationView() -> some View {
|
||||
OnboardingConditionsView()
|
||||
ChooseServerOperators(onboarding: true)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
@ -243,15 +236,15 @@ private func showCreateProfileAlert(
|
|||
_ error: Error
|
||||
) {
|
||||
let m = ChatModel.shared
|
||||
switch error as? ChatError {
|
||||
case .errorStore(.duplicateName),
|
||||
.error(.userExists):
|
||||
switch error as? ChatResponse {
|
||||
case .chatCmdError(_, .errorStore(.duplicateName)),
|
||||
.chatCmdError(_, .error(.userExists)):
|
||||
if m.currentUser == nil {
|
||||
AlertManager.shared.showAlert(duplicateUserAlert)
|
||||
} else {
|
||||
showAlert(.duplicateUserError)
|
||||
}
|
||||
case .error(.invalidDisplayName):
|
||||
case .chatCmdError(_, .error(.invalidDisplayName)):
|
||||
if m.currentUser == nil {
|
||||
AlertManager.shared.showAlert(invalidDisplayNameAlert)
|
||||
} else {
|
||||
|
|
|
@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View {
|
|||
Spacer()
|
||||
|
||||
if let userAddress = m.userAddress {
|
||||
SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: Binding.constant(false))
|
||||
SimpleXLinkQRCode(uri: userAddress.connReqContact)
|
||||
.frame(maxHeight: g.size.width)
|
||||
shareQRCodeButton(userAddress)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
@ -77,9 +77,9 @@ struct CreateSimpleXAddress: View {
|
|||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let connLinkContact = try await apiCreateUserAddress(short: false)
|
||||
let connReqContact = try await apiCreateUserAddress()
|
||||
DispatchQueue.main.async {
|
||||
m.userAddress = UserContactLink(connLinkContact: connLinkContact)
|
||||
m.userAddress = UserContactLink(connReqContact: connReqContact)
|
||||
}
|
||||
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.connLinkContact.simplexChatUri(short: false))])
|
||||
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
|
||||
} label: {
|
||||
Label("Share", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ struct SendAddressMailView: View {
|
|||
let messageBody = String(format: NSLocalizedString("""
|
||||
<p>Hi!</p>
|
||||
<p><a href="%@">Connect to me via SimpleX Chat</a></p>
|
||||
""", comment: "email text"), simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false)))
|
||||
""", comment: "email text"), simplexChatLink(userAddress.connReqContact))
|
||||
MailView(
|
||||
isShowing: self.$showMailView,
|
||||
result: $mailViewResult,
|
||||
|
|
|
@ -23,7 +23,7 @@ struct OnboardingView: View {
|
|||
case .step3_CreateSimpleXAddress: // deprecated
|
||||
CreateSimpleXAddress()
|
||||
case .step3_ChooseServerOperators:
|
||||
OnboardingConditionsView()
|
||||
ChooseServerOperators(onboarding: true)
|
||||
.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 // changed to simplified conditions
|
||||
case step3_ChooseServerOperators
|
||||
case step4_SetNotificationsMode
|
||||
case onboardingComplete
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ struct SetNotificationsMode: View {
|
|||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
let v = ScrollView {
|
||||
ScrollView {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("Push notifications")
|
||||
.font(.largeTitle)
|
||||
|
@ -57,17 +57,11 @@ struct SetNotificationsMode: View {
|
|||
.padding(25)
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
if #available(iOS 16.4, *) {
|
||||
v.scrollBounceBehavior(.basedOnSize)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.sheet(isPresented: $showInfo) {
|
||||
NotificationsInfoView()
|
||||
}
|
||||
.navigationBarHidden(true) // necessary on iOS 15
|
||||
}
|
||||
|
||||
private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) {
|
||||
|
|
|
@ -18,7 +18,7 @@ struct SimpleXInfo: View {
|
|||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
let v = ScrollView {
|
||||
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,9 +66,6 @@ struct SimpleXInfo: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 75)
|
||||
.padding(.bottom, 25)
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
|
@ -91,17 +88,14 @@ struct SimpleXInfo: View {
|
|||
createProfileNavLinkActive: $createProfileNavLinkActive
|
||||
)
|
||||
}
|
||||
if #available(iOS 16.4, *) {
|
||||
v.scrollBounceBehavior(.basedOnSize)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
setLastVersionDefault()
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationBarHidden(true) // necessary on iOS 15
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 75)
|
||||
.padding(.bottom, 25)
|
||||
}
|
||||
|
||||
private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
|
||||
|
@ -135,7 +129,6 @@ struct SimpleXInfo: View {
|
|||
|
||||
NavigationLink(isActive: $createProfileNavLinkActive) {
|
||||
CreateFirstProfile()
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
|
|
|
@ -594,6 +594,8 @@ 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)
|
||||
|
@ -604,7 +606,16 @@ fileprivate struct NewOperatorsView: View {
|
|||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(10)
|
||||
HStack {
|
||||
Text("Enable Flux in Network & servers settings for better metadata privacy.")
|
||||
Button("Enable Flux") {
|
||||
showOperatorsSheet = true
|
||||
}
|
||||
Text("for better metadata privacy.")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showOperatorsSheet) {
|
||||
NavigationView {
|
||||
ChooseServerOperators(onboarding: false)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -456,12 +456,12 @@ struct ConnectDesktopView: View {
|
|||
}
|
||||
} catch let e {
|
||||
await MainActor.run {
|
||||
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
|
||||
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
|
||||
default: errorAlert(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: APIResult<ChatResponse2> = APIResult.error(ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||
let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||
Task {
|
||||
await TerminalItems.shared.addCommand(.now, .string(composeState.message), resp)
|
||||
await TerminalItems.shared.addCommand(.now, cmd, resp)
|
||||
}
|
||||
} else {
|
||||
let cmd = composeState.message
|
||||
DispatchQueue.global().async {
|
||||
Task {
|
||||
await MainActor.run { composeState.inProgress = true }
|
||||
await sendTerminalCmd(cmd)
|
||||
await MainActor.run { composeState.inProgress = false }
|
||||
composeState.inProgress = true
|
||||
_ = await chatSendCmd(cmd)
|
||||
composeState.inProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,38 +164,12 @@ struct TerminalView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func sendTerminalCmd(_ cmd: String) async {
|
||||
let start: Date = .now
|
||||
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
|
||||
let d = sendSimpleXCmdStr(cmd)
|
||||
Task {
|
||||
guard let d else {
|
||||
await TerminalItems.shared.addCommand(start, ChatCommand.string(cmd), APIResult<ChatResponse2>.error(.invalidJSON(json: nil)))
|
||||
return
|
||||
}
|
||||
let r0: APIResult<ChatResponse0> = decodeAPIResult(d)
|
||||
guard case .invalid = r0 else {
|
||||
await TerminalItems.shared.addCommand(start, .string(cmd), r0)
|
||||
return
|
||||
}
|
||||
let r1: APIResult<ChatResponse1> = decodeAPIResult(d)
|
||||
guard case .invalid = r1 else {
|
||||
await TerminalItems.shared.addCommand(start, .string(cmd), r1)
|
||||
return
|
||||
}
|
||||
let r2: APIResult<ChatResponse2> = decodeAPIResult(d)
|
||||
await TerminalItems.shared.addCommand(start, .string(cmd), r2)
|
||||
}
|
||||
cont.resume(returning: ())
|
||||
}
|
||||
}
|
||||
|
||||
struct TerminalView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.terminalItems = [
|
||||
.err(.now, APIResult<ChatResponse2>.invalid(type: "contactSubscribed", json: "{}".data(using: .utf8)!).unexpected),
|
||||
.err(.now, APIResult<ChatResponse2>.invalid(type: "newChatItems", json: "{}".data(using: .utf8)!).unexpected)
|
||||
.resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")),
|
||||
.resp(.now, ChatResponse.response(type: "newChatItems", json: "{}"))
|
||||
]
|
||||
return NavigationView {
|
||||
TerminalView()
|
||||
|
|
|
@ -38,6 +38,7 @@ 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) }
|
||||
|
@ -77,6 +78,7 @@ 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)
|
||||
|
|
|
@ -209,16 +209,11 @@ struct AdvancedNetworkSettings: View {
|
|||
}
|
||||
|
||||
Section {
|
||||
Picker("Use web port", selection: $netCfg.smpWebPortServers) {
|
||||
ForEach(SMPWebPortServers.allCases, id: \.self) { Text($0.text) }
|
||||
}
|
||||
.frame(height: 36)
|
||||
Toggle("Use web port", isOn: $netCfg.smpWebPort)
|
||||
} header: {
|
||||
Text("TCP port for messaging")
|
||||
} footer: {
|
||||
netCfg.smpWebPortServers == .preset
|
||||
? Text("Use TCP port 443 for preset servers only.")
|
||||
: Text("Use TCP port \(netCfg.smpWebPortServers == .all ? "443" : "5223") when no port is specified.")
|
||||
Text("Use TCP port \(netCfg.smpWebPort ? "443" : "5223") when no port is specified.")
|
||||
}
|
||||
|
||||
Section("TCP connection") {
|
||||
|
|
|
@ -38,9 +38,9 @@ struct OperatorView: View {
|
|||
.allowsHitTesting(!testing)
|
||||
}
|
||||
|
||||
private func operatorView() -> some View {
|
||||
@ViewBuilder private func operatorView() -> some View {
|
||||
let duplicateHosts = findDuplicateHosts(serverErrors)
|
||||
return VStack {
|
||||
VStack {
|
||||
List {
|
||||
Section {
|
||||
infoViewLink()
|
||||
|
@ -500,14 +500,14 @@ struct SingleOperatorUsageConditionsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func acceptConditionsButton() -> some View {
|
||||
@ViewBuilder private func acceptConditionsButton() -> some View {
|
||||
let operatorIds = ChatModel.shared.conditions.serverOperators
|
||||
.filter {
|
||||
$0.operatorId == userServers[operatorIndex].operator_.operatorId || // Opened operator
|
||||
($0.enabled && !$0.conditionsAcceptance.conditionsAccepted) // Other enabled operators with conditions not accepted
|
||||
}
|
||||
.map { $0.operatorId }
|
||||
return Button {
|
||||
Button {
|
||||
acceptForOperators(operatorIds, operatorIndex)
|
||||
} label: {
|
||||
Text("Accept conditions")
|
||||
|
|
|
@ -38,9 +38,9 @@ struct YourServersView: View {
|
|||
.allowsHitTesting(!testing)
|
||||
}
|
||||
|
||||
private func yourServersView() -> some View {
|
||||
@ViewBuilder private func yourServersView() -> some View {
|
||||
let duplicateHosts = findDuplicateHosts(serverErrors)
|
||||
return List {
|
||||
List {
|
||||
if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty {
|
||||
Section {
|
||||
ForEach($userServers[operatorIndex].smpServers) { srv in
|
||||
|
|
|
@ -14,13 +14,12 @@ struct PrivacySettings: View {
|
|||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
|
||||
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
||||
@State private var chatListOpenLinks = privacyChatListOpenLinksDefault.get()
|
||||
@AppStorage(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()
|
||||
|
@ -76,6 +75,17 @@ 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)
|
||||
}
|
||||
|
@ -101,11 +111,6 @@ 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)
|
||||
|
|
|
@ -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,7 +99,6 @@ 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,
|
||||
|
@ -184,6 +183,8 @@ let connectViaLinkTabDefault = EnumDefault<ConnectViaLinkTab>(defaults: UserDefa
|
|||
|
||||
let privacySimplexLinkModeDefault = EnumDefault<SimpleXLinkMode>(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_SIMPLEX_LINK_MODE, withDefault: .description)
|
||||
|
||||
let privacyChatListOpenLinksDefault = EnumDefault<PrivacyChatListOpenLinksMode>(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_CHAT_LIST_OPEN_LINKS, withDefault: PrivacyChatListOpenLinksMode.ask)
|
||||
|
||||
let privacyLocalAuthModeDefault = EnumDefault<LAMode>(defaults: UserDefaults.standard, forKey: DEFAULT_LA_MODE, withDefault: .system)
|
||||
|
||||
let privacyDeliveryReceiptsSet = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET)
|
||||
|
@ -280,159 +281,159 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 {
|
||||
@ViewBuilder func settingsView() -> some View {
|
||||
let user = chatModel.currentUser
|
||||
List {
|
||||
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
|
||||
NavigationLink {
|
||||
AppearanceSettings()
|
||||
.navigationTitle("Appearance")
|
||||
NotificationsView()
|
||||
.navigationTitle("Notifications")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("sun.max", color: theme.colors.secondary) { Text("Appearance") }
|
||||
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") }
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Help").foregroundColor(theme.colors.secondary)) {
|
||||
if let user = user {
|
||||
|
||||
NavigationLink {
|
||||
ChatHelp(dismissSettingsSheet: dismiss)
|
||||
.navigationTitle("Welcome \(user.displayName)!")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) {
|
||||
chatDatabaseRow()
|
||||
NavigationLink {
|
||||
MigrateFromDevice(showProgressOnSettings: $showProgress)
|
||||
.toolbar {
|
||||
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text("Migrate device").font(.headline)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Help").foregroundColor(theme.colors.secondary)) {
|
||||
if let user = user {
|
||||
NavigationLink {
|
||||
ChatHelp(dismissSettingsSheet: dismiss)
|
||||
.navigationTitle("Welcome \(user.displayName)!")
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("questionmark", color: theme.colors.secondary) { Text("How to use it") }
|
||||
}
|
||||
}
|
||||
NavigationLink {
|
||||
WhatsNewView(viaSettings: true, updatedConditions: false)
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
settingsRow("plus", color: theme.colors.secondary) { Text("What's new") }
|
||||
}
|
||||
NavigationLink {
|
||||
SimpleXInfo(onboarding: false)
|
||||
.navigationBarTitle("", displayMode: .inline)
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("questionmark", color: theme.colors.secondary) { Text("How to use it") }
|
||||
settingsRow("info", color: theme.colors.secondary) { Text("About SimpleX Chat") }
|
||||
}
|
||||
}
|
||||
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)
|
||||
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 {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import MessageUI
|
||||
@preconcurrency import SimpleXChat
|
||||
import SimpleXChat
|
||||
|
||||
struct UserAddressView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
|
@ -16,7 +16,6 @@ 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
|
||||
|
@ -136,8 +135,8 @@ struct UserAddressView: View {
|
|||
|
||||
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
|
||||
Section {
|
||||
SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: $showShortLink)
|
||||
.id("simplex-contact-address-qrcode-\(userAddress.connLinkContact.simplexChatUri(short: showShortLink))")
|
||||
SimpleXLinkQRCode(uri: userAddress.connReqContact)
|
||||
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
|
||||
shareQRCodeButton(userAddress)
|
||||
// if MFMailComposeViewController.canSendMail() {
|
||||
// shareViaEmailButton(userAddress)
|
||||
|
@ -154,7 +153,8 @@ struct UserAddressView: View {
|
|||
}
|
||||
addressSettingsButton(userAddress)
|
||||
} header: {
|
||||
ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink)
|
||||
Text("For social media")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
if aas.business {
|
||||
Text("Add your team members to the conversations.")
|
||||
|
@ -193,10 +193,9 @@ struct UserAddressView: View {
|
|||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
|
||||
let connLinkContact = try await apiCreateUserAddress(short: short)
|
||||
let connReqContact = try await apiCreateUserAddress()
|
||||
DispatchQueue.main.async {
|
||||
chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact)
|
||||
chatModel.userAddress = UserContactLink(connReqContact: connReqContact)
|
||||
alert = .shareOnCreate
|
||||
progressIndicator = false
|
||||
}
|
||||
|
@ -232,7 +231,7 @@ struct UserAddressView: View {
|
|||
|
||||
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
|
||||
Button {
|
||||
showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: showShortLink))])
|
||||
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
|
||||
} label: {
|
||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
||||
Text("Share address")
|
||||
|
@ -295,28 +294,6 @@ 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
|
||||
|
@ -565,7 +542,7 @@ private func saveAAS(_ aas: Binding<AutoAcceptState>, _ savedAAS: Binding<AutoAc
|
|||
struct UserAddressView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.userAddress = UserContactLink(connLinkContact: CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil))
|
||||
chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")
|
||||
|
||||
|
||||
return Group {
|
||||
|
|
|
@ -133,6 +133,7 @@ struct UserProfile: View {
|
|||
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func overlayButton(
|
||||
_ systemName: String,
|
||||
edge: Edge.Set,
|
||||
|
|
|
@ -221,11 +221,11 @@ struct UserProfilesView: View {
|
|||
!user.hidden ? nil : trimmedSearchTextOrPassword
|
||||
}
|
||||
|
||||
private func profileActionView(_ action: UserProfileAction) -> some View {
|
||||
@ViewBuilder 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) }
|
||||
return List {
|
||||
List {
|
||||
switch action {
|
||||
case let .deleteUser(user, delSMPQueues):
|
||||
actionHeader("Delete profile", user)
|
||||
|
|
|
@ -9,10 +9,6 @@
|
|||
<string>applinks:simplex.chat</string>
|
||||
<string>applinks:www.simplex.chat</string>
|
||||
<string>applinks:simplex.chat?mode=developer</string>
|
||||
<string>applinks:*.simplex.im</string>
|
||||
<string>applinks:*.simplex.im?mode=developer</string>
|
||||
<string>applinks:*.simplexonflux.com</string>
|
||||
<string>applinks:*.simplexonflux.com?mode=developer</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
|
|
|
@ -553,9 +553,8 @@
|
|||
<target state="translated">يمكنك أنت وجهة اتصالك إرسال رسائل صوتية.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve" approved="no">
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target state="translated">حسب ملف تعريف الدردشة (افتراضي) أو [حسب الاتصال] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve" approved="no">
|
||||
|
@ -583,9 +582,8 @@
|
|||
<target state="translated">إلغاء</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target state="translated">لا يمكن الوصول إلى سلسلة المفاتيح لحفظ كلمة مرور قاعدة البيانات</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot receive file" xml:space="preserve" approved="no">
|
||||
|
@ -603,9 +601,8 @@
|
|||
<target state="translated">تغيير عبارة مرور قاعدة البيانات؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change member role?" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Change member role?" xml:space="preserve">
|
||||
<source>Change member role?</source>
|
||||
<target state="translated">تغيير دور العضو؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address" xml:space="preserve" approved="no">
|
||||
|
@ -672,14 +669,12 @@
|
|||
<target state="translated">تحقق من عنوان الخادم وحاول مرة أخرى.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
<target state="translated">اختر الملف</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose from library" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Choose from library" xml:space="preserve">
|
||||
<source>Choose from library</source>
|
||||
<target state="translated">اختر من المكتبة</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Clear" xml:space="preserve" approved="no">
|
||||
|
@ -761,9 +756,8 @@
|
|||
<target state="translated">جارِ الاتصال بالخادم…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connecting server… (error: %@)" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Connecting server… (error: %@)" xml:space="preserve">
|
||||
<source>Connecting to server… (error: %@)</source>
|
||||
<target state="translated">الاتصال بالخادم... (الخطأ: %@)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection" xml:space="preserve" approved="no">
|
||||
|
@ -814,9 +808,8 @@
|
|||
<target state="translated">جهة الاتصال مخفية:</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact is connected" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Contact is connected" xml:space="preserve">
|
||||
<source>Contact is connected</source>
|
||||
<target state="translated">تم الاتصال</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact is not connected yet!" xml:space="preserve">
|
||||
|
@ -851,9 +844,8 @@
|
|||
<source>Core built at: %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Core version: v%@" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Core version: v%@" xml:space="preserve">
|
||||
<source>Core version: v%@</source>
|
||||
<target state="translated">الإصدار الأساسي: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create" xml:space="preserve" approved="no">
|
||||
|
@ -903,9 +895,8 @@
|
|||
<target state="translated">عبارة المرور الحالية…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve" approved="no">
|
||||
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
|
||||
<source>Currently maximum supported file size is %@.</source>
|
||||
<target state="translated">الحد الأقصى لحجم الملف المدعوم حاليًا هو %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Dark" xml:space="preserve" approved="no">
|
||||
|
@ -923,11 +914,9 @@
|
|||
<target state="translated">قاعدة البيانات مُعمّاة!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve" approved="no">
|
||||
<trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve">
|
||||
<source>Database encryption passphrase will be updated and stored in the keychain.
|
||||
</source>
|
||||
<target state="translated">سيتم تحديث عبارة المرور الخاصة بتشفير قاعدة البيانات وتخزينها في سلسلة المفاتيح.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve" approved="no">
|
||||
|
@ -962,9 +951,8 @@
|
|||
<target state="translated">عبارة مرور قاعدة البيانات وتصديرها</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve" approved="no">
|
||||
<trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve">
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<target state="translated">عبارة المرور الخاصة بقاعدة البيانات مختلفة عن تلك المحفوظة في سلسلة المفاتيح.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve" approved="no">
|
||||
|
@ -972,11 +960,9 @@
|
|||
<target state="translated">عبارة مرور قاعدة البيانات مطلوبة لفتح الدردشة.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve" approved="no">
|
||||
<trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve">
|
||||
<source>Database will be encrypted and the passphrase stored in the keychain.
|
||||
</source>
|
||||
<target state="translated">سيتم تشفير قاعدة البيانات وتخزين عبارة المرور في سلسلة المفاتيح.
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be encrypted. " xml:space="preserve" approved="no">
|
||||
|
@ -986,9 +972,8 @@
|
|||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database will be migrated when the app restarts" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Database will be migrated when the app restarts" xml:space="preserve">
|
||||
<source>Database will be migrated when the app restarts</source>
|
||||
<target state="translated">سيتم نقل قاعدة البيانات عند إعادة تشغيل التطبيق</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Decentralized" xml:space="preserve" approved="no">
|
||||
|
@ -1088,44 +1073,36 @@
|
|||
<target state="translated">حذف المجموعة؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete invitation" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete invitation" xml:space="preserve">
|
||||
<source>Delete invitation</source>
|
||||
<target state="translated">حذف الدعوة</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete link" xml:space="preserve">
|
||||
<source>Delete link</source>
|
||||
<target state="translated">حذف الرابط</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link?" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete link?" xml:space="preserve">
|
||||
<source>Delete link?</source>
|
||||
<target state="translated">حذف الرابط؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete message?" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete message?" xml:space="preserve">
|
||||
<source>Delete message?</source>
|
||||
<target state="translated">حذف الرسالة؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete messages" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete messages" xml:space="preserve">
|
||||
<source>Delete messages</source>
|
||||
<target state="translated">حذف الرسائل</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete messages after" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete messages after" xml:space="preserve">
|
||||
<source>Delete messages after</source>
|
||||
<target state="translated">حذف الرسائل بعد</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete old database" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete old database" xml:space="preserve">
|
||||
<source>Delete old database</source>
|
||||
<target state="translated">حذف قاعدة البيانات القديمة</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete old database?" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete old database?" xml:space="preserve">
|
||||
<source>Delete old database?</source>
|
||||
<target state="translated">حذف قاعدة البيانات القديمة؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete pending connection" xml:space="preserve">
|
||||
|
@ -1142,9 +1119,8 @@
|
|||
<target state="translated">حذف قائمة الانتظار</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete user profile?" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Delete user profile?" xml:space="preserve">
|
||||
<source>Delete user profile?</source>
|
||||
<target state="translated">حذف ملف تعريف المستخدم؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Description" xml:space="preserve" approved="no">
|
||||
|
@ -1152,9 +1128,8 @@
|
|||
<target state="translated">الوصف</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Develop" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Develop" xml:space="preserve">
|
||||
<source>Develop</source>
|
||||
<target state="translated">يطور</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve" approved="no">
|
||||
|
@ -1187,34 +1162,28 @@
|
|||
<target state="translated">رسائل مباشرة</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve" approved="no">
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<target state="translated">الرسائل المباشرة بين الأعضاء ممنوعة.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
<source>Disable SimpleX Lock</source>
|
||||
<target state="translated">تعطيل قفل SimpleX</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Disappearing messages" xml:space="preserve">
|
||||
<source>Disappearing messages</source>
|
||||
<target state="translated">الرسائل المختفية</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve" approved="no">
|
||||
<trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited in this chat.</source>
|
||||
<target state="translated">يُحظر اختفاء الرسائل في هذه الدردشة.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve" approved="no">
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<target state="translated">الرسائل المختفية ممنوعة.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
<source>Disconnect</source>
|
||||
<target state="translated">قطع الاتصال</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
|
@ -1225,14 +1194,12 @@
|
|||
<source>Display name:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve" approved="no">
|
||||
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
|
||||
<source>Do NOT use SimpleX for emergency calls.</source>
|
||||
<target state="translated">لا تستخدم SimpleX لإجراء مكالمات الطوارئ.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do it later" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Do it later" xml:space="preserve">
|
||||
<source>Do it later</source>
|
||||
<target state="translated">افعل ذلك لاحقا</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Duplicate display name!" xml:space="preserve" approved="no">
|
||||
|
@ -1285,93 +1252,76 @@
|
|||
<target state="translated">تفعيل الإشعارات دورية؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Encrypt" xml:space="preserve">
|
||||
<source>Encrypt</source>
|
||||
<target state="translated">التشفير</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt database?" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Encrypt database?" xml:space="preserve">
|
||||
<source>Encrypt database?</source>
|
||||
<target state="translated">تشفير قاعدة البيانات؟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<target state="translated">قاعدة بيانات مشفرة</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message or another event" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Encrypted message or another event" xml:space="preserve">
|
||||
<source>Encrypted message or another event</source>
|
||||
<target state="translated">رسالة مشفرة أو حدث آخر</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: database error" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Encrypted message: database error" xml:space="preserve">
|
||||
<source>Encrypted message: database error</source>
|
||||
<target state="translated">رسالة مشفرة: خطأ في قاعدة البيانات</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: keychain error" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Encrypted message: keychain error" xml:space="preserve">
|
||||
<source>Encrypted message: keychain error</source>
|
||||
<target state="translated">رسالة مشفرة: خطأ في سلسلة المفاتيح</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: no passphrase" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Encrypted message: no passphrase" xml:space="preserve">
|
||||
<source>Encrypted message: no passphrase</source>
|
||||
<target state="translated">الرسالة المشفرة: لا توجد عبارة مرور</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: unexpected error" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Encrypted message: unexpected error" xml:space="preserve">
|
||||
<source>Encrypted message: unexpected error</source>
|
||||
<target state="translated">رسالة مشفرة: خطأ غير متوقع</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter correct passphrase." xml:space="preserve" approved="no">
|
||||
<trans-unit id="Enter correct passphrase." xml:space="preserve">
|
||||
<source>Enter correct passphrase.</source>
|
||||
<target state="translated">أدخل عبارة المرور الصحيحة.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase…" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Enter passphrase…" xml:space="preserve">
|
||||
<source>Enter passphrase…</source>
|
||||
<target state="translated">أدخل عبارة المرور…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter server manually" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Enter server manually" xml:space="preserve">
|
||||
<source>Enter server manually</source>
|
||||
<target state="translated">أدخل الخادم يدوياً</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Error" xml:space="preserve">
|
||||
<source>Error</source>
|
||||
<target state="translated">خطأ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error accepting contact request" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Error accepting contact request" xml:space="preserve">
|
||||
<source>Error accepting contact request</source>
|
||||
<target state="translated">خطأ في قبول طلب الاتصال</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error accessing database file" xml:space="preserve">
|
||||
<source>Error accessing database file</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error adding member(s)" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Error adding member(s)" xml:space="preserve">
|
||||
<source>Error adding member(s)</source>
|
||||
<target state="translated">خطأ في إضافة عضو (أعضاء)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target state="translated">خطأ في تغيير العنوان</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target state="translated">خطأ في تغيير الدور المتغير</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing setting" xml:space="preserve" approved="no">
|
||||
<trans-unit id="Error changing setting" xml:space="preserve">
|
||||
<source>Error changing setting</source>
|
||||
<target state="translated">خطأ في تغيير الإعدادات</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating address" xml:space="preserve">
|
||||
|
@ -2826,8 +2776,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||
<source>The old database was not removed during the migration, it can be deleted.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and only shared with your contacts.</source>
|
||||
<trans-unit id="The profile is only shared with your contacts." xml:space="preserve">
|
||||
<source>The profile is only shared with your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The sender will NOT be notified" xml:space="preserve">
|
||||
|
@ -5439,324 +5389,6 @@ This is your own one-time link!</source>
|
|||
<source>Enable self-destruct passcode</source>
|
||||
<target state="translated">تفعيل رمز التدمير الذاتي</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't message member" xml:space="preserve" approved="no">
|
||||
<source>Can't message member</source>
|
||||
<target state="translated">لا يمكن الاتصال بالعضو</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Color chats with the new themes." xml:space="preserve" approved="no">
|
||||
<source>Color chats with the new themes.</source>
|
||||
<target state="translated">محادثات ملونة مع السمات الجديدة.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve" approved="no">
|
||||
<source>All chats will be removed from the list %@, and the list deleted.</source>
|
||||
<target state="translated">ستتم إزالة جميع الدردشات من القائمة %@، وسيتم حذف القائمة.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<target state="translated">البلغارية والفنلندية والتايلاندية والأوكرانية - شكرًا للمستخدمين و[Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve" approved="no">
|
||||
<source>Choose _Migrate from another device_ on the new device and scan QR code.</source>
|
||||
<target state="translated">اختر _الترحيل من جهاز آخر_ على الجهاز الجديد وامسح رمز الاستجابة السريعة.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve" approved="no">
|
||||
<source>Conditions will be accepted for the operator(s): **%@**.</source>
|
||||
<target state="translated">سيتم قبول شروط المشغل (المشغلين): **%@**.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted on: %@." xml:space="preserve" approved="no">
|
||||
<source>Conditions will be accepted on: %@.</source>
|
||||
<target state="translated">سيتم قبول الشروط على: %@.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirmed" xml:space="preserve" approved="no">
|
||||
<source>Confirmed</source>
|
||||
<target state="translated">تم التأكيد</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve" approved="no">
|
||||
<source>Connection is blocked by server operator:
|
||||
%@</source>
|
||||
<target state="translated">تم حظر الاتصال من قبل مشغل الخادم:
|
||||
%@</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't call member" xml:space="preserve" approved="no">
|
||||
<source>Can't call member</source>
|
||||
<target state="translated">لا يمكن الاتصال بالعضو</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat already exists" xml:space="preserve" approved="no">
|
||||
<source>Chat already exists</source>
|
||||
<target state="translated">الدردشة موجودة بالفعل</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Check messages every 20 min." xml:space="preserve" approved="no">
|
||||
<source>Check messages every 20 min.</source>
|
||||
<target state="translated">تحقق من الرسائل كل 20 دقيقة.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Check messages when allowed." xml:space="preserve" approved="no">
|
||||
<source>Check messages when allowed.</source>
|
||||
<target state="translated">تحقق من الرسائل عندما يُسمح بذلك.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve" approved="no">
|
||||
<source>Cannot forward message</source>
|
||||
<target state="translated">لا يمكن إعادة توجيه الرسالة</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat preferences were changed." xml:space="preserve" approved="no">
|
||||
<source>Chat preferences were changed.</source>
|
||||
<target state="translated">تم تغيير تفضيلات المحادثة.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve" approved="no">
|
||||
<source>Conditions are already accepted for these operator(s): **%@**.</source>
|
||||
<target state="translated">الشروط مقبولة بالفعل لهذا المشغل (المشغلين): **%@**.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for operator(s): **%@**." xml:space="preserve" approved="no">
|
||||
<source>Conditions will be accepted for operator(s): **%@**.</source>
|
||||
<target state="translated">سيتم قبول شروط المشغل (المشغلين): **%@**.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions accepted on: %@." xml:space="preserve" approved="no">
|
||||
<source>Conditions accepted on: %@.</source>
|
||||
<target state="translated">الشروط المقبولة على: %@.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve" approved="no">
|
||||
<source>Conditions are accepted for the operator(s): **%@**.</source>
|
||||
<target state="translated">يتم قبول شروط المشغل (المشغلين): **%@**.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve" approved="no">
|
||||
<source>Conditions will be automatically accepted for enabled operators on: %@.</source>
|
||||
<target state="translated">سيتم قبول الشروط تلقائيًا للمشغلين الممكّنين على: %@.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve" approved="no">
|
||||
<source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source>
|
||||
<target state="translated">أنشئ ملفًا شخصيًا جديدًا في [تطبيق سطح المكتب](https://simplex.chat/downloads/). 💻</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error adding server" xml:space="preserve" approved="no">
|
||||
<source>Error adding server</source>
|
||||
<target state="translated">خطأ في إضافة الخادم</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Created at: %@" xml:space="preserve" approved="no">
|
||||
<source>Created at: %@</source>
|
||||
<target state="translated">تم الإنشاء في: %@</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve" approved="no">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<target state="translated">حذف %lld الرسائل القديمة للأعضاء؟</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing message" xml:space="preserve" approved="no">
|
||||
<source>Disappearing message</source>
|
||||
<target state="translated">رسالة اختفاء</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enabled" xml:space="preserve" approved="no">
|
||||
<source>Enabled</source>
|
||||
<target state="translated">ممكّنة</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: database migration error" xml:space="preserve" approved="no">
|
||||
<source>Encrypted message: database migration error</source>
|
||||
<target state="translated">رسالة مشفرة: خطأ في ترحيل قاعدة البيانات</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete list?" xml:space="preserve" approved="no">
|
||||
<source>Delete list?</source>
|
||||
<target state="translated">Delete list?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delivered even when Apple drops them." xml:space="preserve" approved="no">
|
||||
<source>Delivered even when Apple drops them.</source>
|
||||
<target state="translated">يتم تسليمها حتى عندما تسقطها شركة Apple.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve" approved="no">
|
||||
<source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source>
|
||||
<target state="translated">عنوان خادم الوجهة %@ غير متوافق مع إعدادات خادم التوجيه %@.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve" approved="no">
|
||||
<source>Destination server version of %@ is incompatible with forwarding server %@.</source>
|
||||
<target state="translated">إصدار خادم الوجهة لـ %@ غير متوافق مع خادم التوجيه %@.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Don't create address" xml:space="preserve" approved="no">
|
||||
<source>Don't create address</source>
|
||||
<target state="translated">لا تنشئ عنوان</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Done" xml:space="preserve" approved="no">
|
||||
<source>Done</source>
|
||||
<target state="translated">تم</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Duration" xml:space="preserve" approved="no">
|
||||
<source>Duration</source>
|
||||
<target state="translated">المدة</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve" approved="no">
|
||||
<source>Encrypt local files</source>
|
||||
<target state="translated">تشفير الملفات المحلية</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encryption renegotiation in progress." xml:space="preserve" approved="no">
|
||||
<source>Encryption renegotiation in progress.</source>
|
||||
<target state="translated">إعادة التفاوض على التشفير قيد التنفيذ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter Passcode" xml:space="preserve" approved="no">
|
||||
<source>Enter Passcode</source>
|
||||
<target state="translated">أدخل رمز المرور</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase" xml:space="preserve" approved="no">
|
||||
<source>Enter passphrase</source>
|
||||
<target state="translated">قم بأدخل عبارة المرور</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter welcome message…" xml:space="preserve" approved="no">
|
||||
<source>Enter welcome message…</source>
|
||||
<target state="translated">أدخل رسالة ترحيب…</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter your name…" xml:space="preserve" approved="no">
|
||||
<source>Enter your name…</source>
|
||||
<target state="translated">أدخل اسمك…</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing to incognito!" xml:space="preserve" approved="no">
|
||||
<source>Error changing to incognito!</source>
|
||||
<target state="translated">خطأ في التغيير إلى التصفح المتخفي!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve" approved="no">
|
||||
<source>Delete %lld messages?</source>
|
||||
<target state="translated">حذف %lld رسائل؟</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error aborting address change" xml:space="preserve" approved="no">
|
||||
<source>Error aborting address change</source>
|
||||
<target state="translated">خطأ في إجهاض تغيير العنوان</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at" xml:space="preserve" approved="no">
|
||||
<source>Disappears at</source>
|
||||
<target state="translated">يختفي عند</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do not use credentials with proxy." xml:space="preserve" approved="no">
|
||||
<source>Do not use credentials with proxy.</source>
|
||||
<target state="translated">لا تستخدم بيانات الاعتماد مع البروكسي.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error accepting conditions" xml:space="preserve" approved="no">
|
||||
<source>Error accepting conditions</source>
|
||||
<target state="translated">خطأ في قبول الشروط</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter password above to show!" xml:space="preserve" approved="no">
|
||||
<source>Enter password above to show!</source>
|
||||
<target state="translated">أدخل كلمة المرور أعلاه للعرض!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing connection profile" xml:space="preserve" approved="no">
|
||||
<source>Error changing connection profile</source>
|
||||
<target state="translated">خطأ في تغيير ملف تعريف الاتصال</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve" approved="no">
|
||||
<source>Desktop app version %@ is not compatible with this app.</source>
|
||||
<target state="translated">إصدار تطبيق سطح المكتب %@ غير متوافق مع هذا التطبيق.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve" approved="no">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<target state="translated">تشفير الملفات والوسائط المخزنة</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter this device name…" xml:space="preserve" approved="no">
|
||||
<source>Enter this device name…</source>
|
||||
<target state="translated">أدخل اسم الجهاز…</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter welcome message… (optional)" xml:space="preserve" approved="no">
|
||||
<source>Enter welcome message… (optional)</source>
|
||||
<target state="translated">أدخل رسالة ترحيب... (اختياري)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Correct name to %@?" xml:space="preserve" approved="no">
|
||||
<source>Correct name to %@?</source>
|
||||
<target state="translated">الاسم الصحيح ل %@؟</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete member message?" xml:space="preserve" approved="no">
|
||||
<source>Delete member message?</source>
|
||||
<target state="translated">حذف رسالة العضو؟</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable automatic message deletion?" xml:space="preserve" approved="no">
|
||||
<source>Disable automatic message deletion?</source>
|
||||
<target state="translated">تعطيل حذف الرسائل التلقائي؟</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable delete messages" xml:space="preserve" approved="no">
|
||||
<source>Disable delete messages</source>
|
||||
<target state="translated">تعطيل حذف الرسائل</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable for all" xml:space="preserve" approved="no">
|
||||
<source>Disable for all</source>
|
||||
<target state="translated">تعطيل للجميع</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disabled" xml:space="preserve" approved="no">
|
||||
<source>Disabled</source>
|
||||
<target state="translated">عاجز</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Documents:" xml:space="preserve" approved="no">
|
||||
<source>Documents:</source>
|
||||
<target state="translated">المستندات:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve" approved="no">
|
||||
<source>By using SimpleX Chat you agree to:
|
||||
- send only legal content in public groups.
|
||||
- respect other users – no spam.</source>
|
||||
<target state="translated">باستخدامك SimpleX Chat، فإنك توافق على:
|
||||
- إرسال محتوى قانوني فقط في المجموعات العامة.
|
||||
- احترام المستخدمين الآخرين - ممنوع إرسال رسائل مزعجة.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Configure server operators" xml:space="preserve" approved="no">
|
||||
<source>Configure server operators</source>
|
||||
<target state="translated">تكوين مشغلي الخادم</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve" approved="no">
|
||||
<source>Enable Flux in Network & servers settings for better metadata privacy.</source>
|
||||
<target state="translated">تمكين التدفق في إعدادات الشبكة والخوادم لتحسين خصوصية البيانات الوصفية.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve" approved="no">
|
||||
<source>Discover and join groups</source>
|
||||
<target state="translated">اكتشف المجموعات وانضم إليها</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover via local network" xml:space="preserve" approved="no">
|
||||
<source>Discover via local network</source>
|
||||
<target state="translated">اكتشف عبر الشبكة المحلية</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enabled for" xml:space="preserve" approved="no">
|
||||
<source>Enabled for</source>
|
||||
<target state="translated">ممكّن ل</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve" approved="no">
|
||||
<source>Encrypted message: app is stopped</source>
|
||||
<target state="translated">رسالة مشفرة: تم إيقاف التطبيق</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter group name…" xml:space="preserve" approved="no">
|
||||
<source>Enter group name…</source>
|
||||
<target state="translated">أدخل اسم المجموعة…</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT use private routing." xml:space="preserve" approved="no">
|
||||
<source>Do NOT use private routing.</source>
|
||||
<target state="translated">لا تستخدم التوجيه الخاص.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encryption re-negotiation error" xml:space="preserve" approved="no">
|
||||
<source>Encryption re-negotiation error</source>
|
||||
<target state="translated">خطأ في إعادة تفاوض التشفير</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection with desktop stopped" xml:space="preserve" approved="no">
|
||||
<source>Connection with desktop stopped</source>
|
||||
<target state="translated">تم إيقاف الاتصال بسطح المكتب</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Destination server error: %@" xml:space="preserve" approved="no">
|
||||
<source>Destination server error: %@</source>
|
||||
<target state="translated">خطأ خادم الوجهة: %@</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve" approved="no">
|
||||
<source>Do NOT send messages directly, even if your or destination server does not support private routing.</source>
|
||||
<target state="translated">لا ترسل الرسائل بشكل مباشر، حتى لو كان خادمك أو خادم الوجهة لا يدعم التوجيه الخاص.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve" approved="no">
|
||||
<source>Direct messages between members are prohibited in this chat.</source>
|
||||
<target state="translated">يُحظر إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect desktop?" xml:space="preserve" approved="no">
|
||||
<source>Disconnect desktop?</source>
|
||||
<target state="translated">فصل سطح المكتب؟</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable (keep overrides)" xml:space="preserve" approved="no">
|
||||
<source>Disable (keep overrides)</source>
|
||||
<target state="translated">تعطيل (الاحتفاظ بالتجاوزات)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at: %@" xml:space="preserve" approved="no">
|
||||
<source>Disappears at: %@</source>
|
||||
<target state="translated">يختفي عند: %@</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do not send history to new members." xml:space="preserve" approved="no">
|
||||
<source>Do not send history to new members.</source>
|
||||
<target state="translated">لا ترسل التاريخ إلى الأعضاء الجدد.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve" approved="no">
|
||||
<source>Encryption re-negotiation failed.</source>
|
||||
<target state="translated">فشل إعادة التفاوض على التشفير.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ar" datatype="plaintext">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="bg" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" (can be copied)" xml:space="preserve">
|
||||
|
@ -431,7 +431,7 @@
|
|||
<source>1 day</source>
|
||||
<target>1 ден</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 hour" xml:space="preserve">
|
||||
<source>1 hour</source>
|
||||
|
@ -447,13 +447,13 @@ time interval</note>
|
|||
<source>1 month</source>
|
||||
<target>1 месец</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 week" xml:space="preserve">
|
||||
<source>1 week</source>
|
||||
<target>1 седмица</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 year" xml:space="preserve">
|
||||
<source>1 year</source>
|
||||
|
@ -551,8 +551,8 @@ time interval</note>
|
|||
<source>Accept</source>
|
||||
<target>Приеми</target>
|
||||
<note>accept contact request via notification
|
||||
accept incoming call via notification
|
||||
swipe action</note>
|
||||
accept incoming call via notification
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept conditions" xml:space="preserve">
|
||||
<source>Accept conditions</source>
|
||||
|
@ -573,7 +573,7 @@ swipe action</note>
|
|||
<source>Accept incognito</source>
|
||||
<target>Приеми инкогнито</target>
|
||||
<note>accept contact request via notification
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accepted conditions" xml:space="preserve">
|
||||
<source>Accepted conditions</source>
|
||||
|
@ -778,10 +778,6 @@ swipe action</note>
|
|||
<source>All reports will be archived for you.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All servers" xml:space="preserve">
|
||||
<source>All servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
|
||||
<source>All your contacts will remain connected.</source>
|
||||
<target>Всички ваши контакти ще останат свързани.</target>
|
||||
|
@ -1063,6 +1059,10 @@ swipe action</note>
|
|||
<target>Архивиране на база данни</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Ask" xml:space="preserve">
|
||||
<source>Ask</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Attach" xml:space="preserve">
|
||||
<source>Attach</source>
|
||||
<target>Прикачи</target>
|
||||
|
@ -1300,12 +1300,6 @@ swipe action</note>
|
|||
<target>Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve">
|
||||
<source>By using SimpleX Chat you agree to:
|
||||
- send only legal content in public groups.
|
||||
- respect other users – no spam.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Разговорът вече приключи!</target>
|
||||
|
@ -1354,7 +1348,7 @@ swipe action</note>
|
|||
<source>Cancel</source>
|
||||
<target>Отказ</target>
|
||||
<note>alert action
|
||||
alert button</note>
|
||||
alert button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve">
|
||||
<source>Cancel migration</source>
|
||||
|
@ -1443,7 +1437,7 @@ alert button</note>
|
|||
<source>Change self-destruct passcode</source>
|
||||
<target>Промени кода за достъп за самоунищожение</target>
|
||||
<note>authentication reason
|
||||
set passcode view</note>
|
||||
set passcode view</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat" xml:space="preserve">
|
||||
<source>Chat</source>
|
||||
|
@ -1660,6 +1654,14 @@ set passcode view</note>
|
|||
<source>Conditions of use</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for enabled operators after 30 days." xml:space="preserve">
|
||||
<source>Conditions will be accepted for enabled operators after 30 days.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for operator(s): **%@**." xml:space="preserve">
|
||||
<source>Conditions will be accepted for operator(s): **%@**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve">
|
||||
<source>Conditions will be accepted for the operator(s): **%@**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -1677,10 +1679,6 @@ set passcode view</note>
|
|||
<target>Конфигурирай ICE сървъри</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Configure server operators" xml:space="preserve">
|
||||
<source>Configure server operators</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm" xml:space="preserve">
|
||||
<source>Confirm</source>
|
||||
<target>Потвърди</target>
|
||||
|
@ -2246,7 +2244,7 @@ This is your own one-time link!</source>
|
|||
<source>Delete</source>
|
||||
<target>Изтрий</target>
|
||||
<note>alert action
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
|
@ -2713,7 +2711,7 @@ swipe action</note>
|
|||
<source>Download</source>
|
||||
<target>Изтегли</target>
|
||||
<note>alert button
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download errors" xml:space="preserve">
|
||||
<source>Download errors</source>
|
||||
|
@ -2785,8 +2783,8 @@ chat item action</note>
|
|||
<target>Активиране (запазване на промените)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve">
|
||||
<source>Enable Flux in Network & servers settings for better metadata privacy.</source>
|
||||
<trans-unit id="Enable Flux" xml:space="preserve">
|
||||
<source>Enable Flux</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable SimpleX Lock" xml:space="preserve">
|
||||
|
@ -3339,9 +3337,7 @@ chat item action</note>
|
|||
<trans-unit id="Error: %@" xml:space="preserve">
|
||||
<source>Error: %@</source>
|
||||
<target>Грешка: %@</target>
|
||||
<note>alert message
|
||||
file error text
|
||||
snd error text</note>
|
||||
<note>alert message</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: URL is invalid" xml:space="preserve">
|
||||
<source>Error: URL is invalid</source>
|
||||
|
@ -5260,7 +5256,7 @@ Requires compatible VPN.</source>
|
|||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<target>Отвори</target>
|
||||
<note>alert action</note>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
|
@ -5290,15 +5286,23 @@ Requires compatible VPN.</source>
|
|||
<target>Отвори група</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open link?" xml:space="preserve">
|
||||
<source>Open link?</source>
|
||||
<note>alert title</note>
|
||||
<trans-unit id="Open link" xml:space="preserve">
|
||||
<source>Open link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open links from chat list" xml:space="preserve">
|
||||
<source>Open links from chat list</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||
<source>Open migration to another device</source>
|
||||
<target>Отвори миграцията към друго устройство</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open web link?" xml:space="preserve">
|
||||
<source>Open web link?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening app…" xml:space="preserve">
|
||||
<source>Opening app…</source>
|
||||
<target>Приложението се отваря…</target>
|
||||
|
@ -5585,19 +5589,11 @@ Error: %@</source>
|
|||
<source>Privacy for your customers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy policy and conditions of use." xml:space="preserve">
|
||||
<source>Privacy policy and conditions of use.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy redefined" xml:space="preserve">
|
||||
<source>Privacy redefined</source>
|
||||
<target>Поверителността преосмислена</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve">
|
||||
<source>Private chats, groups and your contacts are not accessible to server operators.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private filenames" xml:space="preserve">
|
||||
<source>Private filenames</source>
|
||||
<target>Поверителни имена на файлове</target>
|
||||
|
@ -5938,7 +5934,7 @@ Enable in *Network & servers* settings.</source>
|
|||
<source>Reject</source>
|
||||
<target>Отхвърляне</target>
|
||||
<note>reject incoming call via notification
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reject (sender NOT notified)" xml:space="preserve">
|
||||
<source>Reject (sender NOT notified)</source>
|
||||
|
@ -6157,6 +6153,10 @@ swipe action</note>
|
|||
<source>Review conditions</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Review later" xml:space="preserve">
|
||||
<source>Review later</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Revoke" xml:space="preserve">
|
||||
<source>Revoke</source>
|
||||
<target>Отзови</target>
|
||||
|
@ -6203,7 +6203,7 @@ swipe action</note>
|
|||
<source>Save</source>
|
||||
<target>Запази</target>
|
||||
<note>alert button
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
|
||||
<source>Save (and notify contacts)</source>
|
||||
|
@ -6736,7 +6736,7 @@ chat item action</note>
|
|||
<source>Share</source>
|
||||
<target>Сподели</target>
|
||||
<note>alert action
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share 1-time link" xml:space="preserve">
|
||||
<source>Share 1-time link</source>
|
||||
|
@ -6792,10 +6792,6 @@ chat item action</note>
|
|||
<target>Сподели с контактите</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Short link" xml:space="preserve">
|
||||
<source>Short link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<target>Покажи QR код</target>
|
||||
|
@ -6889,10 +6885,6 @@ chat item action</note>
|
|||
<source>SimpleX address or 1-time link?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX channel link" xml:space="preserve">
|
||||
<source>SimpleX channel link</source>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX contact address" xml:space="preserve">
|
||||
<source>SimpleX contact address</source>
|
||||
<target>SimpleX адрес за контакт</target>
|
||||
|
@ -6990,7 +6982,7 @@ chat item action</note>
|
|||
<trans-unit id="Spam" xml:space="preserve">
|
||||
<source>Spam</source>
|
||||
<note>blocking reason
|
||||
report reason</note>
|
||||
report reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Square, circle, or anything in between." xml:space="preserve">
|
||||
<source>Square, circle, or anything in between.</source>
|
||||
|
@ -7329,10 +7321,19 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||
<target>Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The profile is only shared with your contacts." xml:space="preserve">
|
||||
<source>The profile is only shared with your contacts.</source>
|
||||
<target>Профилът се споделя само с вашите контакти.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve">
|
||||
<source>The same conditions will apply to operator **%@**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The same conditions will apply to operator(s): **%@**." xml:space="preserve">
|
||||
<source>The same conditions will apply to operator(s): **%@**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The second preset operator in the app!" xml:space="preserve">
|
||||
<source>The second preset operator in the app!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -7442,10 +7443,6 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||
<target>Това е вашят еднократен линк за връзка!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve">
|
||||
<source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve">
|
||||
<source>This link was used with another mobile device, please create a new link on the desktop.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -7728,10 +7725,6 @@ To connect, please ask your contact to create another connection link and check
|
|||
<target>Непрочетено</target>
|
||||
<note>swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unsupported connection link" xml:space="preserve">
|
||||
<source>Unsupported connection link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
|
||||
<source>Up to 100 last messages are sent to new members.</source>
|
||||
<target>На новите членове се изпращат до последните 100 съобщения.</target>
|
||||
|
@ -7819,10 +7812,6 @@ To connect, please ask your contact to create another connection link and check
|
|||
<source>Use TCP port %@ when no port is specified.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve">
|
||||
<source>Use TCP port 443 for preset servers only.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use chat" xml:space="preserve">
|
||||
<source>Use chat</source>
|
||||
<target>Използвай чата</target>
|
||||
|
@ -7883,10 +7872,6 @@ To connect, please ask your contact to create another connection link and check
|
|||
<source>Use servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use short links (BETA)" xml:space="preserve">
|
||||
<source>Use short links (BETA)</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use the app while in the call." xml:space="preserve">
|
||||
<source>Use the app while in the call.</source>
|
||||
<target>Използвайте приложението по време на разговора.</target>
|
||||
|
@ -8168,6 +8153,10 @@ To connect, please ask your contact to create another connection link and check
|
|||
<source>XFTP server</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Yes" xml:space="preserve">
|
||||
<source>Yes</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
|
||||
<source>You **must not** use the same database on two devices.</source>
|
||||
<target>**Не трябва** да използвате една и съща база данни на две устройства.</target>
|
||||
|
@ -8257,6 +8246,10 @@ Repeat join request?</source>
|
|||
<source>You can change it in Appearance settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can configure operators in Network & servers settings." xml:space="preserve">
|
||||
<source>You can configure operators in Network & servers settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can configure servers via settings." xml:space="preserve">
|
||||
<source>You can configure servers via settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -8490,6 +8483,11 @@ Repeat connection request?</source>
|
|||
<target>Вашите ICE сървъри</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your SMP servers" xml:space="preserve">
|
||||
<source>Your SMP servers</source>
|
||||
<target>Вашите SMP сървъри</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your SimpleX address" xml:space="preserve">
|
||||
<source>Your SimpleX address</source>
|
||||
<target>Вашият адрес в SimpleX</target>
|
||||
|
@ -8572,11 +8570,6 @@ Repeat connection request?</source>
|
|||
<target>Вашият профил **%@** ще бъде споделен.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and only shared with your contacts.</source>
|
||||
<target>Профилът се споделя само с вашите контакти.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
|
||||
<target>Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил.</target>
|
||||
|
@ -8586,6 +8579,11 @@ Repeat connection request?</source>
|
|||
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
|
||||
<note>alert message</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
|
||||
<source>Your profile, contacts and delivered messages are stored on your device.</source>
|
||||
<target>Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your random profile" xml:space="preserve">
|
||||
<source>Your random profile</source>
|
||||
<target>Вашият автоматично генериран профил</target>
|
||||
|
@ -8720,8 +8718,7 @@ Repeat connection request?</source>
|
|||
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||
<source>blocked by admin</source>
|
||||
<target>блокиран от админ</target>
|
||||
<note>blocked chat item
|
||||
marked deleted chat item preview text</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bold" xml:space="preserve">
|
||||
<source>bold</source>
|
||||
|
@ -8890,7 +8887,7 @@ marked deleted chat item preview text</note>
|
|||
<source>default (%@)</source>
|
||||
<target>по подразбиране (%@)</target>
|
||||
<note>delete after time
|
||||
pref value</note>
|
||||
pref value</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="default (no)" xml:space="preserve">
|
||||
<source>default (no)</source>
|
||||
|
@ -9016,10 +9013,19 @@ pref value</note>
|
|||
<target>грешка</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="event happened" xml:space="preserve">
|
||||
<source>event happened</source>
|
||||
<target>събитие се случи</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="expired" xml:space="preserve">
|
||||
<source>expired</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="for better metadata privacy." xml:space="preserve">
|
||||
<source>for better metadata privacy.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="forwarded" xml:space="preserve">
|
||||
<source>forwarded</source>
|
||||
<target>препратено</target>
|
||||
|
@ -9225,8 +9231,8 @@ pref value</note>
|
|||
<source>off</source>
|
||||
<target>изключено</target>
|
||||
<note>enabled status
|
||||
group pref value
|
||||
time to disappear</note>
|
||||
group pref value
|
||||
time to disappear</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="offered %@" xml:space="preserve">
|
||||
<source>offered %@</source>
|
||||
|
@ -9582,7 +9588,7 @@ last received msg: %2$@</source>
|
|||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="bg" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
|
@ -9619,7 +9625,7 @@ last received msg: %2$@</source>
|
|||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="bg" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
@ -9641,17 +9647,13 @@ last received msg: %2$@</source>
|
|||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="bg" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="%d new events" xml:space="preserve">
|
||||
<source>%d new events</source>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="From %d chat(s)" xml:space="preserve">
|
||||
<source>From %d chat(s)</source>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="From: %@" xml:space="preserve">
|
||||
<source>From: %@</source>
|
||||
<note>notification body</note>
|
||||
|
@ -9664,11 +9666,15 @@ last received msg: %2$@</source>
|
|||
<source>New messages</source>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New messages in %d chats" xml:space="preserve">
|
||||
<source>New messages in %d chats</source>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="bg" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
@ -9687,7 +9693,7 @@ last received msg: %2$@</source>
|
|||
</file>
|
||||
<file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="bg" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="%@" xml:space="preserve">
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "bg",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "16C5032a",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "16.2"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
|
@ -3422,8 +3422,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||
<source>The old database was not removed during the migration, it can be deleted.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and only shared with your contacts.</source>
|
||||
<trans-unit id="The profile is only shared with your contacts." xml:space="preserve">
|
||||
<source>The profile is only shared with your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The sender will NOT be notified" xml:space="preserve">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" (can be copied)" xml:space="preserve">
|
||||
|
@ -145,22 +145,18 @@
|
|||
</trans-unit>
|
||||
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
|
||||
<source>%d file(s) are still being downloaded.</source>
|
||||
<target>%d soubor(y) stále stahován(y).</target>
|
||||
<note>forward confirmation reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
|
||||
<source>%d file(s) failed to download.</source>
|
||||
<target>%d soubor(y) se nepodařilo stáhnout.</target>
|
||||
<note>forward confirmation reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
|
||||
<source>%d file(s) were deleted.</source>
|
||||
<target>%d soubor(y) smazán(y).</target>
|
||||
<note>forward confirmation reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
|
||||
<source>%d file(s) were not downloaded.</source>
|
||||
<target>%d soubor(y) nestažen(y).</target>
|
||||
<note>forward confirmation reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d hours" xml:space="preserve">
|
||||
|
@ -424,7 +420,7 @@
|
|||
<source>1 day</source>
|
||||
<target>1 den</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 hour" xml:space="preserve">
|
||||
<source>1 hour</source>
|
||||
|
@ -440,13 +436,13 @@ time interval</note>
|
|||
<source>1 month</source>
|
||||
<target>1 měsíc</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 week" xml:space="preserve">
|
||||
<source>1 week</source>
|
||||
<target>1 týden</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 year" xml:space="preserve">
|
||||
<source>1 year</source>
|
||||
|
@ -541,8 +537,8 @@ time interval</note>
|
|||
<source>Accept</source>
|
||||
<target>Přijmout</target>
|
||||
<note>accept contact request via notification
|
||||
accept incoming call via notification
|
||||
swipe action</note>
|
||||
accept incoming call via notification
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept conditions" xml:space="preserve">
|
||||
<source>Accept conditions</source>
|
||||
|
@ -562,7 +558,7 @@ swipe action</note>
|
|||
<source>Accept incognito</source>
|
||||
<target>Přijmout inkognito</target>
|
||||
<note>accept contact request via notification
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accepted conditions" xml:space="preserve">
|
||||
<source>Accepted conditions</source>
|
||||
|
@ -747,10 +743,6 @@ swipe action</note>
|
|||
<source>All reports will be archived for you.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All servers" xml:space="preserve">
|
||||
<source>All servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
|
||||
<source>All your contacts will remain connected.</source>
|
||||
<target>Všechny vaše kontakty zůstanou připojeny.</target>
|
||||
|
@ -1016,6 +1008,10 @@ swipe action</note>
|
|||
<source>Archiving database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Ask" xml:space="preserve">
|
||||
<source>Ask</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Attach" xml:space="preserve">
|
||||
<source>Attach</source>
|
||||
<target>Připojit</target>
|
||||
|
@ -1231,12 +1227,6 @@ swipe action</note>
|
|||
<target>Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve">
|
||||
<source>By using SimpleX Chat you agree to:
|
||||
- send only legal content in public groups.
|
||||
- respect other users – no spam.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Hovor již skončil!</target>
|
||||
|
@ -1281,7 +1271,7 @@ swipe action</note>
|
|||
<source>Cancel</source>
|
||||
<target>Zrušit</target>
|
||||
<note>alert action
|
||||
alert button</note>
|
||||
alert button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve">
|
||||
<source>Cancel migration</source>
|
||||
|
@ -1366,7 +1356,7 @@ alert button</note>
|
|||
<source>Change self-destruct passcode</source>
|
||||
<target>Změnit sebedestrukční heslo</target>
|
||||
<note>authentication reason
|
||||
set passcode view</note>
|
||||
set passcode view</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat" xml:space="preserve">
|
||||
<source>Chat</source>
|
||||
|
@ -1579,6 +1569,14 @@ set passcode view</note>
|
|||
<source>Conditions of use</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for enabled operators after 30 days." xml:space="preserve">
|
||||
<source>Conditions will be accepted for enabled operators after 30 days.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for operator(s): **%@**." xml:space="preserve">
|
||||
<source>Conditions will be accepted for operator(s): **%@**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve">
|
||||
<source>Conditions will be accepted for the operator(s): **%@**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -1596,10 +1594,6 @@ set passcode view</note>
|
|||
<target>Konfigurace serverů ICE</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Configure server operators" xml:space="preserve">
|
||||
<source>Configure server operators</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm" xml:space="preserve">
|
||||
<source>Confirm</source>
|
||||
<target>Potvrdit</target>
|
||||
|
@ -1942,7 +1936,6 @@ This is your own one-time link!</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="Create profile" xml:space="preserve">
|
||||
<source>Create profile</source>
|
||||
<target>Vytvořte si profil</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create queue" xml:space="preserve">
|
||||
|
@ -2141,7 +2134,7 @@ This is your own one-time link!</source>
|
|||
<source>Delete</source>
|
||||
<target>Smazat</target>
|
||||
<note>alert action
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
|
@ -2598,7 +2591,7 @@ swipe action</note>
|
|||
<trans-unit id="Download" xml:space="preserve">
|
||||
<source>Download</source>
|
||||
<note>alert button
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download errors" xml:space="preserve">
|
||||
<source>Download errors</source>
|
||||
|
@ -2667,8 +2660,8 @@ chat item action</note>
|
|||
<target>Povolit (zachovat přepsání)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve">
|
||||
<source>Enable Flux in Network & servers settings for better metadata privacy.</source>
|
||||
<trans-unit id="Enable Flux" xml:space="preserve">
|
||||
<source>Enable Flux</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable SimpleX Lock" xml:space="preserve">
|
||||
|
@ -3204,9 +3197,7 @@ chat item action</note>
|
|||
<trans-unit id="Error: %@" xml:space="preserve">
|
||||
<source>Error: %@</source>
|
||||
<target>Chyba: %@</target>
|
||||
<note>alert message
|
||||
file error text
|
||||
snd error text</note>
|
||||
<note>alert message</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: URL is invalid" xml:space="preserve">
|
||||
<source>Error: URL is invalid</source>
|
||||
|
@ -5065,7 +5056,7 @@ Vyžaduje povolení sítě VPN.</target>
|
|||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<target>Otevřít</target>
|
||||
<note>alert action</note>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
|
@ -5094,14 +5085,22 @@ Vyžaduje povolení sítě VPN.</target>
|
|||
<source>Open group</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open link?" xml:space="preserve">
|
||||
<source>Open link?</source>
|
||||
<note>alert title</note>
|
||||
<trans-unit id="Open link" xml:space="preserve">
|
||||
<source>Open link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open links from chat list" xml:space="preserve">
|
||||
<source>Open links from chat list</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||
<source>Open migration to another device</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open web link?" xml:space="preserve">
|
||||
<source>Open web link?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening app…" xml:space="preserve">
|
||||
<source>Opening app…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -5374,19 +5373,11 @@ Error: %@</source>
|
|||
<source>Privacy for your customers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy policy and conditions of use." xml:space="preserve">
|
||||
<source>Privacy policy and conditions of use.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy redefined" xml:space="preserve">
|
||||
<source>Privacy redefined</source>
|
||||
<target>Nové vymezení soukromí</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve">
|
||||
<source>Private chats, groups and your contacts are not accessible to server operators.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private filenames" xml:space="preserve">
|
||||
<source>Private filenames</source>
|
||||
<target>Soukromé názvy souborů</target>
|
||||
|
@ -5719,7 +5710,7 @@ Enable in *Network & servers* settings.</source>
|
|||
<source>Reject</source>
|
||||
<target>Odmítnout</target>
|
||||
<note>reject incoming call via notification
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reject (sender NOT notified)" xml:space="preserve">
|
||||
<source>Reject (sender NOT notified)</source>
|
||||
|
@ -5932,6 +5923,10 @@ swipe action</note>
|
|||
<source>Review conditions</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Review later" xml:space="preserve">
|
||||
<source>Review later</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Revoke" xml:space="preserve">
|
||||
<source>Revoke</source>
|
||||
<target>Odvolat</target>
|
||||
|
@ -5977,7 +5972,7 @@ swipe action</note>
|
|||
<source>Save</source>
|
||||
<target>Uložit</target>
|
||||
<note>alert button
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
|
||||
<source>Save (and notify contacts)</source>
|
||||
|
@ -6500,7 +6495,7 @@ chat item action</note>
|
|||
<source>Share</source>
|
||||
<target>Sdílet</target>
|
||||
<note>alert action
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share 1-time link" xml:space="preserve">
|
||||
<source>Share 1-time link</source>
|
||||
|
@ -6555,10 +6550,6 @@ chat item action</note>
|
|||
<target>Sdílet s kontakty</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Short link" xml:space="preserve">
|
||||
<source>Short link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -6651,10 +6642,6 @@ chat item action</note>
|
|||
<source>SimpleX address or 1-time link?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX channel link" xml:space="preserve">
|
||||
<source>SimpleX channel link</source>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX contact address" xml:space="preserve">
|
||||
<source>SimpleX contact address</source>
|
||||
<target>SimpleX kontaktní adresa</target>
|
||||
|
@ -6750,7 +6737,7 @@ chat item action</note>
|
|||
<trans-unit id="Spam" xml:space="preserve">
|
||||
<source>Spam</source>
|
||||
<note>blocking reason
|
||||
report reason</note>
|
||||
report reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Square, circle, or anything in between." xml:space="preserve">
|
||||
<source>Square, circle, or anything in between.</source>
|
||||
|
@ -7081,10 +7068,19 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
|
|||
<target>Stará databáze nebyla během přenášení odstraněna, lze ji smazat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The profile is only shared with your contacts." xml:space="preserve">
|
||||
<source>The profile is only shared with your contacts.</source>
|
||||
<target>Profil je sdílen pouze s vašimi kontakty.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve">
|
||||
<source>The same conditions will apply to operator **%@**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The same conditions will apply to operator(s): **%@**." xml:space="preserve">
|
||||
<source>The same conditions will apply to operator(s): **%@**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The second preset operator in the app!" xml:space="preserve">
|
||||
<source>The second preset operator in the app!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -7187,10 +7183,6 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
|
|||
<source>This is your own one-time link!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve">
|
||||
<source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve">
|
||||
<source>This link was used with another mobile device, please create a new link on the desktop.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -7464,10 +7456,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
|||
<target>Nepřečtený</target>
|
||||
<note>swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unsupported connection link" xml:space="preserve">
|
||||
<source>Unsupported connection link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
|
||||
<source>Up to 100 last messages are sent to new members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -7552,10 +7540,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
|||
<source>Use TCP port %@ when no port is specified.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve">
|
||||
<source>Use TCP port 443 for preset servers only.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use chat" xml:space="preserve">
|
||||
<source>Use chat</source>
|
||||
<target>Použijte chat</target>
|
||||
|
@ -7614,10 +7598,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
|||
<source>Use servers</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use short links (BETA)" xml:space="preserve">
|
||||
<source>Use short links (BETA)</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use the app while in the call." xml:space="preserve">
|
||||
<source>Use the app while in the call.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -7881,6 +7861,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
|||
<source>XFTP server</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Yes" xml:space="preserve">
|
||||
<source>Yes</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
|
||||
<source>You **must not** use the same database on two devices.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -7961,6 +7945,10 @@ Repeat join request?</source>
|
|||
<source>You can change it in Appearance settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can configure operators in Network & servers settings." xml:space="preserve">
|
||||
<source>You can configure operators in Network & servers settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can configure servers via settings." xml:space="preserve">
|
||||
<source>You can configure servers via settings.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -8186,6 +8174,11 @@ Repeat connection request?</source>
|
|||
<target>Vaše servery ICE</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your SMP servers" xml:space="preserve">
|
||||
<source>Your SMP servers</source>
|
||||
<target>Vaše servery SMP</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your SimpleX address" xml:space="preserve">
|
||||
<source>Your SimpleX address</source>
|
||||
<target>Vaše SimpleX adresa</target>
|
||||
|
@ -8267,11 +8260,6 @@ Repeat connection request?</source>
|
|||
<target>Váš profil **%@** bude sdílen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and only shared with your contacts.</source>
|
||||
<target>Profil je sdílen pouze s vašimi kontakty.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
|
||||
<target>Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil.</target>
|
||||
|
@ -8281,6 +8269,11 @@ Repeat connection request?</source>
|
|||
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
|
||||
<note>alert message</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
|
||||
<source>Your profile, contacts and delivered messages are stored on your device.</source>
|
||||
<target>Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your random profile" xml:space="preserve">
|
||||
<source>Your random profile</source>
|
||||
<target>Váš náhodný profil</target>
|
||||
|
@ -8408,8 +8401,7 @@ Repeat connection request?</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||
<source>blocked by admin</source>
|
||||
<note>blocked chat item
|
||||
marked deleted chat item preview text</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bold" xml:space="preserve">
|
||||
<source>bold</source>
|
||||
|
@ -8577,7 +8569,7 @@ marked deleted chat item preview text</note>
|
|||
<source>default (%@)</source>
|
||||
<target>výchozí (%@)</target>
|
||||
<note>delete after time
|
||||
pref value</note>
|
||||
pref value</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="default (no)" xml:space="preserve">
|
||||
<source>default (no)</source>
|
||||
|
@ -8702,10 +8694,18 @@ pref value</note>
|
|||
<target>chyba</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="event happened" xml:space="preserve">
|
||||
<source>event happened</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="expired" xml:space="preserve">
|
||||
<source>expired</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="for better metadata privacy." xml:space="preserve">
|
||||
<source>for better metadata privacy.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="forwarded" xml:space="preserve">
|
||||
<source>forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
@ -8909,8 +8909,8 @@ pref value</note>
|
|||
<source>off</source>
|
||||
<target>vypnuto</target>
|
||||
<note>enabled status
|
||||
group pref value
|
||||
time to disappear</note>
|
||||
group pref value
|
||||
time to disappear</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="offered %@" xml:space="preserve">
|
||||
<source>offered %@</source>
|
||||
|
@ -9250,7 +9250,7 @@ last received msg: %2$@</source>
|
|||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
|
@ -9286,7 +9286,7 @@ last received msg: %2$@</source>
|
|||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
@ -9308,17 +9308,13 @@ last received msg: %2$@</source>
|
|||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="%d new events" xml:space="preserve">
|
||||
<source>%d new events</source>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="From %d chat(s)" xml:space="preserve">
|
||||
<source>From %d chat(s)</source>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="From: %@" xml:space="preserve">
|
||||
<source>From: %@</source>
|
||||
<note>notification body</note>
|
||||
|
@ -9331,11 +9327,15 @@ last received msg: %2$@</source>
|
|||
<source>New messages</source>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New messages in %d chats" xml:space="preserve">
|
||||
<source>New messages in %d chats</source>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
@ -9354,7 +9354,7 @@ last received msg: %2$@</source>
|
|||
</file>
|
||||
<file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="%@" xml:space="preserve">
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "cs",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "16C5032a",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "16.2"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" (can be copied)" xml:space="preserve">
|
||||
|
@ -440,7 +440,7 @@
|
|||
<source>1 day</source>
|
||||
<target>Älter als ein Tag</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 hour" xml:space="preserve">
|
||||
<source>1 hour</source>
|
||||
|
@ -456,13 +456,13 @@ time interval</note>
|
|||
<source>1 month</source>
|
||||
<target>Älter als ein Monat</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 week" xml:space="preserve">
|
||||
<source>1 week</source>
|
||||
<target>Älter als eine Woche</target>
|
||||
<note>delete after time
|
||||
time interval</note>
|
||||
time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 year" xml:space="preserve">
|
||||
<source>1 year</source>
|
||||
|
@ -562,8 +562,8 @@ time interval</note>
|
|||
<source>Accept</source>
|
||||
<target>Annehmen</target>
|
||||
<note>accept contact request via notification
|
||||
accept incoming call via notification
|
||||
swipe action</note>
|
||||
accept incoming call via notification
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept conditions" xml:space="preserve">
|
||||
<source>Accept conditions</source>
|
||||
|
@ -584,7 +584,7 @@ swipe action</note>
|
|||
<source>Accept incognito</source>
|
||||
<target>Inkognito akzeptieren</target>
|
||||
<note>accept contact request via notification
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accepted conditions" xml:space="preserve">
|
||||
<source>Accepted conditions</source>
|
||||
|
@ -796,11 +796,6 @@ swipe action</note>
|
|||
<target>Alle Meldungen werden für Sie archiviert.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All servers" xml:space="preserve">
|
||||
<source>All servers</source>
|
||||
<target>Alle Server</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
|
||||
<source>All your contacts will remain connected.</source>
|
||||
<target>Alle Ihre Kontakte bleiben verbunden.</target>
|
||||
|
@ -1091,6 +1086,11 @@ swipe action</note>
|
|||
<target>Datenbank wird archiviert</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Ask" xml:space="preserve">
|
||||
<source>Ask</source>
|
||||
<target>Fragen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Attach" xml:space="preserve">
|
||||
<source>Attach</source>
|
||||
<target>Anhängen</target>
|
||||
|
@ -1331,15 +1331,6 @@ swipe action</note>
|
|||
<target>Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve">
|
||||
<source>By using SimpleX Chat you agree to:
|
||||
- send only legal content in public groups.
|
||||
- respect other users – no spam.</source>
|
||||
<target>Durch die Nutzung von SimpleX Chat erklären Sie sich damit einverstanden:
|
||||
- nur legale Inhalte in öffentlichen Gruppen zu versenden.
|
||||
- andere Nutzer zu respektieren - kein Spam.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Anruf ist bereits beendet!</target>
|
||||
|
@ -1389,7 +1380,7 @@ swipe action</note>
|
|||
<source>Cancel</source>
|
||||
<target>Abbrechen</target>
|
||||
<note>alert action
|
||||
alert button</note>
|
||||
alert button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve">
|
||||
<source>Cancel migration</source>
|
||||
|
@ -1480,7 +1471,7 @@ alert button</note>
|
|||
<source>Change self-destruct passcode</source>
|
||||
<target>Selbstzerstörungs-Zugangscode ändern</target>
|
||||
<note>authentication reason
|
||||
set passcode view</note>
|
||||
set passcode view</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat" xml:space="preserve">
|
||||
<source>Chat</source>
|
||||
|
@ -1722,6 +1713,16 @@ set passcode view</note>
|
|||
<target>Nutzungsbedingungen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for enabled operators after 30 days." xml:space="preserve">
|
||||
<source>Conditions will be accepted for enabled operators after 30 days.</source>
|
||||
<target>Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for operator(s): **%@**." xml:space="preserve">
|
||||
<source>Conditions will be accepted for operator(s): **%@**.</source>
|
||||
<target>Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve">
|
||||
<source>Conditions will be accepted for the operator(s): **%@**.</source>
|
||||
<target>Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**.</target>
|
||||
|
@ -1742,11 +1743,6 @@ set passcode view</note>
|
|||
<target>ICE-Server konfigurieren</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Configure server operators" xml:space="preserve">
|
||||
<source>Configure server operators</source>
|
||||
<target>Server-Betreiber konfigurieren</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm" xml:space="preserve">
|
||||
<source>Confirm</source>
|
||||
<target>Bestätigen</target>
|
||||
|
@ -2195,7 +2191,7 @@ Das ist Ihr eigener Einmal-Link!</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="Current profile" xml:space="preserve">
|
||||
<source>Current profile</source>
|
||||
<target>Aktuelles Profil</target>
|
||||
<target>Aktueller Profil</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
|
||||
|
@ -2345,7 +2341,7 @@ Das ist Ihr eigener Einmal-Link!</target>
|
|||
<source>Delete</source>
|
||||
<target>Löschen</target>
|
||||
<note>alert action
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
|
@ -2841,7 +2837,7 @@ swipe action</note>
|
|||
<source>Download</source>
|
||||
<target>Herunterladen</target>
|
||||
<note>alert button
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download errors" xml:space="preserve">
|
||||
<source>Download errors</source>
|
||||
|
@ -2918,9 +2914,9 @@ chat item action</note>
|
|||
<target>Aktivieren (vorgenommene Einstellungen bleiben erhalten)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve">
|
||||
<source>Enable Flux in Network & servers settings for better metadata privacy.</source>
|
||||
<target>Für einen besseren Metadatenschutz Flux in den Netzwerk- und Servereinstellungen aktivieren.</target>
|
||||
<trans-unit id="Enable Flux" xml:space="preserve">
|
||||
<source>Enable Flux</source>
|
||||
<target>Flux aktivieren</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable SimpleX Lock" xml:space="preserve">
|
||||
|
@ -3325,7 +3321,7 @@ chat item action</note>
|
|||
</trans-unit>
|
||||
<trans-unit id="Error receiving file" xml:space="preserve">
|
||||
<source>Error receiving file</source>
|
||||
<target>Fehler beim Herunterladen der Datei</target>
|
||||
<target>Fehler beim Empfangen der Datei</target>
|
||||
<note>alert title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error reconnecting server" xml:space="preserve">
|
||||
|
@ -3496,9 +3492,7 @@ chat item action</note>
|
|||
<trans-unit id="Error: %@" xml:space="preserve">
|
||||
<source>Error: %@</source>
|
||||
<target>Fehler: %@</target>
|
||||
<note>alert message
|
||||
file error text
|
||||
snd error text</note>
|
||||
<note>alert message</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: URL is invalid" xml:space="preserve">
|
||||
<source>Error: URL is invalid</source>
|
||||
|
@ -3651,12 +3645,12 @@ snd error text</note>
|
|||
</trans-unit>
|
||||
<trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve">
|
||||
<source>File will be received when your contact completes uploading it.</source>
|
||||
<target>Die Datei wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</target>
|
||||
<target>Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve">
|
||||
<source>File will be received when your contact is online, please wait or check later!</source>
|
||||
<target>Die Datei wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<target>Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="File: %@" xml:space="preserve">
|
||||
|
@ -4145,12 +4139,12 @@ Fehler: %2$@</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve">
|
||||
<source>Image will be received when your contact completes uploading it.</source>
|
||||
<target>Das Bild wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</target>
|
||||
<target>Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve">
|
||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||
<target>Das Bild wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<target>Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immediately" xml:space="preserve">
|
||||
|
@ -4942,12 +4936,12 @@ Das ist Ihr Link für die Gruppe %@!</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<target>Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt.</target>
|
||||
<target>Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<target>Nachrichten, Dateien und Anrufe sind durch **Quantum-resistente E2E-Verschlüsselung** mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt.</target>
|
||||
<target>Nachrichten, Dateien und Anrufe sind durch **Quantum-resistente E2E-Verschlüsselung** mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate device" xml:space="preserve">
|
||||
|
@ -5312,7 +5306,7 @@ Das ist Ihr Link für die Gruppe %@!</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="No received or sent files" xml:space="preserve">
|
||||
<source>No received or sent files</source>
|
||||
<target>Keine herunter- oder hochgeladenen Dateien</target>
|
||||
<target>Keine empfangenen oder gesendeten Dateien</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No servers for private message routing." xml:space="preserve">
|
||||
|
@ -5322,7 +5316,7 @@ Das ist Ihr Link für die Gruppe %@!</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="No servers to receive files." xml:space="preserve">
|
||||
<source>No servers to receive files.</source>
|
||||
<target>Keine Server für das Herunterladen von Dateien.</target>
|
||||
<target>Keine Server für den Empfang von Dateien.</target>
|
||||
<note>servers error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No servers to receive messages." xml:space="preserve">
|
||||
|
@ -5541,7 +5535,7 @@ Dies erfordert die Aktivierung eines VPNs.</target>
|
|||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<target>Öffnen</target>
|
||||
<note>alert action</note>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
|
@ -5573,15 +5567,26 @@ Dies erfordert die Aktivierung eines VPNs.</target>
|
|||
<target>Gruppe öffnen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open link?" xml:space="preserve">
|
||||
<source>Open link?</source>
|
||||
<note>alert title</note>
|
||||
<trans-unit id="Open link" xml:space="preserve">
|
||||
<source>Open link</source>
|
||||
<target>Web-Link öffnen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open links from chat list" xml:space="preserve">
|
||||
<source>Open links from chat list</source>
|
||||
<target>Web-Links aus der Chat-Liste öffnen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||
<source>Open migration to another device</source>
|
||||
<target>Migration auf ein anderes Gerät öffnen</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open web link?" xml:space="preserve">
|
||||
<source>Open web link?</source>
|
||||
<target>Web-Link öffnen?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening app…" xml:space="preserve">
|
||||
<source>Opening app…</source>
|
||||
<target>App wird geöffnet…</target>
|
||||
|
@ -5888,21 +5893,11 @@ Fehler: %@</target>
|
|||
<target>Schutz der Privatsphäre Ihrer Kunden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy policy and conditions of use." xml:space="preserve">
|
||||
<source>Privacy policy and conditions of use.</source>
|
||||
<target>Datenschutz- und Nutzungsbedingungen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy redefined" xml:space="preserve">
|
||||
<source>Privacy redefined</source>
|
||||
<target>Datenschutz neu definiert</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve">
|
||||
<source>Private chats, groups and your contacts are not accessible to server operators.</source>
|
||||
<target>Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private filenames" xml:space="preserve">
|
||||
<source>Private filenames</source>
|
||||
<target>Neutrale Dateinamen</target>
|
||||
|
@ -6177,7 +6172,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
|
||||
<source>Receiving file will be stopped.</source>
|
||||
<target>Das Herunterladen der Datei wird beendet.</target>
|
||||
<target>Der Empfang der Datei wird beendet.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving via" xml:space="preserve">
|
||||
|
@ -6269,7 +6264,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.</target>
|
|||
<source>Reject</source>
|
||||
<target>Ablehnen</target>
|
||||
<note>reject incoming call via notification
|
||||
swipe action</note>
|
||||
swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reject (sender NOT notified)" xml:space="preserve">
|
||||
<source>Reject (sender NOT notified)</source>
|
||||
|
@ -6506,6 +6501,11 @@ swipe action</note>
|
|||
<target>Nutzungsbedingungen einsehen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Review later" xml:space="preserve">
|
||||
<source>Review later</source>
|
||||
<target>Später einsehen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Revoke" xml:space="preserve">
|
||||
<source>Revoke</source>
|
||||
<target>Widerrufen</target>
|
||||
|
@ -6543,7 +6543,7 @@ swipe action</note>
|
|||
</trans-unit>
|
||||
<trans-unit id="Safely receive files" xml:space="preserve">
|
||||
<source>Safely receive files</source>
|
||||
<target>Dateien sicher herunterladen</target>
|
||||
<target>Dateien sicher empfangen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safer groups" xml:space="preserve">
|
||||
|
@ -6555,7 +6555,7 @@ swipe action</note>
|
|||
<source>Save</source>
|
||||
<target>Speichern</target>
|
||||
<note>alert button
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
|
||||
<source>Save (and notify contacts)</source>
|
||||
|
@ -7126,7 +7126,7 @@ chat item action</note>
|
|||
<source>Share</source>
|
||||
<target>Teilen</target>
|
||||
<note>alert action
|
||||
chat item action</note>
|
||||
chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share 1-time link" xml:space="preserve">
|
||||
<source>Share 1-time link</source>
|
||||
|
@ -7188,11 +7188,6 @@ chat item action</note>
|
|||
<target>Mit Kontakten teilen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Short link" xml:space="preserve">
|
||||
<source>Short link</source>
|
||||
<target>Verkürzter Link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<target>QR-Code anzeigen</target>
|
||||
|
@ -7293,11 +7288,6 @@ chat item action</note>
|
|||
<target>SimpleX-Adresse oder Einmal-Link?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX channel link" xml:space="preserve">
|
||||
<source>SimpleX channel link</source>
|
||||
<target>SimpleX-Kanal-Link</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX contact address" xml:space="preserve">
|
||||
<source>SimpleX contact address</source>
|
||||
<target>SimpleX-Kontaktadressen-Link</target>
|
||||
|
@ -7404,7 +7394,7 @@ chat item action</note>
|
|||
<source>Spam</source>
|
||||
<target>Spam</target>
|
||||
<note>blocking reason
|
||||
report reason</note>
|
||||
report reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Square, circle, or anything in between." xml:space="preserve">
|
||||
<source>Square, circle, or anything in between.</source>
|
||||
|
@ -7463,17 +7453,17 @@ report reason</note>
|
|||
</trans-unit>
|
||||
<trans-unit id="Stop file" xml:space="preserve">
|
||||
<source>Stop file</source>
|
||||
<target>Herunterladen beenden</target>
|
||||
<target>Datei beenden</target>
|
||||
<note>cancel file action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop receiving file?" xml:space="preserve">
|
||||
<source>Stop receiving file?</source>
|
||||
<target>Das Herunterladen der Datei beenden?</target>
|
||||
<target>Den Empfang der Datei beenden?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop sending file?" xml:space="preserve">
|
||||
<source>Stop sending file?</source>
|
||||
<target>Das Hochladen der Datei beenden?</target>
|
||||
<target>Das Senden der Datei beenden?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop sharing" xml:space="preserve">
|
||||
|
@ -7763,11 +7753,21 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
|
|||
<target>Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The profile is only shared with your contacts." xml:space="preserve">
|
||||
<source>The profile is only shared with your contacts.</source>
|
||||
<target>Das Profil wird nur mit Ihren Kontakten geteilt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve">
|
||||
<source>The same conditions will apply to operator **%@**.</source>
|
||||
<target>Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The same conditions will apply to operator(s): **%@**." xml:space="preserve">
|
||||
<source>The same conditions will apply to operator(s): **%@**.</source>
|
||||
<target>Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The second preset operator in the app!" xml:space="preserve">
|
||||
<source>The second preset operator in the app!</source>
|
||||
<target>Der zweite voreingestellte Netzwerk-Betreiber in der App!</target>
|
||||
|
@ -7825,7 +7825,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
|
|||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve">
|
||||
<source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source>
|
||||
<target>Es werden alle herunter- und hochgeladenen Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden!</target>
|
||||
<target>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!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve">
|
||||
|
@ -7883,11 +7883,6 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
|
|||
<target>Das ist Ihr eigener Einmal-Link!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve">
|
||||
<source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source>
|
||||
<target>Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve">
|
||||
<source>This link was used with another mobile device, please create a new link on the desktop.</source>
|
||||
<target>Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App.</target>
|
||||
|
@ -8187,11 +8182,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||
<target>Ungelesen</target>
|
||||
<note>swipe action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unsupported connection link" xml:space="preserve">
|
||||
<source>Unsupported connection link</source>
|
||||
<target>Verbindungs-Link wird nicht unterstützt</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
|
||||
<source>Up to 100 last messages are sent to new members.</source>
|
||||
<target>Bis zu 100 der letzten Nachrichten werden an neue Mitglieder gesendet.</target>
|
||||
|
@ -8287,11 +8277,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||
<target>Solange kein Port konfiguriert ist, wird TCP-Port %@ genutzt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve">
|
||||
<source>Use TCP port 443 for preset servers only.</source>
|
||||
<target>TCP-Port 443 nur für voreingestellte Server verwenden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use chat" xml:space="preserve">
|
||||
<source>Use chat</source>
|
||||
<target>Verwenden Sie Chat</target>
|
||||
|
@ -8357,11 +8342,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||
<target>Verwende Server</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use short links (BETA)" xml:space="preserve">
|
||||
<source>Use short links (BETA)</source>
|
||||
<target>Kurze Links verwenden (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use the app while in the call." xml:space="preserve">
|
||||
<source>Use the app while in the call.</source>
|
||||
<target>Die App kann während eines Anrufs genutzt werden.</target>
|
||||
|
@ -8444,12 +8424,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||
</trans-unit>
|
||||
<trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve">
|
||||
<source>Video will be received when your contact completes uploading it.</source>
|
||||
<target>Das Video wird heruntergeladen, sobald Ihr Kontakt das Hochladen beendet hat.</target>
|
||||
<target>Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve">
|
||||
<source>Video will be received when your contact is online, please wait or check later!</source>
|
||||
<target>Das Video wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</target>
|
||||
<target>Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
|
||||
|
@ -8657,6 +8637,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||
<target>XFTP-Server</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Yes" xml:space="preserve">
|
||||
<source>Yes</source>
|
||||
<target>Ja</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
|
||||
<source>You **must not** use the same database on two devices.</source>
|
||||
<target>Sie dürfen die selbe Datenbank **nicht** auf zwei Geräten nutzen.</target>
|
||||
|
@ -8749,6 +8734,11 @@ Verbindungsanfrage wiederholen?</target>
|
|||
<target>Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can configure operators in Network & servers settings." xml:space="preserve">
|
||||
<source>You can configure operators in Network & servers settings.</source>
|
||||
<target>Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can configure servers via settings." xml:space="preserve">
|
||||
<source>You can configure servers via settings.</source>
|
||||
<target>Sie können die Server über die Einstellungen konfigurieren.</target>
|
||||
|
@ -8991,6 +8981,11 @@ Verbindungsanfrage wiederholen?</target>
|
|||
<target>Ihre ICE-Server</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your SMP servers" xml:space="preserve">
|
||||
<source>Your SMP servers</source>
|
||||
<target>Ihre SMP-Server</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your SimpleX address" xml:space="preserve">
|
||||
<source>Your SimpleX address</source>
|
||||
<target>Ihre SimpleX-Adresse</target>
|
||||
|
@ -9076,11 +9071,6 @@ Verbindungsanfrage wiederholen?</target>
|
|||
<target>Ihr Profil **%@** wird geteilt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and only shared with your contacts.</source>
|
||||
<target>Das Profil wird nur mit Ihren Kontakten geteilt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
|
||||
<target>Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
|
@ -9091,6 +9081,11 @@ Verbindungsanfrage wiederholen?</target>
|
|||
<target>Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet.</target>
|
||||
<note>alert message</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
|
||||
<source>Your profile, contacts and delivered messages are stored on your device.</source>
|
||||
<target>Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your random profile" xml:space="preserve">
|
||||
<source>Your random profile</source>
|
||||
<target>Ihr Zufallsprofil</target>
|
||||
|
@ -9229,8 +9224,7 @@ Verbindungsanfrage wiederholen?</target>
|
|||
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||
<source>blocked by admin</source>
|
||||
<target>wurde vom Administrator blockiert</target>
|
||||
<note>blocked chat item
|
||||
marked deleted chat item preview text</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bold" xml:space="preserve">
|
||||
<source>bold</source>
|
||||
|
@ -9401,7 +9395,7 @@ marked deleted chat item preview text</note>
|
|||
<source>default (%@)</source>
|
||||
<target>Default (%@)</target>
|
||||
<note>delete after time
|
||||
pref value</note>
|
||||
pref value</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="default (no)" xml:space="preserve">
|
||||
<source>default (no)</source>
|
||||
|
@ -9528,11 +9522,21 @@ pref value</note>
|
|||
<target>Fehler</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="event happened" xml:space="preserve">
|
||||
<source>event happened</source>
|
||||
<target>event happened</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="expired" xml:space="preserve">
|
||||
<source>expired</source>
|
||||
<target>Abgelaufen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="for better metadata privacy." xml:space="preserve">
|
||||
<source>for better metadata privacy.</source>
|
||||
<target>für einen besseren Metadatenschutz.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="forwarded" xml:space="preserve">
|
||||
<source>forwarded</source>
|
||||
<target>weitergeleitet</target>
|
||||
|
@ -9742,8 +9746,8 @@ pref value</note>
|
|||
<source>off</source>
|
||||
<target>Aus</target>
|
||||
<note>enabled status
|
||||
group pref value
|
||||
time to disappear</note>
|
||||
group pref value
|
||||
time to disappear</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="offered %@" xml:space="preserve">
|
||||
<source>offered %@</source>
|
||||
|
@ -10113,7 +10117,7 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
|||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
|
@ -10143,14 +10147,14 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
|||
</trans-unit>
|
||||
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
|
||||
<source>SimpleX needs access to Photo Library for saving captured and received media</source>
|
||||
<target>SimpleX benötigt Zugriff auf das Fotoalbum, um selbst gemachte oder heruntergeladene Bilder zu speichern</target>
|
||||
<target>SimpleX benötigt Zugriff auf das Fotoalbum, um selbst gemachte oder empfangene Bilder zu speichern</target>
|
||||
<note>Privacy - Photo Library Additions Usage Description</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
@ -10172,7 +10176,7 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
|||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="%d new events" xml:space="preserve">
|
||||
|
@ -10180,11 +10184,6 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
|||
<target>%d neue Ereignisse</target>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="From %d chat(s)" xml:space="preserve">
|
||||
<source>From %d chat(s)</source>
|
||||
<target>Von %d Chat(s)</target>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="From: %@" xml:space="preserve">
|
||||
<source>From: %@</source>
|
||||
<target>Von: %@</target>
|
||||
|
@ -10200,11 +10199,16 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
|||
<target>Neue Nachrichten</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New messages in %d chats" xml:space="preserve">
|
||||
<source>New messages in %d chats</source>
|
||||
<target>Neue Nachrichten in %d Chats</target>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
@ -10226,7 +10230,7 @@ Zuletzt empfangene Nachricht: %2$@</target>
|
|||
</file>
|
||||
<file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="%@" xml:space="preserve">
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "de",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "16C5032a",
|
||||
"toolBuildNumber" : "15F31d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "16.2"
|
||||
"toolVersion" : "15.4"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
|
@ -3043,8 +3043,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||
<source>The old database was not removed during the migration, it can be deleted.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve">
|
||||
<source>Your profile is stored on your device and only shared with your contacts.</source>
|
||||
<trans-unit id="The profile is only shared with your contacts." xml:space="preserve">
|
||||
<source>The profile is only shared with your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The sender will NOT be notified" xml:space="preserve">
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue