Compare commits

..

10 Commits

Author SHA1 Message Date
Evgeny Poberezkin 503d3d77e6 Merge branch 'master' into master-ghc8107 2023-09-23 08:47:28 +01:00
Evgeny Poberezkin 81bd7d97c5 Merge branch 'master' into master-ghc8107 2023-09-22 17:21:54 +01:00
Evgeny Poberezkin 8f57925067 compatibility with GHC 8.10.7 2023-09-22 14:01:25 +01:00
Evgeny Poberezkin 9bf99db82e Merge branch 'master' into master-ghc8107 2023-09-22 13:46:50 +01:00
Evgeny Poberezkin 5615cdbf1a Merge branch 'master' into master-android 2023-09-21 17:04:47 +01:00
Evgeny Poberezkin d802ae0058 Merge branch 'master' into master-android 2023-09-21 12:06:10 +01:00
Evgeny Poberezkin 8f2278198c Merge branch 'master' into master-android 2023-09-20 14:55:25 +01:00
spaced4ndy 10937a5a4e Merge branch 'master' into master-android 2023-09-20 17:36:53 +04:00
Evgeny Poberezkin 6aff6e9804 Merge branch 'master-ghc9' into master-ghc8107 2023-09-18 21:56:35 +01:00
Evgeny Poberezkin 95477cae7e core: use commit from simplexmq branch master-ghc8107 2023-09-18 21:45:50 +01:00
516 changed files with 8925 additions and 36659 deletions
+7 -65
View File
@@ -8,7 +8,6 @@ on:
- users - users
tags: tags:
- "v*" - "v*"
- "!*-fdroid"
pull_request: pull_request:
jobs: jobs:
@@ -79,10 +78,10 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Haskell - name: Setup Haskell
uses: haskell-actions/setup@v2 uses: haskell/actions/setup@v2
with: with:
ghc-version: "9.6.3" ghc-version: "8.10.7"
cabal-version: "3.10.1.0" cabal-version: "latest"
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@v3 uses: actions/cache@v3
@@ -188,7 +187,7 @@ jobs:
APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }} APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }}
APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }} APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }}
run: | run: |
scripts/ci/build-desktop-mac.sh scripts/build-desktop-mac.sh
path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg) path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg)
echo "package_path=$path" >> $GITHUB_OUTPUT echo "package_path=$path" >> $GITHUB_OUTPUT
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
@@ -259,40 +258,15 @@ jobs:
# Unix / # Unix /
# / Windows # / 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: 'Setup MSYS2'
if: matrix.os == 'windows-latest'
uses: msys2/setup-msys2@v2
with:
msystem: ucrt64
update: true
install: >-
git
perl
make
pacboy: >-
toolchain:p
cmake:p
# * In powershell multiline commands do not fail if individual commands fail - https://github.community/t/multiline-commands-on-windows-do-not-fail-if-individual-commands-fail/16753
# * And GitHub Actions does not support parameterizing shell in a matrix job - https://github.community/t/using-matrix-to-specify-shell-is-it-possible/17065
- name: Windows build - name: Windows build
id: windows_build id: windows_build
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
shell: msys2 {0} shell: cmd
run: | run: |
export PATH=$PATH:/c/ghcup/bin
scripts/desktop/prepare-openssl-windows.sh
openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g')
rm cabal.project.local 2>/dev/null || true
echo "ignore-project: False" >> cabal.project.local
echo "package direct-sqlcipher" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local
echo " extra-include-dirs: $openssl_windows_style_path\include" >> cabal.project.local
echo " extra-lib-dirs: $openssl_windows_style_path" >> cabal.project.local
rm -rf dist-newstyle/src/direct-sq*
sed -i "s/, unix /--, unix /" simplex-chat.cabal
cabal build --enable-tests cabal build --enable-tests
rm -rf dist-newstyle/src/direct-sq* rm -rf dist-newstyle/src/direct-sq*
path=$(cabal list-bin simplex-chat | tail -n 1) path=$(cabal list-bin simplex-chat | tail -n 1)
@@ -318,36 +292,4 @@ jobs:
body: | body: |
${{ steps.windows_build.outputs.bin_hash }} ${{ steps.windows_build.outputs.bin_hash }}
- name: Windows build desktop
id: windows_desktop_build
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
shell: msys2 {0}
run: |
export PATH=$PATH:/c/ghcup/bin
scripts/desktop/build-lib-windows.sh
cd apps/multiplatform
./gradlew packageMsi
path=$(echo $PWD/release/main/msi/*imple*.msi | sed 's#/\([a-z]\)#\1:#' | sed 's#/#\\#g')
echo "package_path=$path" >> $GITHUB_OUTPUT
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
- name: Windows upload desktop package to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.windows_desktop_build.outputs.package_path }}
asset_name: ${{ matrix.desktop_asset_name }}
tag: ${{ github.ref }}
- 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 }}
# Windows / # Windows /
-1
View File
@@ -9,7 +9,6 @@ on:
- website/** - website/**
- images/** - images/**
- blog/** - blog/**
- docs/**
- .github/workflows/web.yml - .github/workflows/web.yml
jobs: jobs:
+4 -4
View File
@@ -8,12 +8,12 @@ RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/
chmod +x /usr/bin/ghcup chmod +x /usr/bin/ghcup
# Install ghc # Install ghc
RUN ghcup install ghc 9.6.3 RUN ghcup install ghc 8.10.7
# Install cabal # Install cabal
RUN ghcup install cabal 3.10.1.0 RUN ghcup install cabal
# Set both as default # Set both as default
RUN ghcup set ghc 9.6.3 && \ RUN ghcup set ghc 8.10.7 && \
ghcup set cabal 3.10.1.0 ghcup set cabal
COPY . /project COPY . /project
WORKDIR /project WORKDIR /project
+21 -26
View File
@@ -119,22 +119,19 @@ Join our translators to help SimpleX grow!
|locale|language |contributor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[website](https://simplex.chat)|Github docs| |locale|language |contributor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[website](https://simplex.chat)|Github docs|
|:----:|:-------:|:---------:|:---------:|:---------:|:---------:| |:----:|:-------:|:---------:|:---------:|:---------:|:---------:|
|🇬🇧 en|English | |✓|✓|✓|✓| |🇬🇧 en|English | |✓|✓|✓|✓|
|ar|العربية |[jermanuts](https://github.com/jermanuts)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/ar/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ar/)<br>-|[![website](https://hosted.weblate.org/widgets/simplex-chat/ar/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ar/)|| |ar|العربية |[jermanuts](https://github.com/jermanuts)||[![website](https://hosted.weblate.org/widgets/simplex-chat/ar/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ar/)||
|🇧🇬 bg|Български | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/bg/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/bg/)<br>[![ios app](https://hosted.weblate.org/widget/simplex-chat/ios/bg/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/bg/)||| |🇧🇬 bg|Български |-|[![android app](https://hosted.weblate.org/widgets/simplex-chat/bg/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/bg/)<br>-|||
|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/cs/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/cs/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/cs/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)| |🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/cs/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/cs/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/cs/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)|
|🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/de/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/de/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/de/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/de/)|| |🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/de/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/de/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/de/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/de/)||
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)|| |🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|🇫🇮 fi|Suomi | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/fi/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fi/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fi/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fi/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fi/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fi/)||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)| |🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|🇮🇱 he|עִברִית | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/he/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/he/)<br>-|||
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)|| |🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|🇯🇵 ja|日本語 | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/ja/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/ja/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ja/)|| |🇯🇵 ja|Japanese ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ja/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)|| |🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/pl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)||| |🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/pl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)|| |🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)||| |🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/th/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/th/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/th/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/th/)||| |🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/th/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/th/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/th/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/th/)|||
|🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)|| |🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
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! 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!
@@ -230,20 +227,24 @@ You can use SimpleX with your own servers and still communicate with people usin
## News and updates ## News and updates
Recent and important updates: Recent updates:
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). [July 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md). [May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). [Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
[Mar 28, 2023. v4.6 released - with Android 8+ and ARMv7a support, hidden profiles, community moderation, improved audio/video calls and reduced battery usage](./blog/20230328-simplex-chat-v4-6-hidden-profiles.md).
[Mar 1, 2023. SimpleX File Transfer Protocol send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md). [Mar 1, 2023. SimpleX File Transfer Protocol send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
[Feb 4, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
[Jan 3, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
[Dec 6, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md).
[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). [Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md). [Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
@@ -289,20 +290,18 @@ What is already implemented:
3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message. 3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well). 4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks. 5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
6. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed. 6. Starting from v2 of SMP protocol (the current version is v4) all message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key. 8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details. 9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings. 10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections. 11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
12. Manual messaging queue rotations to move conversation to another SMP relay. 12. Manual messaging queue rotations to move conversation to another SMP relay.
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
14. Local files encryption, except videos (to be added later).
We plan to add: We plan to add:
1. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party. 1. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`. This is currently in progress.
2. Post-quantum resistant key exchange in double ratchet protocol. 2. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days). 3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time. 4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
5. Reproducible builds this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code. 5. Reproducible builds this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
@@ -366,26 +365,22 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Message editing history - ✅ Message editing history
- ✅ Reduced battery and traffic usage in large groups. - ✅ Reduced battery and traffic usage in large groups.
- ✅ Message delivery confirmation (with sender opt-out per contact). - ✅ Message delivery confirmation (with sender opt-out per contact).
- Desktop client. - 🏗 Desktop client.
- ✅ Encryption of local files stored in the app.
- ✅ Using mobile profiles from the desktop app.
- 🏗 Improve experience for the new users.
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
- 🏗 Large groups, communities and public channels.
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- Privacy & security slider - a simple way to set all settings at once.
- Improve sending videos (including encryption of locally stored videos).
- SMP queue redundancy and rotation (manual is supported). - SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address. - Include optional message into connection request sent via contact address.
- Local app files encryption.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.). - Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
- Large groups, communities and public channels.
- Feeds/broadcasts. - Feeds/broadcasts.
- Ephemeral/disappearing/OTR conversations with the existing contacts. - Ephemeral/disappearing/OTR conversations with the existing contacts.
- Privately share your location. - Privately share your location.
- Web widgets for custom interactivity in the chats. - Web widgets for custom interactivity in the chats.
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.). - Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
- Supporting the same profile on multiple devices.
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages: - Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
- keep all your contacts and groups even if you lose the domain. - keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups. - the server doesn't have information about your contacts and groups.
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- High capacity multi-node SMP relays. - High capacity multi-node SMP relays.
## Disclaimers ## Disclaimers
+1 -4
View File
@@ -55,6 +55,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
didReceiveRemoteNotification userInfo: [AnyHashable : Any], didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
logger.debug("AppDelegate: didReceiveRemoteNotification") logger.debug("AppDelegate: didReceiveRemoteNotification")
print("*** userInfo", userInfo)
let m = ChatModel.shared let m = ChatModel.shared
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
m.notificationMode != .off { m.notificationMode != .off {
@@ -120,10 +121,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
BGManager.shared.receiveMessages(complete) BGManager.shared.receiveMessages(complete)
} }
static func keepScreenOn(_ on: Bool) {
UIApplication.shared.isIdleTimerDisabled = on
}
} }
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
+20 -12
View File
@@ -31,11 +31,11 @@ struct ContentView: View {
@State private var chatListActionSheet: ChatListActionSheet? = nil @State private var chatListActionSheet: ChatListActionSheet? = nil
private enum ChatListActionSheet: Identifiable { private enum ChatListActionSheet: Identifiable {
case planAndConnectSheet(sheet: PlanAndConnectActionSheet) case connectViaUrl(action: ConnReqType, link: String)
var id: String { var id: String {
switch self { switch self {
case let .planAndConnectSheet(sheet): return sheet.id case let .connectViaUrl(_, link): return "connectViaUrl \(link)"
} }
} }
} }
@@ -93,7 +93,7 @@ struct ContentView: View {
mainView() mainView()
.actionSheet(item: $chatListActionSheet) { sheet in .actionSheet(item: $chatListActionSheet) { sheet in
switch sheet { switch sheet {
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false) case let .connectViaUrl(action, link): return connectViaUrlSheet(action, link)
} }
} }
} else { } else {
@@ -290,16 +290,12 @@ struct ContentView: View {
if let url = m.appOpenUrl { if let url = m.appOpenUrl {
m.appOpenUrl = nil m.appOpenUrl = nil
var path = url.path var path = url.path
logger.debug("ContentView.connectViaUrl path: \(path)")
if (path == "/contact" || path == "/invitation") { if (path == "/contact" || path == "/invitation") {
path.removeFirst() path.removeFirst()
let action: ConnReqType = path == "contact" ? .contact : .invitation
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
planAndConnect( chatListActionSheet = .connectViaUrl(action: action, link: link)
link,
showAlert: showPlanAndConnectAlert,
showActionSheet: { chatListActionSheet = .planAndConnectSheet(sheet: $0) },
dismiss: false,
incognito: nil
)
} else { } else {
AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid"))) AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid")))
} }
@@ -307,8 +303,20 @@ struct ContentView: View {
} }
} }
private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) { private func connectViaUrlSheet(_ action: ConnReqType, _ link: String) -> ActionSheet {
AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false)) let title: LocalizedStringKey
switch action {
case .contact: title = "Connect via contact link"
case .invitation: title = "Connect via one-time link"
}
return ActionSheet(
title: Text(title),
buttons: [
.default(Text("Use current profile")) { connectViaLink(link, incognito: false) },
.default(Text("Use new incognito profile")) { connectViaLink(link, incognito: true) },
.cancel()
]
)
} }
} }
-32
View File
@@ -46,7 +46,6 @@ class AudioRecorder {
audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH) audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH)
await MainActor.run { await MainActor.run {
AppDelegate.keepScreenOn(true)
recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
guard let time = self.audioRecorder?.currentTime else { return } guard let time = self.audioRecorder?.currentTime else { return }
self.onTimer?(time) self.onTimer?(time)
@@ -58,10 +57,6 @@ class AudioRecorder {
} }
return nil return nil
} catch let error { } catch let error {
await MainActor.run {
AppDelegate.keepScreenOn(false)
}
try? av.setCategory(AVAudioSession.Category.soloAmbient)
logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)") logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)")
return .error(error.localizedDescription) return .error(error.localizedDescription)
} }
@@ -76,8 +71,6 @@ class AudioRecorder {
timer.invalidate() timer.invalidate()
} }
recordingTimer = nil recordingTimer = nil
AppDelegate.keepScreenOn(false)
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.soloAmbient)
} }
private func checkPermission() async -> Bool { private func checkPermission() async -> Bool {
@@ -128,19 +121,14 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
if self.audioPlayer?.isPlaying ?? false { if self.audioPlayer?.isPlaying ?? false {
AppDelegate.keepScreenOn(true)
guard let time = self.audioPlayer?.currentTime else { return } guard let time = self.audioPlayer?.currentTime else { return }
self.onTimer?(time) self.onTimer?(time)
AudioPlayer.changeAudioSession(true)
} else {
AudioPlayer.changeAudioSession(false)
} }
} }
} }
func pause() { func pause() {
audioPlayer?.pause() audioPlayer?.pause()
AppDelegate.keepScreenOn(false)
} }
func play() { func play() {
@@ -161,8 +149,6 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
func stop() { func stop() {
if let player = audioPlayer { if let player = audioPlayer {
player.stop() player.stop()
AppDelegate.keepScreenOn(false)
AudioPlayer.changeAudioSession(false)
} }
audioPlayer = nil audioPlayer = nil
if let timer = playbackTimer { if let timer = playbackTimer {
@@ -171,24 +157,6 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
playbackTimer = nil playbackTimer = nil
} }
static func changeAudioSession(_ playback: Bool) {
// When there is a audio recording, setting any other category will disable sound
if AVAudioSession.sharedInstance().category == .playAndRecord {
return
}
if playback {
if AVAudioSession.sharedInstance().category != .playback {
logger.log("AudioSession: playback")
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: .duckOthers)
}
} else {
if AVAudioSession.sharedInstance().category != .soloAmbient {
logger.log("AudioSession: soloAmbient")
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.soloAmbient)
}
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
stop() stop()
self.onFinishPlayback?() self.onFinishPlayback?()
+62 -139
View File
@@ -62,9 +62,8 @@ final class ChatModel: ObservableObject {
// current chat // current chat
@Published var chatId: String? @Published var chatId: String?
@Published var reversedChatItems: [ChatItem] = [] @Published var reversedChatItems: [ChatItem] = []
var chatItemStatuses: Dictionary<Int64, CIStatus> = [:]
@Published var chatToTop: String? @Published var chatToTop: String?
@Published var groupMembers: [GMember] = [] @Published var groupMembers: [GroupMember] = []
// items in the terminal view // items in the terminal view
@Published var showingTerminal = false @Published var showingTerminal = false
@Published var terminalItems: [TerminalItem] = [] @Published var terminalItems: [TerminalItem] = []
@@ -85,8 +84,6 @@ final class ChatModel: ObservableObject {
@Published var activeCall: Call? @Published var activeCall: Call?
@Published var callCommand: WCallCommand? @Published var callCommand: WCallCommand?
@Published var showCallView = false @Published var showCallView = false
// remote desktop
@Published var remoteCtrlSession: RemoteCtrlSession?
// currently showing QR code // currently showing QR code
@Published var connReqInv: String? @Published var connReqInv: String?
// audio recording and playback // audio recording and playback
@@ -112,10 +109,6 @@ final class ChatModel: ObservableObject {
notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get() notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
} }
var activeRemoteCtrl: Bool {
remoteCtrlSession?.active ?? false
}
func getUser(_ userId: Int64) -> User? { func getUser(_ userId: Int64) -> User? {
currentUser?.userId == userId currentUser?.userId == userId
? currentUser ? currentUser
@@ -159,20 +152,6 @@ final class ChatModel: ObservableObject {
} }
} }
func getGroupChat(_ groupId: Int64) -> Chat? {
chats.first { chat in
if case let .group(groupInfo) = chat.chatInfo {
return groupInfo.groupId == groupId
} else {
return false
}
}
}
func getGroupMember(_ groupMemberId: Int64) -> GMember? {
groupMembers.first { $0.groupMemberId == groupMemberId }
}
private func getChatIndex(_ id: String) -> Int? { private func getChatIndex(_ id: String) -> Int? {
chats.firstIndex(where: { $0.id == id }) chats.firstIndex(where: { $0.id == id })
} }
@@ -186,7 +165,6 @@ final class ChatModel: ObservableObject {
func updateChatInfo(_ cInfo: ChatInfo) { func updateChatInfo(_ cInfo: ChatInfo) {
if let i = getChatIndex(cInfo.id) { if let i = getChatIndex(cInfo.id) {
chats[i].chatInfo = cInfo chats[i].chatInfo = cInfo
chats[i].created = Date.now
} }
} }
@@ -200,7 +178,7 @@ final class ChatModel: ObservableObject {
func updateContactConnectionStats(_ contact: Contact, _ connectionStats: ConnectionStats) { func updateContactConnectionStats(_ contact: Contact, _ connectionStats: ConnectionStats) {
var updatedConn = contact.activeConn var updatedConn = contact.activeConn
updatedConn?.connectionStats = connectionStats updatedConn.connectionStats = connectionStats
var updatedContact = contact var updatedContact = contact
updatedContact.activeConn = updatedConn updatedContact.activeConn = updatedConn
updateContact(updatedContact) updateContact(updatedContact)
@@ -318,11 +296,7 @@ final class ChatModel: ObservableObject {
return false return false
} else { } else {
withAnimation(itemAnimation()) { withAnimation(itemAnimation()) {
var ci = cItem reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
ci.meta.itemStatus = status
}
reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
} }
return true return true
} }
@@ -335,22 +309,26 @@ final class ChatModel: ObservableObject {
} }
} }
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) { func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) { if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
withAnimation { withAnimation {
_updateChatItem(at: i, with: cItem) _updateChatItem(at: i, with: cItem)
} }
} else if let status = status {
chatItemStatuses.updateValue(status, forKey: cItem.id)
} }
} }
private func _updateChatItem(at i: Int, with cItem: ChatItem) { private func _updateChatItem(at i: Int, with cItem: ChatItem) {
let ci = reversedChatItems[i]
reversedChatItems[i] = cItem reversedChatItems[i] = cItem
reversedChatItems[i].viewTimestamp = .now reversedChatItems[i].viewTimestamp = .now
// on some occasions the confirmation of message being accepted by the server (tick)
// arrives earlier than the response from API, and item remains without tick
if case .sndNew = cItem.meta.itemStatus {
reversedChatItems[i].meta.itemStatus = ci.meta.itemStatus
}
} }
func getChatItemIndex(_ cItem: ChatItem) -> Int? { private func getChatItemIndex(_ cItem: ChatItem) -> Int? {
reversedChatItems.firstIndex(where: { $0.id == cItem.id }) reversedChatItems.firstIndex(where: { $0.id == cItem.id })
} }
@@ -486,7 +464,6 @@ final class ChatModel: ObservableObject {
} }
// clear current chat // clear current chat
if chatId == cInfo.id { if chatId == cInfo.id {
chatItemStatuses = [:]
reversedChatItems = [] reversedChatItems = []
} }
} }
@@ -539,62 +516,27 @@ final class ChatModel: ObservableObject {
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount }) users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
} }
// this function analyses "connected" events and assumes that each member will be there only once func getConnectedMemberNames(_ ci: ChatItem) -> [String] {
func getConnectedMemberNames(_ chatItem: ChatItem) -> (Int, [String]) { guard var i = getChatItemIndex(ci) else { return [] }
var count = 0
var ns: [String] = [] var ns: [String] = []
if let ciCategory = chatItem.mergeCategory, while i < reversedChatItems.count, let m = reversedChatItems[i].memberConnected {
var i = getChatItemIndex(chatItem) { ns.append(m.displayName)
while i < reversedChatItems.count { i += 1
let ci = reversedChatItems[i]
if ci.mergeCategory != ciCategory { break }
if let m = ci.memberConnected {
ns.append(m.displayName)
}
count += 1
i += 1
}
} }
return (count, ns) return ns
} }
// returns the index of the passed item and the next item (it has smaller index) func getChatItemNeighbors(_ ci: ChatItem) -> (ChatItem?, ChatItem?) {
func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) { if let i = getChatItemIndex(ci) {
if let i = getChatItemIndex(ci) { return (
(i, i > 0 ? reversedChatItems[i - 1] : nil) i + 1 < reversedChatItems.count ? reversedChatItems[i + 1] : nil,
i - 1 >= 0 ? reversedChatItems[i - 1] : nil
)
} else { } else {
(nil, nil) return (nil, nil)
} }
} }
// returns the index of the first item in the same merged group (the first hidden item)
// and the previous visible item with another merge category
func getPrevShownChatItem(_ ciIndex: Int?, _ ciCategory: CIMergeCategory?) -> (Int?, ChatItem?) {
guard var i = ciIndex else { return (nil, nil) }
let fst = reversedChatItems.count - 1
while i < fst {
i = i + 1
let ci = reversedChatItems[i]
if ciCategory == nil || ciCategory != ci.mergeCategory {
return (i - 1, ci)
}
}
return (i, nil)
}
// returns the previous member in the same merge group and the count of members in this group
func getPrevHiddenMember(_ member: GroupMember, _ range: ClosedRange<Int>) -> (GroupMember?, Int) {
var prevMember: GroupMember? = nil
var memberIds: Set<Int64> = []
for i in range {
if case let .groupRcv(m) = reversedChatItems[i].chatDir {
if prevMember == nil && m.groupMemberId != member.groupMemberId { prevMember = m }
memberIds.insert(m.groupMemberId)
}
}
return (prevMember, memberIds.count)
}
func popChat(_ id: String) { func popChat(_ id: String) {
if let i = getChatIndex(id) { if let i = getChatIndex(id) {
popChat_(i) popChat_(i)
@@ -629,14 +571,13 @@ final class ChatModel: ObservableObject {
} }
// update current chat // update current chat
if chatId == groupInfo.id { if chatId == groupInfo.id {
if let i = groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) { if let i = groupMembers.firstIndex(where: { $0.id == member.id }) {
withAnimation(.default) { withAnimation(.default) {
self.groupMembers[i].wrapped = member self.groupMembers[i] = member
self.groupMembers[i].created = Date.now
} }
return false return false
} else { } else {
withAnimation { groupMembers.append(GMember(member)) } withAnimation { groupMembers.append(member) }
return true return true
} }
} else { } else {
@@ -645,10 +586,11 @@ final class ChatModel: ObservableObject {
} }
func updateGroupMemberConnectionStats(_ groupInfo: GroupInfo, _ member: GroupMember, _ connectionStats: ConnectionStats) { func updateGroupMemberConnectionStats(_ groupInfo: GroupInfo, _ member: GroupMember, _ connectionStats: ConnectionStats) {
if var conn = member.activeConn { if let conn = member.activeConn {
conn.connectionStats = connectionStats var updatedConn = conn
updatedConn.connectionStats = connectionStats
var updatedMember = member var updatedMember = member
updatedMember.activeConn = conn updatedMember.activeConn = updatedConn
_ = upsertGroupMember(groupInfo, updatedMember) _ = upsertGroupMember(groupInfo, updatedMember)
} }
} }
@@ -677,17 +619,11 @@ final class ChatModel: ObservableObject {
} }
func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) { func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
if let conn = contact.activeConn { networkStatuses[contact.activeConn.agentConnId] = status
networkStatuses[conn.agentConnId] = status
}
} }
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
if let conn = contact.activeConn { networkStatuses[contact.activeConn.agentConnId] ?? .unknown
networkStatuses[conn.agentConnId] ?? .unknown
} else {
.unknown
}
} }
} }
@@ -753,53 +689,40 @@ final class Chat: ObservableObject, Identifiable {
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
} }
final class GMember: ObservableObject, Identifiable { enum NetworkStatus: Decodable, Equatable {
@Published var wrapped: GroupMember case unknown
var created = Date.now case connected
case disconnected
case error(String)
init(_ member: GroupMember) { var statusString: LocalizedStringKey {
self.wrapped = member get {
switch self {
case .connected: return "connected"
case .error: return "error"
default: return "connecting"
}
}
} }
var id: String { wrapped.id } var statusExplanation: LocalizedStringKey {
var groupId: Int64 { wrapped.groupId } get {
var groupMemberId: Int64 { wrapped.groupMemberId } switch self {
var displayName: String { wrapped.displayName } case .connected: return "You are connected to the server used to receive messages from this contact."
var viewId: String { get { "\(wrapped.id) \(created.timeIntervalSince1970)" } } case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
static let sampleData = GMember(GroupMember.sampleData) default: return "Trying to connect to the server used to receive messages from this contact."
} }
}
struct RemoteCtrlSession {
var ctrlAppInfo: CtrlAppInfo?
var appVersion: String
var sessionState: UIRemoteCtrlSessionState
func updateState(_ state: UIRemoteCtrlSessionState) -> RemoteCtrlSession {
RemoteCtrlSession(ctrlAppInfo: ctrlAppInfo, appVersion: appVersion, sessionState: state)
} }
var active: Bool { var imageName: String {
if case .connected = sessionState { true } else { false } get {
} switch self {
case .unknown: return "circle.dotted"
var discovery: Bool { case .connected: return "circle.fill"
if case .searching = sessionState { true } else { false } case .disconnected: return "ellipsis.circle.fill"
} case .error: return "exclamationmark.circle.fill"
}
var sessionCode: String? {
switch sessionState {
case let .pendingConfirmation(_, sessionCode): sessionCode
case let .connected(_, sessionCode): sessionCode
default: nil
} }
} }
} }
enum UIRemoteCtrlSessionState {
case starting
case searching
case found(remoteCtrl: RemoteCtrlInfo, compatible: Bool)
case connecting(remoteCtrl_: RemoteCtrlInfo?)
case pendingConfirmation(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
case connected(remoteCtrl: RemoteCtrlInfo, sessionCode: String)
}
+34 -198
View File
@@ -257,12 +257,6 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
throw r throw r
} }
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return }
throw r
}
func apiExportArchive(config: ArchiveConfig) async throws { func apiExportArchive(config: ArchiveConfig) async throws {
try await sendCommandOkResp(.apiExportArchive(config: config)) try await sendCommandOkResp(.apiExportArchive(config: config))
} }
@@ -312,7 +306,6 @@ func loadChat(chat: Chat, search: String = "") {
do { do {
let cInfo = chat.chatInfo let cInfo = chat.chatInfo
let m = ChatModel.shared let m = ChatModel.shared
m.chatItemStatuses = [:]
m.reversedChatItems = [] m.reversedChatItems = []
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search)
m.updateChatInfo(chat.chatInfo) m.updateChatInfo(chat.chatInfo)
@@ -502,10 +495,6 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings)) try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
} }
func apiSetMemberSettings(_ groupId: Int64, _ groupMemberId: Int64, _ memberSettings: GroupMemberSettings) async throws {
try await sendCommandOkResp(.apiSetMemberSettings(groupId: groupId, groupMemberId: groupMemberId, memberSettings: memberSettings))
}
func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) { func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
let r = await chatSendCmd(.apiContactInfo(contactId: contactId)) let r = await chatSendCmd(.apiContactInfo(contactId: contactId))
if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) } if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
@@ -597,14 +586,6 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
throw r throw r
} }
func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
let userId = try currentUserId("apiConnectPlan")
let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq))
if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan }
logger.error("apiConnectPlan error: \(responseError(r))")
throw r
}
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? { func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq) let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
if let alert = alert { if let alert = alert {
@@ -629,7 +610,10 @@ func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert
if let c = m.getContactChat(contact.contactId) { if let c = m.getContactChat(contact.contactId) {
await MainActor.run { m.chatId = c.id } await MainActor.run { m.chatId = c.id }
} }
let alert = contactAlreadyExistsAlert(contact) let alert = mkAlert(
title: "Contact already exists",
message: "You are already connected to \(contact.displayName)."
)
return (nil, alert) return (nil, alert)
case .chatCmdError(_, .error(.invalidConnReq)): case .chatCmdError(_, .error(.invalidConnReq)):
let alert = mkAlert( let alert = mkAlert(
@@ -657,13 +641,6 @@ func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert
return (nil, alert) return (nil, alert)
} }
func contactAlreadyExistsAlert(_ contact: Contact) -> Alert {
mkAlert(
title: "Contact already exists",
message: "You are already connected to \(contact.displayName)."
)
}
private func connectionErrorAlert(_ r: ChatResponse) -> Alert { private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
if let networkErrorAlert = networkErrorAlert(r) { if let networkErrorAlert = networkErrorAlert(r) {
return networkErrorAlert return networkErrorAlert
@@ -675,30 +652,18 @@ private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
} }
} }
func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Contact?, Alert?) { func apiDeleteChat(type: ChatType, id: Int64) async throws {
guard let userId = ChatModel.shared.currentUser?.userId else { let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false)
logger.error("apiConnectContactViaAddress: no current user")
return (nil, nil)
}
let r = await chatSendCmd(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId))
if case let .sentInvitationToContact(_, contact, _) = r { return (contact, nil) }
logger.error("apiConnectContactViaAddress error: \(responseError(r))")
let alert = connectionErrorAlert(r)
return (nil, alert)
}
func apiDeleteChat(type: ChatType, id: Int64, notify: Bool? = nil) async throws {
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false)
if case .direct = type, case .contactDeleted = r { return } if case .direct = type, case .contactDeleted = r { return }
if case .contactConnection = type, case .contactConnectionDeleted = r { return } if case .contactConnection = type, case .contactConnectionDeleted = r { return }
if case .group = type, case .groupDeletedUser = r { return } if case .group = type, case .groupDeletedUser = r { return }
throw r throw r
} }
func deleteChat(_ chat: Chat, notify: Bool? = nil) async { func deleteChat(_ chat: Chat) async {
do { do {
let cInfo = chat.chatInfo let cInfo = chat.chatInfo
try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, notify: notify) try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId)
DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) } DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) }
} catch let error { } catch let error {
logger.error("deleteChat apiDeleteChat error: \(responseError(error))") logger.error("deleteChat apiDeleteChat error: \(responseError(error))")
@@ -736,9 +701,8 @@ func apiUpdateProfile(profile: Profile) async throws -> (Profile, [Contact])? {
let userId = try currentUserId("apiUpdateProfile") let userId = try currentUserId("apiUpdateProfile")
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile)) let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
switch r { switch r {
case .userProfileNoChange: return (profile, []) case .userProfileNoChange: return nil
case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts) case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts)
case .chatCmdError(_, .errorStore(.duplicateName)): return nil;
default: throw r default: throw r
} }
} }
@@ -905,46 +869,6 @@ func apiCancelFile(fileId: Int64) async -> AChatItem? {
} }
} }
func setLocalDeviceName(_ displayName: String) throws {
try sendCommandOkRespSync(.setLocalDeviceName(displayName: displayName))
}
func connectRemoteCtrl(desktopAddress: String) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) {
let r = await chatSendCmd(.connectRemoteCtrl(xrcpInvitation: desktopAddress))
if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) }
throw r
}
func findKnownRemoteCtrl() async throws {
try await sendCommandOkResp(.findKnownRemoteCtrl)
}
func confirmRemoteCtrl(_ rcId: Int64) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) {
let r = await chatSendCmd(.confirmRemoteCtrl(remoteCtrlId: rcId))
if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) }
throw r
}
func verifyRemoteCtrlSession(_ sessCode: String) async throws -> RemoteCtrlInfo {
let r = await chatSendCmd(.verifyRemoteCtrlSession(sessionCode: sessCode))
if case let .remoteCtrlConnected(rc) = r { return rc }
throw r
}
func listRemoteCtrls() throws -> [RemoteCtrlInfo] {
let r = chatSendCmdSync(.listRemoteCtrls)
if case let .remoteCtrlList(rcInfo) = r { return rcInfo }
throw r
}
func stopRemoteCtrl() async throws {
try await sendCommandOkResp(.stopRemoteCtrl)
}
func deleteRemoteCtrl(_ rcId: Int64) async throws {
try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId))
}
func networkErrorAlert(_ r: ChatResponse) -> Alert? { func networkErrorAlert(_ r: ChatResponse) -> Alert? {
switch r { switch r {
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
@@ -1020,12 +944,6 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
} }
} }
func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
let r = chatSendCmdSync(.apiGetNetworkStatuses)
if case let .networkStatuses(_, statuses) = r { return statuses }
throw r
}
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async { func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
do { do {
if chat.chatStats.unreadCount > 0 { if chat.chatStats.unreadCount > 0 {
@@ -1073,15 +991,9 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
throw r throw r
} }
private func sendCommandOkRespSync(_ cmd: ChatCommand) throws { func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
let r = chatSendCmdSync(cmd)
if case .cmdOk = r { return }
throw r
}
func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInfo {
let userId = try currentUserId("apiNewGroup") let userId = try currentUserId("apiNewGroup")
let r = chatSendCmdSync(.apiNewGroup(userId: userId, incognito: incognito, groupProfile: groupProfile)) let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
if case let .groupCreated(_, groupInfo) = r { return groupInfo } if case let .groupCreated(_, groupInfo) = r { return groupInfo }
throw r throw r
} }
@@ -1141,8 +1053,8 @@ func apiListMembers(_ groupId: Int64) async -> [GroupMember] {
return [] return []
} }
func filterMembersToAdd(_ ms: [GMember]) -> [Contact] { func filterMembersToAdd(_ ms: [GroupMember]) -> [Contact] {
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil } let memberContactIds = ms.compactMap{ m in m.memberCurrent ? m.memberContactId : nil }
return ChatModel.shared.chats return ChatModel.shared.chats
.compactMap{ $0.chatInfo.contact } .compactMap{ $0.chatInfo.contact }
.filter{ !memberContactIds.contains($0.apiId) } .filter{ !memberContactIds.contains($0.apiId) }
@@ -1221,7 +1133,6 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(getXFTPCfg()) try setXFTPConfig(getXFTPCfg())
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
m.chatInitialized = true m.chatInitialized = true
m.currentUser = try apiGetActiveUser() m.currentUser = try apiGetActiveUser()
if m.currentUser == nil { if m.currentUser == nil {
@@ -1374,20 +1285,12 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.removeChat(connection.id) m.removeChat(connection.id)
} }
} }
case let .contactDeletedByContact(user, contact):
if active(user) && contact.directOrUsed {
await MainActor.run {
m.updateContact(contact)
}
}
case let .contactConnected(user, contact, _): case let .contactConnected(user, contact, _):
if active(user) && contact.directOrUsed { if active(user) && contact.directOrUsed {
await MainActor.run { await MainActor.run {
m.updateContact(contact) m.updateContact(contact)
if let conn = contact.activeConn { m.dismissConnReqView(contact.activeConn.id)
m.dismissConnReqView(conn.id) m.removeChat(contact.activeConn.id)
m.removeChat(conn.id)
}
} }
} }
if contact.directOrUsed { if contact.directOrUsed {
@@ -1400,10 +1303,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
if active(user) && contact.directOrUsed { if active(user) && contact.directOrUsed {
await MainActor.run { await MainActor.run {
m.updateContact(contact) m.updateContact(contact)
if let conn = contact.activeConn { m.dismissConnReqView(contact.activeConn.id)
m.dismissConnReqView(conn.id) m.removeChat(contact.activeConn.id)
m.removeChat(conn.id)
}
} }
} }
case let .receivedContactRequest(user, contactRequest): case let .receivedContactRequest(user, contactRequest):
@@ -1428,12 +1329,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.updateChatInfo(cInfo) m.updateChatInfo(cInfo)
} }
} }
case let .groupMemberUpdated(user, groupInfo, _, toMember):
if active(user) {
await MainActor.run {
_ = m.upsertGroupMember(groupInfo, toMember)
}
}
case let .contactsMerged(user, intoContact, mergedContact): case let .contactsMerged(user, intoContact, mergedContact):
if active(user) && m.hasChat(mergedContact.id) { if active(user) && m.hasChat(mergedContact.id) {
await MainActor.run { await MainActor.run {
@@ -1447,6 +1342,13 @@ func processReceivedMsg(_ res: ChatResponse) async {
await updateContactsStatus(contactRefs, status: .connected) await updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs): case let .contactsDisconnected(_, contactRefs):
await updateContactsStatus(contactRefs, status: .disconnected) await updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubError(user, contact, chatError):
await MainActor.run {
if active(user) {
m.updateContact(contact)
}
processContactSubError(contact, chatError)
}
case let .contactSubSummary(_, contactSubscriptions): case let .contactSubSummary(_, contactSubscriptions):
await MainActor.run { await MainActor.run {
for sub in contactSubscriptions { for sub in contactSubscriptions {
@@ -1461,18 +1363,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
} }
} }
} }
case let .networkStatus(status, connections):
await MainActor.run {
for cId in connections {
m.networkStatuses[cId] = status
}
}
case let .networkStatuses(_, statuses): ()
await MainActor.run {
for s in statuses {
m.networkStatuses[s.agentConnId] = s.networkStatus
}
}
case let .newChatItem(user, aChatItem): case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem let cItem = aChatItem.chatItem
@@ -1494,8 +1384,11 @@ func processReceivedMsg(_ res: ChatResponse) async {
case let .chatItemStatusUpdated(user, aChatItem): case let .chatItemStatusUpdated(user, aChatItem):
let cInfo = aChatItem.chatInfo let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem let cItem = aChatItem.chatItem
if !cItem.isDeletedContent && active(user) { if !cItem.isDeletedContent {
await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) } let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true
if added && cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
} }
if let endTask = m.messageDelivery[cItem.id] { if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus { switch cItem.meta.itemStatus {
@@ -1542,19 +1435,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
await MainActor.run { await MainActor.run {
m.updateGroup(groupInfo) m.updateGroup(groupInfo)
if let conn = hostContact?.activeConn { if let hostContact = hostContact {
m.dismissConnReqView(conn.id) m.dismissConnReqView(hostContact.activeConn.id)
m.removeChat(conn.id) m.removeChat(hostContact.activeConn.id)
}
}
case let .groupLinkConnecting(user, groupInfo, hostMember):
if !active(user) { return }
await MainActor.run {
m.updateGroup(groupInfo)
if let hostConn = hostMember.activeConn {
m.dismissConnReqView(hostConn.id)
m.removeChat(hostConn.id)
} }
} }
case let .joinedGroupMemberConnecting(user, groupInfo, _, member): case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
@@ -1616,11 +1499,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.updateGroup(toGroup) m.updateGroup(toGroup)
} }
} }
case let .memberRole(user, groupInfo, byMember: _, member: member, fromRole: _, toRole: _): case let .memberRole(user, groupInfo, _, _, _, _):
if active(user) { if active(user) {
await MainActor.run { await MainActor.run {
m.updateGroup(groupInfo) m.updateGroup(groupInfo)
_ = m.upsertGroupMember(groupInfo, member)
} }
} }
case let .newMemberContactReceivedInv(user, contact, _, _): case let .newMemberContactReceivedInv(user, contact, _, _):
@@ -1716,39 +1598,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
await MainActor.run { await MainActor.run {
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
} }
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible):
await MainActor.run {
if let sess = m.remoteCtrlSession, case .searching = sess.sessionState {
let state = UIRemoteCtrlSessionState.found(remoteCtrl: remoteCtrl, compatible: compatible)
m.remoteCtrlSession = RemoteCtrlSession(
ctrlAppInfo: ctrlAppInfo_,
appVersion: appVersion,
sessionState: state
)
}
}
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode):
await MainActor.run {
let state = UIRemoteCtrlSessionState.pendingConfirmation(remoteCtrl_: remoteCtrl_, sessionCode: sessionCode)
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
}
case let .remoteCtrlConnected(remoteCtrl):
// TODO currently it is returned in response to command, so it is redundant
await MainActor.run {
let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "")
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
}
case .remoteCtrlStopped:
// This delay is needed to cancel the session that fails on network failure,
// e.g. when user did not grant permission to access local network yet.
if let sess = m.remoteCtrlSession {
m.remoteCtrlSession = nil
if case .connected = sess.sessionState {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
switchToLocalSession()
}
}
}
default: default:
logger.debug("unsupported event: \(res.responseType)") logger.debug("unsupported event: \(res.responseType)")
} }
@@ -1762,19 +1611,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
} }
} }
func switchToLocalSession() {
let m = ChatModel.shared
m.remoteCtrlSession = nil
do {
m.users = try listUsers()
try getUserChatData()
let statuses = (try apiGetNetworkStatuses()).map { s in (s.agentConnId, s.networkStatus) }
m.networkStatuses = Dictionary(uniqueKeysWithValues: statuses)
} catch let error {
logger.debug("error updating chat data: \(responseError(error))")
}
}
func active(_ user: any UserLike) -> Bool { func active(_ user: any UserLike) -> Bool {
user.userId == ChatModel.shared.currentUser?.id user.userId == ChatModel.shared.currentUser?.id
} }
@@ -1807,7 +1643,7 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted" case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
default: err = String(describing: chatError) default: err = String(describing: chatError)
} }
m.setContactNetworkStatus(contact, .error(connectionError: err)) m.setContactNetworkStatus(contact, .error(err))
} }
func refreshCallInvitations() throws { func refreshCallInvitations() throws {
+5 -23
View File
@@ -19,11 +19,7 @@ let bgSuspendTimeout: Int = 5 // seconds
let terminationTimeout: Int = 3 // seconds let terminationTimeout: Int = 3 // seconds
private func _suspendChat(timeout: Int) { private func _suspendChat(timeout: Int) {
// this is a redundant check to prevent logical errors, like the one fixed in this PR if ChatModel.ok {
let state = appStateGroupDefault.get()
if !state.canSuspend {
logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)")
} else if ChatModel.ok {
appStateGroupDefault.set(.suspending) appStateGroupDefault.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000) apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
let endTask = beginBGTask(chatSuspended) let endTask = beginBGTask(chatSuspended)
@@ -35,7 +31,9 @@ private func _suspendChat(timeout: Int) {
func suspendChat() { func suspendChat() {
suspendLockQueue.sync { suspendLockQueue.sync {
_suspendChat(timeout: appSuspendTimeout) if appStateGroupDefault.get() != .stopped {
_suspendChat(timeout: appSuspendTimeout)
}
} }
} }
@@ -47,25 +45,15 @@ func suspendBgRefresh() {
} }
} }
private var terminating = false
func terminateChat() { func terminateChat() {
logger.debug("terminateChat")
suspendLockQueue.sync { suspendLockQueue.sync {
switch appStateGroupDefault.get() { switch appStateGroupDefault.get() {
case .suspending: case .suspending:
// suspend instantly if already suspending // suspend instantly if already suspending
_chatSuspended() _chatSuspended()
// when apiSuspendChat is called with timeout 0, it won't send any events on suspension
if ChatModel.ok { apiSuspendChat(timeoutMicroseconds: 0) } if ChatModel.ok { apiSuspendChat(timeoutMicroseconds: 0) }
chatCloseStore() case .stopped: ()
case .suspended:
chatCloseStore()
case .stopped:
chatCloseStore()
default: default:
terminating = true
// the store will be closed in _chatSuspended when event is received
_suspendChat(timeout: terminationTimeout) _suspendChat(timeout: terminationTimeout)
} }
} }
@@ -85,14 +73,10 @@ private func _chatSuspended() {
if ChatModel.shared.chatRunning == true { if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.stop() ChatReceiver.shared.stop()
} }
if terminating {
chatCloseStore()
}
} }
func activateChat(appState: AppState = .active) { func activateChat(appState: AppState = .active) {
logger.debug("DEBUGGING: activateChat") logger.debug("DEBUGGING: activateChat")
terminating = false
suspendLockQueue.sync { suspendLockQueue.sync {
appStateGroupDefault.set(appState) appStateGroupDefault.set(appState)
if ChatModel.ok { apiActivateChat() } if ChatModel.ok { apiActivateChat() }
@@ -101,7 +85,6 @@ func activateChat(appState: AppState = .active) {
} }
func initChatAndMigrate(refreshInvitations: Bool = true) { func initChatAndMigrate(refreshInvitations: Bool = true) {
terminating = false
let m = ChatModel.shared let m = ChatModel.shared
if (!m.chatInitialized) { if (!m.chatInitialized) {
do { do {
@@ -114,7 +97,6 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
} }
func startChatAndActivate() { func startChatAndActivate() {
terminating = false
logger.debug("DEBUGGING: startChatAndActivate") logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true { if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start() ChatReceiver.shared.start()
+1 -4
View File
@@ -26,10 +26,7 @@ struct SimpleXApp: App {
@State private var showInitializationView = false @State private var showInitializationView = false
init() { init() {
DispatchQueue.global(qos: .background).sync { hs_init(0, nil)
haskell_init()
// hs_init(0, nil)
}
UserDefaults.standard.register(defaults: appDefaults) UserDefaults.standard.register(defaults: appDefaults)
setGroupDefaults() setGroupDefaults()
registerGroupDefaults() registerGroupDefaults()
@@ -39,7 +39,6 @@ struct ActiveCallView: View {
} }
.onAppear { .onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)") logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true)
createWebRTCClient() createWebRTCClient()
dismissAllSheets() dismissAllSheets()
} }
@@ -49,7 +48,6 @@ struct ActiveCallView: View {
} }
.onDisappear { .onDisappear {
logger.debug("ActiveCallView: disappear") logger.debug("ActiveCallView: disappear")
AppDelegate.keepScreenOn(false)
client?.endCall() client?.endCall()
} }
.onChange(of: m.callCommand) { _ in sendCommandToClient()} .onChange(of: m.callCommand) { _ in sendCommandToClient()}
@@ -108,6 +108,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
try audioSession.setActive(true) try audioSession.setActive(true)
logger.debug("audioSession activated") logger.debug("audioSession activated")
} catch { } catch {
print(error)
logger.error("failed activating audio session") logger.error("failed activating audio session")
} }
} }
@@ -120,6 +121,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
try audioSession.setActive(false) try audioSession.setActive(false)
logger.debug("audioSession deactivated") logger.debug("audioSession deactivated")
} catch { } catch {
print(error)
logger.error("failed deactivating audio session") logger.error("failed deactivating audio session")
} }
suspendOnEndCall() suspendOnEndCall()
+34 -44
View File
@@ -99,12 +99,12 @@ struct ChatInfoView: View {
@Binding var connectionCode: String? @Binding var connectionCode: String?
@FocusState private var aliasTextFieldFocused: Bool @FocusState private var aliasTextFieldFocused: Bool
@State private var alert: ChatInfoViewAlert? = nil @State private var alert: ChatInfoViewAlert? = nil
@State private var showDeleteContactActionSheet = false
@State private var sendReceipts = SendReceipts.userDefault(true) @State private var sendReceipts = SendReceipts.userDefault(true)
@State private var sendReceiptsUserDefault = true @State private var sendReceiptsUserDefault = true
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
enum ChatInfoViewAlert: Identifiable { enum ChatInfoViewAlert: Identifiable {
case deleteContactAlert
case clearChatAlert case clearChatAlert
case networkStatusAlert case networkStatusAlert
case switchAddressAlert case switchAddressAlert
@@ -114,6 +114,7 @@ struct ChatInfoView: View {
var id: String { var id: String {
switch self { switch self {
case .deleteContactAlert: return "deleteContactAlert"
case .clearChatAlert: return "clearChatAlert" case .clearChatAlert: return "clearChatAlert"
case .networkStatusAlert: return "networkStatusAlert" case .networkStatusAlert: return "networkStatusAlert"
case .switchAddressAlert: return "switchAddressAlert" case .switchAddressAlert: return "switchAddressAlert"
@@ -163,13 +164,13 @@ struct ChatInfoView: View {
// synchronizeConnectionButtonForce() // synchronizeConnectionButtonForce()
// } // }
} }
.disabled(!contact.ready || !contact.active) .disabled(!contact.ready)
if let contactLink = contact.contactLink { if let contactLink = contact.contactLink {
Section { Section {
SimpleXLinkQRCode(uri: contactLink) QRCode(uri: contactLink)
Button { Button {
showShareSheet(items: [simplexChatLink(contactLink)]) showShareSheet(items: [contactLink])
} label: { } label: {
Label("Share address", systemImage: "square.and.arrow.up") Label("Share address", systemImage: "square.and.arrow.up")
} }
@@ -180,7 +181,7 @@ struct ChatInfoView: View {
} }
} }
if contact.ready && contact.active { if contact.ready {
Section("Servers") { Section("Servers") {
networkStatusRow() networkStatusRow()
.onTapGesture { .onTapGesture {
@@ -191,7 +192,8 @@ struct ChatInfoView: View {
alert = .switchAddressAlert alert = .switchAddressAlert
} }
.disabled( .disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } !contact.ready
|| connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited || connStats.ratchetSyncSendProhibited
) )
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
@@ -232,6 +234,7 @@ struct ChatInfoView: View {
} }
.alert(item: $alert) { alertItem in .alert(item: $alert) { alertItem in
switch(alertItem) { switch(alertItem) {
case .deleteContactAlert: return deleteContactAlert()
case .clearChatAlert: return clearChatAlert() case .clearChatAlert: return clearChatAlert()
case .networkStatusAlert: return networkStatusAlert() case .networkStatusAlert: return networkStatusAlert()
case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
@@ -240,26 +243,6 @@ struct ChatInfoView: View {
case let .error(title, error): return mkAlert(title: title, message: error) case let .error(title, error): return mkAlert(title: title, message: error)
} }
} }
.actionSheet(isPresented: $showDeleteContactActionSheet) {
if contact.ready && contact.active {
return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [
.destructive(Text("Delete and notify contact")) { deleteContact(notify: true) },
.destructive(Text("Delete")) { deleteContact(notify: false) },
.cancel()
]
)
} else {
return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [
.destructive(Text("Delete")) { deleteContact() },
.cancel()
]
)
}
}
} }
private func contactInfoHeader() -> some View { private func contactInfoHeader() -> some View {
@@ -338,7 +321,7 @@ struct ChatInfoView: View {
verify: { code in verify: { code in
if let r = apiVerifyContact(chat.chatInfo.apiId, connectionCode: code) { if let r = apiVerifyContact(chat.chatInfo.apiId, connectionCode: code) {
let (verified, existingCode) = r let (verified, existingCode) = r
contact.activeConn?.connectionCode = verified ? SecurityCode(securityCode: existingCode, verifiedAt: .now) : nil contact.activeConn.connectionCode = verified ? SecurityCode(securityCode: existingCode, verifiedAt: .now) : nil
connectionCode = existingCode connectionCode = existingCode
DispatchQueue.main.async { DispatchQueue.main.async {
chat.chatInfo = .direct(contact: contact) chat.chatInfo = .direct(contact: contact)
@@ -432,7 +415,7 @@ struct ChatInfoView: View {
private func deleteContactButton() -> some View { private func deleteContactButton() -> some View {
Button(role: .destructive) { Button(role: .destructive) {
showDeleteContactActionSheet = true alert = .deleteContactAlert
} label: { } label: {
Label("Delete contact", systemImage: "trash") Label("Delete contact", systemImage: "trash")
.foregroundColor(Color.red) .foregroundColor(Color.red)
@@ -448,23 +431,30 @@ struct ChatInfoView: View {
} }
} }
private func deleteContact(notify: Bool? = nil) { private func deleteContactAlert() -> Alert {
Task { Alert(
do { title: Text("Delete contact?"),
try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, notify: notify) message: Text("Contact and all messages will be deleted - this cannot be undone!"),
await MainActor.run { primaryButton: .destructive(Text("Delete")) {
dismiss() Task {
chatModel.chatId = nil do {
chatModel.removeChat(chat.chatInfo.id) try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId)
await MainActor.run {
dismiss()
chatModel.chatId = nil
chatModel.removeChat(chat.chatInfo.id)
}
} catch let error {
logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
let a = getErrorAlert(error, "Error deleting contact")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
} }
} catch let error { },
logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))") secondaryButton: .cancel()
let a = getErrorAlert(error, "Error deleting contact") )
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
} }
private func clearChatAlert() -> Alert { private func clearChatAlert() -> Alert {
@@ -11,7 +11,7 @@ import SimpleXChat
struct CICallItemView: View { struct CICallItemView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@ObservedObject var chat: Chat var chatInfo: ChatInfo
var chatItem: ChatItem var chatItem: ChatItem
var status: CICallStatus var status: CICallStatus
var duration: Int var duration: Int
@@ -60,7 +60,7 @@ struct CICallItemView: View {
@ViewBuilder private func acceptCallButton() -> some View { @ViewBuilder private func acceptCallButton() -> some View {
if case let .direct(contact) = chat.chatInfo { if case let .direct(contact) = chatInfo {
Button { Button {
if let invitation = m.callInvitations[contact.id] { if let invitation = m.callInvitations[contact.id] {
CallController.shared.answerCall(invitation: invitation) CallController.shared.answerCall(invitation: invitation)
@@ -10,92 +10,20 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct CIChatFeatureView: View { struct CIChatFeatureView: View {
@EnvironmentObject var m: ChatModel
var chatItem: ChatItem var chatItem: ChatItem
@Binding var revealed: Bool
var feature: Feature var feature: Feature
var icon: String? = nil var icon: String? = nil
var iconColor: Color var iconColor: Color
var body: some View { var body: some View {
if !revealed, let fs = mergedFeautures() {
HStack {
ForEach(fs, content: featureIconView)
}
.padding(.horizontal, 6)
.padding(.vertical, 6)
} else {
fullFeatureView
}
}
private struct FeatureInfo: Identifiable {
var icon: String
var scale: CGFloat
var color: Color
var param: String?
init(_ f: Feature, _ color: Color, _ param: Int?) {
self.icon = f.iconFilled
self.scale = f.iconScale
self.color = color
self.param = f.hasParam && param != nil ? timeText(param) : nil
}
var id: String {
"\(icon) \(color) \(param ?? "")"
}
}
private func mergedFeautures() -> [FeatureInfo]? {
var fs: [FeatureInfo] = []
var icons: Set<String> = []
if var i = m.getChatItemIndex(chatItem) {
while i < m.reversedChatItems.count,
let f = featureInfo(m.reversedChatItems[i]) {
if !icons.contains(f.icon) {
fs.insert(f, at: 0)
icons.insert(f.icon)
}
i += 1
}
}
return fs.count > 1 ? fs : nil
}
private func featureInfo(_ ci: ChatItem) -> FeatureInfo? {
switch ci.content {
case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param)
case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param)
case let .rcvGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param)
case let .sndGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param)
default: nil
}
}
@ViewBuilder private func featureIconView(_ f: FeatureInfo) -> some View {
let i = Image(systemName: f.icon)
.foregroundColor(f.color)
.scaleEffect(f.scale)
if let param = f.param {
HStack {
i
chatEventText(Text(param)).lineLimit(1)
}
} else {
i
}
}
private var fullFeatureView: some View {
HStack(alignment: .bottom, spacing: 4) { HStack(alignment: .bottom, spacing: 4) {
Image(systemName: icon ?? feature.iconFilled) Image(systemName: icon ?? feature.iconFilled)
.foregroundColor(iconColor) .foregroundColor(iconColor)
.scaleEffect(feature.iconScale) .scaleEffect(feature.iconScale)
chatEventText(chatItem) chatEventText(chatItem)
} }
.padding(.horizontal, 6) .padding(.leading, 6)
.padding(.vertical, 4) .padding(.bottom, 6)
.textSelection(.disabled) .textSelection(.disabled)
} }
} }
@@ -103,6 +31,6 @@ struct CIChatFeatureView: View {
struct CIChatFeatureView_Previews: PreviewProvider { struct CIChatFeatureView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let enabled = FeatureEnabled(forUser: false, forContact: false) let enabled = FeatureEnabled(forUser: false, forContact: false)
CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor) CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
} }
} }
@@ -13,9 +13,11 @@ struct CIEventView: View {
var eventText: Text var eventText: Text
var body: some View { var body: some View {
eventText HStack(alignment: .bottom, spacing: 0) {
.padding(.horizontal, 6) eventText
.padding(.vertical, 4) }
.padding(.leading, 6)
.padding(.bottom, 6)
.textSelection(.disabled) .textSelection(.disabled)
} }
} }
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct CIFeaturePreferenceView: View { struct CIFeaturePreferenceView: View {
@ObservedObject var chat: Chat @EnvironmentObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
var feature: ChatFeature var feature: ChatFeature
var allowed: FeatureAllowed var allowed: FeatureAllowed
@@ -80,6 +80,7 @@ struct CIFeaturePreferenceView_Previews: PreviewProvider {
quotedItem: nil, quotedItem: nil,
file: nil file: nil
) )
CIFeaturePreferenceView(chat: Chat.sampleData, chatItem: chatItem, feature: ChatFeature.timedMessages, allowed: .yes, param: 30) CIFeaturePreferenceView(chatItem: chatItem, feature: ChatFeature.timedMessages, allowed: .yes, param: 30)
.environmentObject(Chat.sampleData)
} }
} }
@@ -10,7 +10,6 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct CIFileView: View { struct CIFileView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
let file: CIFile? let file: CIFile?
let edited: Bool let edited: Bool
@@ -84,7 +83,7 @@ struct CIFileView: View {
if fileSizeValid() { if fileSizeValid() {
Task { Task {
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task") logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
if let user = m.currentUser { if let user = ChatModel.shared.currentUser {
let encrypted = privacyEncryptLocalFilesGroupDefault.get() let encrypted = privacyEncryptLocalFilesGroupDefault.get()
await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted) await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted)
} }
@@ -235,17 +234,18 @@ struct CIFileView_Previews: PreviewProvider {
file: nil file: nil
) )
Group { Group {
ChatItemView(chat: Chat.sampleData, chatItem: sentFile, revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentFile, revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: fileChatItemWtFile, revealed: Binding.constant(false))
} }
.previewLayout(.fixed(width: 360, height: 360)) .previewLayout(.fixed(width: 360, height: 360))
.environmentObject(Chat.sampleData)
} }
} }
@@ -17,45 +17,34 @@ struct CIGroupInvitationView: View {
var memberRole: GroupMemberRole var memberRole: GroupMemberRole
var chatIncognito: Bool = false var chatIncognito: Bool = false
@State private var frameWidth: CGFloat = 0 @State private var frameWidth: CGFloat = 0
@State private var inProgress = false
@State private var progressByTimeout = false
var body: some View { var body: some View {
let action = !chatItem.chatDir.sent && groupInvitation.status == .pending let action = !chatItem.chatDir.sent && groupInvitation.status == .pending
let v = ZStack(alignment: .bottomTrailing) { let v = ZStack(alignment: .bottomTrailing) {
ZStack { VStack(alignment: .leading) {
VStack(alignment: .leading) { groupInfoView(action)
groupInfoView(action) .padding(.horizontal, 2)
.padding(.horizontal, 2) .padding(.top, 8)
.padding(.top, 8) .padding(.bottom, 6)
.padding(.bottom, 6) .overlay(DetermineWidth())
Divider().frame(width: frameWidth)
if action {
groupInvitationText()
.overlay(DetermineWidth())
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
.foregroundColor(chatIncognito ? .indigo : .accentColor)
.font(.callout)
.padding(.trailing, 60)
.overlay(DetermineWidth())
} else {
groupInvitationText()
.padding(.trailing, 60)
.overlay(DetermineWidth()) .overlay(DetermineWidth())
Divider().frame(width: frameWidth)
if action {
VStack(alignment: .leading, spacing: 2) {
groupInvitationText()
.overlay(DetermineWidth())
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
.font(.callout)
.padding(.trailing, 60)
.overlay(DetermineWidth())
}
} else {
groupInvitationText()
.padding(.trailing, 60)
.overlay(DetermineWidth())
}
}
.padding(.bottom, 2)
if progressByTimeout {
ProgressView().scaleEffect(2)
} }
} }
.padding(.bottom, 2)
chatItem.timestampText chatItem.timestampText
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -66,24 +55,11 @@ struct CIGroupInvitationView: View {
.cornerRadius(18) .cornerRadius(18)
.textSelection(.disabled) .textSelection(.disabled)
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
.onChange(of: inProgress) { inProgress in
if inProgress {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
progressByTimeout = inProgress
}
} else {
progressByTimeout = false
}
}
if action { if action {
v.onTapGesture { v.onTapGesture {
inProgress = true joinGroup(groupInvitation.groupId)
joinGroup(groupInvitation.groupId) {
await MainActor.run { inProgress = false }
}
} }
.disabled(inProgress)
} else { } else {
v v
} }
@@ -91,7 +67,7 @@ struct CIGroupInvitationView: View {
private func groupInfoView(_ action: Bool) -> some View { private func groupInfoView(_ action: Bool) -> some View {
var color: Color var color: Color
if action && !inProgress { if action {
color = chatIncognito ? .indigo : .accentColor color = chatIncognito ? .indigo : .accentColor
} else { } else {
color = Color(uiColor: .tertiaryLabel) color = Color(uiColor: .tertiaryLabel)
@@ -10,7 +10,6 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct CIImageView: View { struct CIImageView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
let chatItem: ChatItem let chatItem: ChatItem
let image: String let image: String
@@ -37,7 +36,7 @@ struct CIImageView: View {
switch file.fileStatus { switch file.fileStatus {
case .rcvInvitation: case .rcvInvitation:
Task { Task {
if let user = m.currentUser { if let user = ChatModel.shared.currentUser {
await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile) await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile)
} }
} }
@@ -10,7 +10,6 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct CIMemberCreatedContactView: View { struct CIMemberCreatedContactView: View {
@EnvironmentObject var m: ChatModel
var chatItem: ChatItem var chatItem: ChatItem
var body: some View { var body: some View {
@@ -22,7 +21,7 @@ struct CIMemberCreatedContactView: View {
.onTapGesture { .onTapGesture {
dismissAllSheets(animated: true) dismissAllSheets(animated: true)
DispatchQueue.main.async { DispatchQueue.main.async {
m.chatId = "@\(contactId)" ChatModel.shared.chatId = "@\(contactId)"
} }
} }
} else { } else {
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct CIMetaView: View { struct CIMetaView: View {
@ObservedObject var chat: Chat @EnvironmentObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
var metaColor = Color.secondary var metaColor = Color.secondary
var paleMetaColor = Color(UIColor.tertiaryLabel) var paleMetaColor = Color(UIColor.tertiaryLabel)
@@ -95,14 +95,15 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text {
struct CIMetaView_Previews: PreviewProvider { struct CIMetaView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete))) CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial))) CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete))) CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial))) CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete))) CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true)) CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample()) CIMetaView(chatItem: ChatItem.getDeletedContentSample())
} }
.previewLayout(.fixed(width: 360, height: 100)) .previewLayout(.fixed(width: 360, height: 100))
.environmentObject(Chat.sampleData)
} }
} }
@@ -12,8 +12,7 @@ import SimpleXChat
let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup." let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup."
struct CIRcvDecryptionError: View { struct CIRcvDecryptionError: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var chat: Chat
@ObservedObject var chat: Chat
var msgDecryptError: MsgDecryptError var msgDecryptError: MsgDecryptError
var msgCount: UInt32 var msgCount: UInt32
var chatItem: ChatItem var chatItem: ChatItem
@@ -46,7 +45,7 @@ struct CIRcvDecryptionError: View {
do { do {
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId) let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
if let s = stats { if let s = stats {
m.updateGroupMemberConnectionStats(groupInfo, member, s) ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, member, s)
} }
} catch let error { } catch let error {
logger.error("apiGroupMemberInfo error: \(responseError(error))") logger.error("apiGroupMemberInfo error: \(responseError(error))")
@@ -66,7 +65,7 @@ struct CIRcvDecryptionError: View {
@ViewBuilder private func viewBody() -> some View { @ViewBuilder private func viewBody() -> some View {
if case let .direct(contact) = chat.chatInfo, if case let .direct(contact) = chat.chatInfo,
let contactStats = contact.activeConn?.connectionStats { let contactStats = contact.activeConn.connectionStats {
if contactStats.ratchetSyncAllowed { if contactStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) { decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncContactConnection(contact) } alert = .syncAllowedAlert { syncContactConnection(contact) }
@@ -80,8 +79,8 @@ struct CIRcvDecryptionError: View {
} }
} else if case let .group(groupInfo) = chat.chatInfo, } else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir, case let .groupRcv(groupMember) = chatItem.chatDir,
let mem = m.getGroupMember(groupMember.groupMemberId), let modelMember = ChatModel.shared.groupMembers.first(where: { $0.id == groupMember.id }),
let memberStats = mem.wrapped.activeConn?.connectionStats { let memberStats = modelMember.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed { if memberStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) { decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) } alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
@@ -123,7 +122,7 @@ struct CIRcvDecryptionError: View {
) )
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem) CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12) .padding(.horizontal, 12)
} }
.onTapGesture(perform: { onClick() }) .onTapGesture(perform: { onClick() })
@@ -143,7 +142,7 @@ struct CIRcvDecryptionError: View {
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem) CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12) .padding(.horizontal, 12)
} }
.onTapGesture(perform: { onClick() }) .onTapGesture(perform: { onClick() })
@@ -165,8 +164,6 @@ struct CIRcvDecryptionError: View {
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
case .other: case .other:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
case .ratchetSync:
message = Text("Encryption re-negotiation failed.")
} }
return message return message
} }
@@ -176,7 +173,7 @@ struct CIRcvDecryptionError: View {
do { do {
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, false) let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, false)
await MainActor.run { await MainActor.run {
m.updateGroupMemberConnectionStats(groupInfo, mem, stats) ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, mem, stats)
} }
} catch let error { } catch let error {
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))") logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
@@ -193,7 +190,7 @@ struct CIRcvDecryptionError: View {
do { do {
let stats = try apiSyncContactRatchet(contact.apiId, false) let stats = try apiSyncContactRatchet(contact.apiId, false)
await MainActor.run { await MainActor.run {
m.updateContactConnectionStats(contact, stats) ChatModel.shared.updateContactConnectionStats(contact, stats)
} }
} catch let error { } catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
@@ -9,10 +9,8 @@
import SwiftUI import SwiftUI
import AVKit import AVKit
import SimpleXChat import SimpleXChat
import Combine
struct CIVideoView: View { struct CIVideoView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
private let chatItem: ChatItem private let chatItem: ChatItem
private let image: String private let image: String
@@ -29,7 +27,6 @@ struct CIVideoView: View {
@State private var showFullScreenPlayer = false @State private var showFullScreenPlayer = false
@State private var timeObserver: Any? = nil @State private var timeObserver: Any? = nil
@State private var fullScreenTimeObserver: Any? = nil @State private var fullScreenTimeObserver: Any? = nil
@State private var publisher: AnyCancellable? = nil
init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding<CGFloat?>, scrollProxy: ScrollViewProxy?) { init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding<CGFloat?>, scrollProxy: ScrollViewProxy?) {
self.chatItem = chatItem self.chatItem = chatItem
@@ -104,7 +101,7 @@ struct CIVideoView: View {
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete
VideoPlayerView(player: player, url: url, showControls: false) VideoPlayerView(player: player, url: url, showControls: false)
.frame(width: w, height: w * preview.size.height / preview.size.width) .frame(width: w, height: w * preview.size.height / preview.size.width)
.onChange(of: m.stopPreviousRecPlay) { playingUrl in .onChange(of: ChatModel.shared.stopPreviousRecPlay) { playingUrl in
if playingUrl != url { if playingUrl != url {
player.pause() player.pause()
videoPlaying = false videoPlaying = false
@@ -127,7 +124,7 @@ struct CIVideoView: View {
} }
if !videoPlaying { if !videoPlaying {
Button { Button {
m.stopPreviousRecPlay = url ChatModel.shared.stopPreviousRecPlay = url
player.play() player.play()
} label: { } label: {
playPauseIcon(canBePlayed ? "play.fill" : "play.slash") playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
@@ -259,7 +256,7 @@ struct CIVideoView: View {
// TODO encrypt: where file size is checked? // TODO encrypt: where file size is checked?
private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) { private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
Task { Task {
if let user = m.currentUser { if let user = ChatModel.shared.currentUser {
await receiveFile(user, file.fileId, encrypted, false) await receiveFile(user, file.fileId, encrypted, false)
} }
} }
@@ -293,17 +290,9 @@ struct CIVideoView: View {
) )
.onAppear { .onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()) { DispatchQueue.main.asyncAfter(deadline: .now()) {
m.stopPreviousRecPlay = url ChatModel.shared.stopPreviousRecPlay = url
if let player = fullPlayer { if let player = fullPlayer {
player.play() player.play()
var played = false
publisher = player.publisher(for: \.timeControlStatus).sink { status in
if played || status == .playing {
AppDelegate.keepScreenOn(status == .playing)
AudioPlayer.changeAudioSession(status == .playing)
}
played = status == .playing
}
fullScreenTimeObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in fullScreenTimeObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in
player.seek(to: CMTime.zero) player.seek(to: CMTime.zero)
player.play() player.play()
@@ -318,7 +307,6 @@ struct CIVideoView: View {
fullScreenTimeObserver = nil fullScreenTimeObserver = nil
fullPlayer?.pause() fullPlayer?.pause()
fullPlayer?.seek(to: CMTime.zero) fullPlayer?.seek(to: CMTime.zero)
publisher?.cancel()
} }
} }
} }
@@ -10,7 +10,6 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct CIVoiceView: View { struct CIVoiceView: View {
@ObservedObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
let recordingFile: CIFile? let recordingFile: CIFile?
let duration: Int let duration: Int
@@ -92,7 +91,7 @@ struct CIVoiceView: View {
} }
private func metaView() -> some View { private func metaView() -> some View {
CIMetaView(chat: chat, chatItem: chatItem) CIMetaView(chatItem: chatItem)
} }
} }
@@ -220,7 +219,7 @@ struct VoiceMessagePlayer: View {
private func downloadButton(_ recordingFile: CIFile) -> some View { private func downloadButton(_ recordingFile: CIFile) -> some View {
Button { Button {
Task { Task {
if let user = chatModel.currentUser { if let user = ChatModel.shared.currentUser {
await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get()) await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get())
} }
} }
@@ -285,7 +284,6 @@ struct CIVoiceView_Previews: PreviewProvider {
) )
Group { Group {
CIVoiceView( CIVoiceView(
chat: Chat.sampleData,
chatItem: ChatItem.getVoiceMsgContentSample(), chatItem: ChatItem.getVoiceMsgContentSample(),
recordingFile: CIFile.getSample(fileName: "voice.m4a", fileSize: 65536, fileStatus: .rcvComplete), recordingFile: CIFile.getSample(fileName: "voice.m4a", fileSize: 65536, fileStatus: .rcvComplete),
duration: 30, duration: 30,
@@ -294,11 +292,12 @@ struct CIVoiceView_Previews: PreviewProvider {
playbackTime: .constant(TimeInterval(20)), playbackTime: .constant(TimeInterval(20)),
allowMenu: Binding.constant(true) allowMenu: Binding.constant(true)
) )
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
} }
.previewLayout(.fixed(width: 360, height: 360)) .previewLayout(.fixed(width: 360, height: 360))
.environmentObject(Chat.sampleData)
} }
} }
@@ -11,7 +11,6 @@ import SimpleXChat
struct DeletedItemView: View { struct DeletedItemView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
var body: some View { var body: some View {
@@ -19,7 +18,7 @@ struct DeletedItemView: View {
Text(chatItem.content.text) Text(chatItem.content.text)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.italic() .italic()
CIMetaView(chat: chat, chatItem: chatItem) CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12) .padding(.horizontal, 12)
} }
.padding(.leading, 12) .padding(.leading, 12)
@@ -33,8 +32,8 @@ struct DeletedItemView: View {
struct DeletedItemView_Previews: PreviewProvider { struct DeletedItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
DeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample()) DeletedItemView(chatItem: ChatItem.getDeletedContentSample())
DeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData))) DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)))
} }
.previewLayout(.fixed(width: 360, height: 200)) .previewLayout(.fixed(width: 360, height: 200))
} }
@@ -10,7 +10,6 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct EmojiItemView: View { struct EmojiItemView: View {
@ObservedObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
var body: some View { var body: some View {
@@ -18,7 +17,7 @@ struct EmojiItemView: View {
emojiText(chatItem.content.text) emojiText(chatItem.content.text)
.padding(.top, 8) .padding(.top, 8)
.padding(.horizontal, 6) .padding(.horizontal, 6)
CIMetaView(chat: chat, chatItem: chatItem) CIMetaView(chatItem: chatItem)
.padding(.bottom, 8) .padding(.bottom, 8)
.padding(.horizontal, 12) .padding(.horizontal, 12)
} }
@@ -33,8 +32,8 @@ func emojiText(_ text: String) -> Text {
struct EmojiItemView_Previews: PreviewProvider { struct EmojiItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group{ Group{
EmojiItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete))) EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete)))
EmojiItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "👍")) EmojiItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "👍"))
} }
.previewLayout(.fixed(width: 360, height: 70)) .previewLayout(.fixed(width: 360, height: 70))
} }
@@ -88,12 +88,13 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
file: CIFile.getSample(fileStatus: .sndComplete) file: CIFile.getSample(fileStatus: .sndComplete)
) )
Group { Group {
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false))
} }
.previewLayout(.fixed(width: 360, height: 360)) .previewLayout(.fixed(width: 360, height: 360))
.environmentObject(Chat.sampleData)
} }
} }
File diff suppressed because one or more lines are too long
@@ -150,7 +150,7 @@ struct FullScreenMediaView: View {
private func startPlayerAndNotify() { private func startPlayerAndNotify() {
if let player = player { if let player = player {
m.stopPreviousRecPlay = url ChatModel.shared.stopPreviousRecPlay = url
player.play() player.play()
} }
} }
@@ -10,12 +10,11 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct IntegrityErrorItemView: View { struct IntegrityErrorItemView: View {
@ObservedObject var chat: Chat
var msgError: MsgErrorType var msgError: MsgErrorType
var chatItem: ChatItem var chatItem: ChatItem
var body: some View { var body: some View {
CIMsgError(chat: chat, chatItem: chatItem) { CIMsgError(chatItem: chatItem) {
switch msgError { switch msgError {
case .msgSkipped: case .msgSkipped:
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
@@ -53,7 +52,6 @@ struct IntegrityErrorItemView: View {
} }
struct CIMsgError: View { struct CIMsgError: View {
@ObservedObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
var onTap: () -> Void var onTap: () -> Void
@@ -62,7 +60,7 @@ struct CIMsgError: View {
Text(chatItem.content.text) Text(chatItem.content.text)
.foregroundColor(.red) .foregroundColor(.red)
.italic() .italic()
CIMetaView(chat: chat, chatItem: chatItem) CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12) .padding(.horizontal, 12)
} }
.padding(.leading, 12) .padding(.leading, 12)
@@ -76,6 +74,6 @@ struct CIMsgError: View {
struct IntegrityErrorItemView_Previews: PreviewProvider { struct IntegrityErrorItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
IntegrityErrorItemView(chat: Chat.sampleData, msgError: .msgBadHash, chatItem: ChatItem.getIntegrityErrorSample()) IntegrityErrorItemView(msgError: .msgBadHash, chatItem: ChatItem.getIntegrityErrorSample())
} }
} }
@@ -10,70 +10,39 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct MarkedDeletedItemView: View { struct MarkedDeletedItemView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
@Binding var revealed: Bool
var body: some View { var body: some View {
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText) HStack(alignment: .bottom, spacing: 0) {
.font(.caption) if case let .moderated(_, byGroupMember) = chatItem.meta.itemDeleted {
.foregroundColor(.secondary) markedDeletedText("moderated by \(byGroupMember.chatViewName)")
.padding(.horizontal, 12) } else {
markedDeletedText("marked deleted")
}
CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12)
}
.padding(.leading, 12)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, colorScheme)) .background(chatItemFrameColor(chatItem, colorScheme))
.cornerRadius(18) .cornerRadius(18)
.textSelection(.disabled) .textSelection(.disabled)
} }
var mergedMarkedDeletedText: LocalizedStringKey { func markedDeletedText(_ s: LocalizedStringKey) -> some View {
if !revealed, Text(s)
let ciCategory = chatItem.mergeCategory, .font(.caption)
var i = m.getChatItemIndex(chatItem) { .foregroundColor(.secondary)
var moderated = 0 .italic()
var blocked = 0 .lineLimit(1)
var deleted = 0
var moderatedBy: Set<String> = []
while i < m.reversedChatItems.count,
let ci = .some(m.reversedChatItems[i]),
ci.mergeCategory == ciCategory,
let itemDeleted = ci.meta.itemDeleted {
switch itemDeleted {
case let .moderated(_, byGroupMember):
moderated += 1
moderatedBy.insert(byGroupMember.displayName)
case .blocked: blocked += 1
case .deleted: deleted += 1
}
i += 1
}
let total = moderated + blocked + deleted
return total <= 1
? markedDeletedText
: total == moderated
? "\(total) messages moderated by \(moderatedBy.joined(separator: ", "))"
: total == blocked
? "\(total) messages blocked"
: "\(total) messages marked deleted"
} else {
return markedDeletedText
}
}
var markedDeletedText: LocalizedStringKey {
switch chatItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
case .blocked: "blocked"
default: "marked deleted"
}
} }
} }
struct MarkedDeletedItemView_Previews: PreviewProvider { struct MarkedDeletedItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
MarkedDeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true)) MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)))
} }
.previewLayout(.fixed(width: 360, height: 200)) .previewLayout(.fixed(width: 360, height: 200))
} }
@@ -25,7 +25,7 @@ private func typing(_ w: Font.Weight = .light) -> Text {
} }
struct MsgContentView: View { struct MsgContentView: View {
@ObservedObject var chat: Chat @EnvironmentObject var chat: Chat
var text: String var text: String
var formattedText: [FormattedText]? = nil var formattedText: [FormattedText]? = nil
var sender: String? = nil var sender: String? = nil
@@ -121,11 +121,13 @@ private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary) case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary)
case let .colored(color): return Text(t).foregroundColor(color.uiColor) case let .colored(color): return Text(t).foregroundColor(color.uiColor)
case .uri: return linkText(t, t, preview, prefix: "") case .uri: return linkText(t, t, preview, prefix: "")
case let .simplexLink(linkType, simplexUri, smpHosts): case let .simplexLink(linkType, simplexUri, trustedUri, smpHosts):
switch privacySimplexLinkModeDefault.get() { switch privacySimplexLinkModeDefault.get() {
case .description: return linkText(simplexLinkText(linkType, smpHosts), simplexUri, preview, prefix: "") case .description: return linkText(simplexLinkText(linkType, smpHosts), simplexUri, preview, prefix: "")
case .full: return linkText(t, simplexUri, preview, prefix: "") case .full: return linkText(t, simplexUri, preview, prefix: "")
case .browser: return linkText(t, simplexUri, preview, prefix: "") case .browser: return trustedUri
? linkText(t, t, preview, prefix: "")
: linkText(t, t, preview, prefix: "", color: .red, uiColor: .red)
} }
case .email: return linkText(t, t, preview, prefix: "mailto:") case .email: return linkText(t, t, preview, prefix: "mailto:")
case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:") case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:")
@@ -152,7 +154,6 @@ struct MsgContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let chatItem = ChatItem.getSample(1, .directSnd, .now, "hello") let chatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
return MsgContentView( return MsgContentView(
chat: Chat.sampleData,
text: chatItem.text, text: chatItem.text,
formattedText: chatItem.formattedText, formattedText: chatItem.formattedText,
sender: chatItem.memberDisplayName, sender: chatItem.memberDisplayName,
@@ -10,7 +10,6 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct ChatItemInfoView: View { struct ChatItemInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var ci: ChatItem var ci: ChatItem
@Binding var chatItemInfo: ChatItemInfo? @Binding var chatItemInfo: ChatItemInfo?
@@ -291,8 +290,8 @@ struct ChatItemInfoView: View {
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] { private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
memberDeliveryStatuses.compactMap({ mds in memberDeliveryStatuses.compactMap({ mds in
if let mem = chatModel.getGroupMember(mds.groupMemberId) { if let mem = ChatModel.shared.groupMembers.first(where: { $0.groupMemberId == mds.groupMemberId }) {
return (mem.wrapped, mds.memberDeliveryStatus) return (mem, mds.memberDeliveryStatus)
} else { } else {
return nil return nil
} }
+52 -80
View File
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct ChatItemView: View { struct ChatItemView: View {
@ObservedObject var chat: Chat var chatInfo: ChatInfo
var chatItem: ChatItem var chatItem: ChatItem
var maxWidth: CGFloat = .infinity var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil @State var scrollProxy: ScrollViewProxy? = nil
@@ -19,19 +19,8 @@ struct ChatItemView: View {
@Binding var audioPlayer: AudioPlayer? @Binding var audioPlayer: AudioPlayer?
@Binding var playbackState: VoiceMessagePlaybackState @Binding var playbackState: VoiceMessagePlaybackState
@Binding var playbackTime: TimeInterval? @Binding var playbackTime: TimeInterval?
init( init(chatInfo: ChatInfo, chatItem: ChatItem, showMember: Bool = false, maxWidth: CGFloat = .infinity, scrollProxy: ScrollViewProxy? = nil, revealed: Binding<Bool>, allowMenu: Binding<Bool> = .constant(false), audioPlayer: Binding<AudioPlayer?> = .constant(nil), playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback), playbackTime: Binding<TimeInterval?> = .constant(nil)) {
chat: Chat, self.chatInfo = chatInfo
chatItem: ChatItem,
showMember: Bool = false,
maxWidth: CGFloat = .infinity,
scrollProxy: ScrollViewProxy? = nil,
revealed: Binding<Bool>,
allowMenu: Binding<Bool> = .constant(false),
audioPlayer: Binding<AudioPlayer?> = .constant(nil),
playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback),
playbackTime: Binding<TimeInterval?> = .constant(nil)
) {
self.chat = chat
self.chatItem = chatItem self.chatItem = chatItem
self.maxWidth = maxWidth self.maxWidth = maxWidth
_scrollProxy = .init(initialValue: scrollProxy) _scrollProxy = .init(initialValue: scrollProxy)
@@ -44,15 +33,15 @@ struct ChatItemView: View {
var body: some View { var body: some View {
let ci = chatItem let ci = chatItem
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) { if chatItem.meta.itemDeleted != nil && !revealed {
MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed) MarkedDeletedItemView(chatItem: chatItem)
} else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive { } else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
EmojiItemView(chat: chat, chatItem: ci) EmojiItemView(chatItem: ci)
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu) CIVoiceView(chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu)
} else if ci.content.msgContent == nil { } else if ci.content.msgContent == nil {
ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
} else { } else {
framedItemView() framedItemView()
} }
@@ -62,17 +51,15 @@ struct ChatItemView: View {
} }
private func framedItemView() -> some View { private func framedItemView() -> some View {
FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) FramedItemView(chatInfo: chatInfo, chatItem: chatItem, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
} }
} }
struct ChatItemContentView<Content: View>: View { struct ChatItemContentView<Content: View>: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@ObservedObject var chat: Chat var chatInfo: ChatInfo
var chatItem: ChatItem var chatItem: ChatItem
@Binding var revealed: Bool
var msgContentView: () -> Content var msgContentView: () -> Content
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
var body: some View { var body: some View {
switch chatItem.content { switch chatItem.content {
@@ -82,16 +69,11 @@ struct ChatItemContentView<Content: View>: View {
case .rcvDeleted: deletedItemView() case .rcvDeleted: deletedItemView()
case let .sndCall(status, duration): callItemView(status, duration) case let .sndCall(status, duration): callItemView(status, duration)
case let .rcvCall(status, duration): callItemView(status, duration) case let .rcvCall(status, duration): callItemView(status, duration)
case let .rcvIntegrityError(msgError): case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem)
if developerTools { case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
IntegrityErrorItemView(chat: chat, msgError: msgError, chatItem: chatItem)
} else {
ZStack {}
}
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(chat: chat, msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case .rcvDirectEvent: eventItemView() case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText)
case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem) case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem)
case .rcvGroupEvent: eventItemView() case .rcvGroupEvent: eventItemView()
case .sndGroupEvent: eventItemView() case .sndGroupEvent: eventItemView()
@@ -100,9 +82,9 @@ struct ChatItemContentView<Content: View>: View {
case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor) case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor)
case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor) case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor)
case let .rcvChatPreference(feature, allowed, param): case let .rcvChatPreference(feature, allowed, param):
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param) CIFeaturePreferenceView(chatItem: chatItem, feature: feature, allowed: allowed, param: param)
case let .sndChatPreference(feature, _, _): case let .sndChatPreference(feature, _, _):
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary) CIChatFeatureView(chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: .secondary)
case let .rcvGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor) case let .rcvGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor) case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red) case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
@@ -114,15 +96,15 @@ struct ChatItemContentView<Content: View>: View {
} }
private func deletedItemView() -> some View { private func deletedItemView() -> some View {
DeletedItemView(chat: chat, chatItem: chatItem) DeletedItemView(chatItem: chatItem)
} }
private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View { private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View {
CICallItemView(chat: chat, chatItem: chatItem, status: status, duration: duration) CICallItemView(chatInfo: chatInfo, chatItem: chatItem, status: status, duration: duration)
} }
private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View { private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View {
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chat.chatInfo.incognito) CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chatInfo.incognito)
} }
private func eventItemView() -> some View { private func eventItemView() -> some View {
@@ -130,9 +112,7 @@ struct ChatItemContentView<Content: View>: View {
} }
private func eventItemViewText() -> Text { private func eventItemViewText() -> Text {
if !revealed, let t = mergedGroupEventText { if let member = chatItem.memberDisplayName {
return chatEventText(t + Text(" ") + chatItem.timestampText)
} else if let member = chatItem.memberDisplayName {
return Text(member + " ") return Text(member + " ")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -144,42 +124,34 @@ struct ChatItemContentView<Content: View>: View {
} }
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View { private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor) CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor)
} }
private var mergedGroupEventText: Text? { private var membersConnectedItemText: Text {
let (count, ns) = chatModel.getConnectedMemberNames(chatItem) if let t = membersConnectedText {
let members: LocalizedStringKey = return chatEventText(t, chatItem.timestampText)
switch ns.count {
case 1: "\(ns[0]) connected"
case 2: "\(ns[0]) and \(ns[1]) connected"
case 3: "\(ns[0] + ", " + ns[1]) and \(ns[2]) connected"
default:
ns.count > 3
? "\(ns[0]), \(ns[1]) and \(ns.count - 2) other members connected"
: ""
}
return if count <= 1 {
nil
} else if ns.count == 0 {
Text("\(count) group events")
} else if count > ns.count {
Text(members) + Text(" ") + Text("and \(count - ns.count) other events")
} else { } else {
Text(members) return eventItemViewText()
} }
} }
}
func chatEventText(_ text: Text) -> Text { private var membersConnectedText: LocalizedStringKey? {
text let ns = chatModel.getConnectedMemberNames(chatItem)
.font(.caption) return ns.count > 3
.foregroundColor(.secondary) ? "\(ns[0]), \(ns[1]) and \(ns.count - 2) other members connected"
.fontWeight(.light) : ns.count == 3
? "\(ns[0] + ", " + ns[1]) and \(ns[2]) connected"
: ns.count == 2
? "\(ns[0]) and \(ns[1]) connected"
: nil
}
} }
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text { func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text {
chatEventText(Text(eventText) + Text(" ") + ts) (Text(eventText) + Text(" ") + ts)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
} }
func chatEventText(_ ci: ChatItem) -> Text { func chatEventText(_ ci: ChatItem) -> Text {
@@ -189,15 +161,15 @@ func chatEventText(_ ci: ChatItem) -> Text {
struct ChatItemView_Previews: PreviewProvider { struct ChatItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group{ Group{
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
} }
.previewLayout(.fixed(width: 360, height: 70)) .previewLayout(.fixed(width: 360, height: 70))
.environmentObject(Chat.sampleData) .environmentObject(Chat.sampleData)
@@ -209,7 +181,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil) let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil)
Group{ Group{
ChatItemView( ChatItemView(
chat: Chat.sampleData, chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem( chatItem: ChatItem(
chatDir: .directRcv, chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
@@ -220,7 +192,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
revealed: Binding.constant(true) revealed: Binding.constant(true)
) )
ChatItemView( ChatItemView(
chat: Chat.sampleData, chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem( chatItem: ChatItem(
chatDir: .directRcv, chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead), meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead),
@@ -231,7 +203,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
revealed: Binding.constant(true) revealed: Binding.constant(true)
) )
ChatItemView( ChatItemView(
chat: Chat.sampleData, chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem( chatItem: ChatItem(
chatDir: .directRcv, chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
@@ -242,7 +214,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
revealed: Binding.constant(true) revealed: Binding.constant(true)
) )
ChatItemView( ChatItemView(
chat: Chat.sampleData, chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem( chatItem: ChatItem(
chatDir: .directRcv, chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
@@ -253,7 +225,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
revealed: Binding.constant(true) revealed: Binding.constant(true)
) )
ChatItemView( ChatItemView(
chat: Chat.sampleData, chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem( chatItem: ChatItem(
chatDir: .directRcv, chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)), meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
+169 -353
View File
@@ -21,7 +21,9 @@ struct ChatView: View {
@State private var showChatInfoSheet: Bool = false @State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false @State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState() @State private var composeState = ComposeState()
@State private var deletingItem: ChatItem? = nil
@State private var keyboardVisible = false @State private var keyboardVisible = false
@State private var showDeleteMessage = false
@State private var connectionStats: ConnectionStats? @State private var connectionStats: ConnectionStats?
@State private var customUserProfile: Profile? @State private var customUserProfile: Profile?
@State private var connectionCode: String? @State private var connectionCode: String?
@@ -34,12 +36,7 @@ struct ChatView: View {
@State private var searchText: String = "" @State private var searchText: String = ""
@FocusState private var searchFocussed @FocusState private var searchFocussed
// opening GroupMemberInfoView on member icon // opening GroupMemberInfoView on member icon
@State private var membersLoaded = false @State private var selectedMember: GroupMember? = nil
@State private var selectedMember: GMember? = nil
// opening GroupLinkView on link button (incognito)
@State private var showGroupLinkSheet: Bool = false
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
var body: some View { var body: some View {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
@@ -94,10 +91,7 @@ struct ChatView: View {
chatModel.chatId = nil chatModel.chatId = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if chatModel.chatId == nil { if chatModel.chatId == nil {
chatModel.chatItemStatuses = [:]
chatModel.reversedChatItems = [] chatModel.reversedChatItems = []
chatModel.groupMembers = []
membersLoaded = false
} }
} }
} }
@@ -114,7 +108,7 @@ struct ChatView: View {
connectionStats = stats connectionStats = stats
customUserProfile = profile customUserProfile = profile
connectionCode = code connectionCode = code
if contact.activeConn?.connectionCode != ct.activeConn?.connectionCode { if contact.activeConn.connectionCode != ct.activeConn.connectionCode {
chat.chatInfo = .direct(contact: ct) chat.chatInfo = .direct(contact: ct)
} }
} }
@@ -135,21 +129,18 @@ struct ChatView: View {
} }
} else if case let .group(groupInfo) = cInfo { } else if case let .group(groupInfo) = cInfo {
Button { Button {
Task { await loadGroupMembers(groupInfo) { showChatInfoSheet = true } } Task {
let groupMembers = await apiListMembers(groupInfo.groupId)
await MainActor.run {
ChatModel.shared.groupMembers = groupMembers
showChatInfoSheet = true
}
}
} label: { } label: {
ChatInfoToolbar(chat: chat) ChatInfoToolbar(chat: chat)
} }
.appSheet(isPresented: $showChatInfoSheet) { .appSheet(isPresented: $showChatInfoSheet) {
GroupChatInfoView( GroupChatInfoView(chat: chat, groupInfo: groupInfo)
chat: chat,
groupInfo: Binding(
get: { groupInfo },
set: { gInfo in
chat.chatInfo = .group(groupInfo: gInfo)
chat.created = Date.now
}
)
)
} }
} }
} }
@@ -159,7 +150,7 @@ struct ChatView: View {
HStack { HStack {
if contact.allowsFeature(.calls) { if contact.allowsFeature(.calls) {
callButton(contact, .audio, imageName: "phone") callButton(contact, .audio, imageName: "phone")
.disabled(!contact.ready || !contact.active) .disabled(!contact.ready)
} }
Menu { Menu {
if contact.allowsFeature(.calls) { if contact.allowsFeature(.calls) {
@@ -168,11 +159,11 @@ struct ChatView: View {
} label: { } label: {
Label("Video call", systemImage: "video") Label("Video call", systemImage: "video")
} }
.disabled(!contact.ready || !contact.active) .disabled(!contact.ready)
} }
searchButton() searchButton()
toggleNtfsButton(chat) toggleNtfsButton(chat)
.disabled(!contact.ready || !contact.active) .disabled(!contact.ready)
} label: { } label: {
Image(systemName: "ellipsis") Image(systemName: "ellipsis")
} }
@@ -181,16 +172,9 @@ struct ChatView: View {
HStack { HStack {
if groupInfo.canAddMembers { if groupInfo.canAddMembers {
if (chat.chatInfo.incognito) { if (chat.chatInfo.incognito) {
groupLinkButton() Image(systemName: "person.crop.circle.badge.plus")
.appSheet(isPresented: $showGroupLinkSheet) { .foregroundColor(Color(uiColor: .tertiaryLabel))
GroupLinkView( .onTapGesture { AlertManager.shared.showAlert(cantInviteIncognitoAlert()) }
groupId: groupInfo.groupId,
groupLink: $groupLink,
groupLinkMemberRole: $groupLinkMemberRole,
showTitle: true,
creatingGroup: false
)
}
} else { } else {
addMembersButton() addMembersButton()
.appSheet(isPresented: $showAddMembersSheet) { .appSheet(isPresented: $showAddMembersSheet) {
@@ -212,17 +196,6 @@ struct ChatView: View {
} }
} }
private func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async {
let groupMembers = await apiListMembers(groupInfo.groupId)
await MainActor.run {
if chatModel.chatId == groupInfo.id {
chatModel.groupMembers = groupMembers.map { GMember.init($0) }
membersLoaded = true
updateView()
}
}
}
private func initChatView() { private func initChatView() {
let cInfo = chat.chatInfo let cInfo = chat.chatInfo
if case let .direct(contact) = cInfo { if case let .direct(contact) = cInfo {
@@ -348,7 +321,6 @@ struct ChatView: View {
@ViewBuilder private func connectingText() -> some View { @ViewBuilder private func connectingText() -> some View {
if case let .direct(contact) = chat.chatInfo, if case let .direct(contact) = chat.chatInfo,
!contact.ready, !contact.ready,
contact.active,
!contact.nextSendGrpInv { !contact.nextSendGrpInv {
Text("connecting…") Text("connecting…")
.font(.caption) .font(.caption)
@@ -431,32 +403,19 @@ struct ChatView: View {
private func addMembersButton() -> some View { private func addMembersButton() -> some View {
Button { Button {
if case let .group(gInfo) = chat.chatInfo { if case let .group(gInfo) = chat.chatInfo {
Task { await loadGroupMembers(gInfo) { showAddMembersSheet = true } } Task {
let groupMembers = await apiListMembers(gInfo.groupId)
await MainActor.run {
ChatModel.shared.groupMembers = groupMembers
showAddMembersSheet = true
}
}
} }
} label: { } label: {
Image(systemName: "person.crop.circle.badge.plus") Image(systemName: "person.crop.circle.badge.plus")
} }
} }
private func groupLinkButton() -> some View {
Button {
if case let .group(gInfo) = chat.chatInfo {
Task {
do {
if let link = try apiGetGroupLink(gInfo.groupId) {
(groupLink, groupLinkMemberRole) = link
}
} catch let error {
logger.error("ChatView apiGetGroupLink: \(responseError(error))")
}
showGroupLinkSheet = true
}
}
} label: {
Image(systemName: "link.badge.plus")
}
}
private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) { private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) {
if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id { if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id {
if loadingItems || firstPage { return } if loadingItems || firstPage { return }
@@ -486,30 +445,73 @@ struct ChatView: View {
} }
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci)
if ci.memberConnected != nil && nextItem?.memberConnected != nil {
// memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
ZStack {} // scroll doesn't work if it's EmptyView()
} else {
if prevItem == nil || showMemberImage(member, prevItem) {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
Text(member.displayName)
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, memberImageSize + 14)
.padding(.top, 7)
}
HStack(alignment: .top, spacing: 8) {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
chatItemWithMenu(ci, maxWidth)
}
}
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, 12)
} else {
chatItemWithMenu(ci, maxWidth)
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, memberImageSize + 8 + 12)
}
}
} else {
chatItemWithMenu(ci, maxWidth)
.padding(.horizontal)
.padding(.top, 5)
}
}
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
ChatItemWithMenu( ChatItemWithMenu(
chat: chat, ci: ci,
chatItem: ci,
maxWidth: maxWidth, maxWidth: maxWidth,
scrollProxy: scrollProxy,
deleteMessage: deleteMessage,
deletingItem: $deletingItem,
composeState: $composeState, composeState: $composeState,
selectedMember: $selectedMember, showDeleteMessage: $showDeleteMessage
chatView: self
) )
.environmentObject(chat)
} }
private struct ChatItemWithMenu: View { private struct ChatItemWithMenu: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var chat: Chat
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat var ci: ChatItem
var chatItem: ChatItem
var maxWidth: CGFloat var maxWidth: CGFloat
var scrollProxy: ScrollViewProxy?
var deleteMessage: (CIDeleteMode) -> Void
@Binding var deletingItem: ChatItem?
@Binding var composeState: ComposeState @Binding var composeState: ComposeState
@Binding var selectedMember: GMember? @Binding var showDeleteMessage: Bool
var chatView: ChatView
@State private var deletingItem: ChatItem? = nil
@State private var showDeleteMessage = false
@State private var deletingItems: [Int64] = []
@State private var showDeleteMessages = false
@State private var revealed = false @State private var revealed = false
@State private var showChatItemInfoSheet: Bool = false @State private var showChatItemInfoSheet: Bool = false
@State private var chatItemInfo: ChatItemInfo? @State private var chatItemInfo: ChatItemInfo?
@@ -521,114 +523,18 @@ struct ChatView: View {
@State private var playbackTime: TimeInterval? @State private var playbackTime: TimeInterval?
var body: some View { var body: some View {
let (currIndex, nextItem) = m.getNextChatItem(chatItem)
let ciCategory = chatItem.mergeCategory
if (ciCategory != nil && ciCategory == nextItem?.mergeCategory) {
// memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView
ZStack {} // scroll doesn't work if it's EmptyView()
} else {
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
let range = itemsRange(currIndex, prevHidden)
if revealed, let range = range {
let items = Array(zip(Array(range), m.reversedChatItems[range]))
ForEach(items, id: \.1.viewId) { (i, ci) in
let prev = i == prevHidden ? prevItem : m.reversedChatItems[i + 1]
chatItemView(ci, nil, prev)
}
} else {
chatItemView(chatItem, range, prevItem)
}
}
}
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?) -> some View {
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let (prevMember, memCount): (GroupMember?, Int) =
if let range = range {
m.getPrevHiddenMember(member, range)
} else {
(nil, 1)
}
if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
Text(memberNames(member, prevMember, memCount))
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, memberImageSize + 14)
.padding(.top, 7)
}
HStack(alignment: .top, spacing: 8) {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture {
if chatView.membersLoaded {
selectedMember = m.getGroupMember(member.groupMemberId)
} else {
Task {
await chatView.loadGroupMembers(groupInfo) {
selectedMember = m.getGroupMember(member.groupMemberId)
}
}
}
}
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true)
}
chatItemWithMenu(ci, range, maxWidth)
}
}
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, 12)
} else {
chatItemWithMenu(ci, range, maxWidth)
.padding(.top, 5)
.padding(.trailing)
.padding(.leading, memberImageSize + 8 + 12)
}
} else {
chatItemWithMenu(ci, range, maxWidth)
.padding(.horizontal)
.padding(.top, 5)
}
}
private func memberNames(_ member: GroupMember, _ prevMember: GroupMember?, _ memCount: Int) -> LocalizedStringKey {
let name = member.displayName
return if let prevName = prevMember?.displayName {
memCount > 2
? "\(name), \(prevName) and \(memCount - 2) members"
: "\(name) and \(prevName)"
} else {
"\(name)"
}
}
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat) -> some View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
let uiMenu: Binding<UIMenu> = Binding( let uiMenu: Binding<UIMenu> = Binding(
get: { UIMenu(title: "", children: menu(ci, range, live: composeState.liveMessage != nil)) }, get: { UIMenu(title: "", children: menu(live: composeState.liveMessage != nil)) },
set: { _ in } set: { _ in }
) )
VStack(alignment: alignment.horizontal, spacing: 3) { VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView( ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
chat: chat, .uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
chatItem: ci, .accessibilityLabel("")
maxWidth: maxWidth,
scrollProxy: chatView.scrollProxy,
revealed: $revealed,
allowMenu: $allowMenu,
audioPlayer: $audioPlayer,
playbackState: $playbackState,
playbackTime: $playbackTime
)
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
chatItemReactions(ci) chatItemReactions()
.padding(.bottom, 4) .padding(.bottom, 4)
} }
} }
@@ -642,11 +548,6 @@ struct ChatView: View {
} }
} }
} }
.confirmationDialog(deleteMessagesTitle, isPresented: $showDeleteMessages, titleVisibility: .visible) {
Button("Delete for me", role: .destructive) {
deleteMessages()
}
}
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
.onDisappear { .onDisappear {
@@ -664,15 +565,7 @@ struct ChatView: View {
} }
} }
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool { private func chatItemReactions() -> some View {
switch (prevItem?.chatDir) {
case .groupSnd: return true
case let .groupRcv(prevMember): return prevMember.groupMemberId != member.groupMemberId
default: return false
}
}
private func chatItemReactions(_ ci: ChatItem) -> some View {
HStack(spacing: 4) { HStack(spacing: 4) {
ForEach(ci.reactions, id: \.reaction) { r in ForEach(ci.reactions, id: \.reaction) { r in
let v = HStack(spacing: 4) { let v = HStack(spacing: 4) {
@@ -692,7 +585,7 @@ struct ChatView: View {
if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) { if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) {
v.onTapGesture { v.onTapGesture {
setReaction(ci, add: !r.userReacted, reaction: r.reaction) setReaction(add: !r.userReacted, reaction: r.reaction)
} }
} else { } else {
v v
@@ -701,10 +594,10 @@ struct ChatView: View {
} }
} }
private func menu(_ ci: ChatItem, _ range: ClosedRange<Int>?, live: Bool) -> [UIMenuElement] { private func menu(live: Bool) -> [UIMenuElement] {
var menu: [UIMenuElement] = [] var menu: [UIMenuElement] = []
if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed { if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed {
let rs = allReactions(ci) let rs = allReactions()
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction, if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction,
rs.count > 0 { rs.count > 0 {
var rm: UIMenu var rm: UIMenu
@@ -721,10 +614,10 @@ struct ChatView: View {
menu.append(rm) menu.append(rm)
} }
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live { if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
menu.append(replyUIAction(ci)) menu.append(replyUIAction())
} }
menu.append(shareUIAction(ci)) menu.append(shareUIAction())
menu.append(copyUIAction(ci)) menu.append(copyUIAction())
if let fileSource = getLoadedFileSource(ci.file) { if let fileSource = getLoadedFileSource(ci.file) {
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
if image.imageData != nil { if image.imageData != nil {
@@ -737,9 +630,9 @@ struct ChatView: View {
} }
} }
if ci.meta.editable && !mc.isVoice && !live { if ci.meta.editable && !mc.isVoice && !live {
menu.append(editAction(ci)) menu.append(editAction())
} }
menu.append(viewInfoUIAction(ci)) menu.append(viewInfoUIAction())
if revealed { if revealed {
menu.append(hideUIAction()) menu.append(hideUIAction())
} }
@@ -749,31 +642,25 @@ struct ChatView: View {
menu.append(cancelFileUIAction(file.fileId, cancelAction)) menu.append(cancelFileUIAction(file.fileId, cancelAction))
} }
if !live || !ci.meta.isLive { if !live || !ci.meta.isLive {
menu.append(deleteUIAction(ci)) menu.append(deleteUIAction())
} }
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) {
menu.append(moderateUIAction(ci, groupInfo)) menu.append(moderateUIAction(groupInfo))
} }
} else if ci.meta.itemDeleted != nil { } else if ci.meta.itemDeleted != nil {
if revealed { if !ci.isDeletedContent {
menu.append(hideUIAction())
} else if !ci.isDeletedContent {
menu.append(revealUIAction()) menu.append(revealUIAction())
} else if range != nil {
menu.append(expandUIAction())
} }
menu.append(viewInfoUIAction(ci)) menu.append(viewInfoUIAction())
menu.append(deleteUIAction(ci)) menu.append(deleteUIAction())
} else if ci.isDeletedContent { } else if ci.isDeletedContent {
menu.append(viewInfoUIAction(ci)) menu.append(viewInfoUIAction())
menu.append(deleteUIAction(ci)) menu.append(deleteUIAction())
} else if ci.mergeCategory != nil && ((range?.count ?? 0) > 1 || revealed) {
menu.append(revealed ? shrinkUIAction() : expandUIAction())
} }
return menu return menu
} }
private func replyUIAction(_ ci: ChatItem) -> UIAction { private func replyUIAction() -> UIAction {
UIAction( UIAction(
title: NSLocalizedString("Reply", comment: "chat item action"), title: NSLocalizedString("Reply", comment: "chat item action"),
image: UIImage(systemName: "arrowshape.turn.up.left") image: UIImage(systemName: "arrowshape.turn.up.left")
@@ -808,11 +695,11 @@ struct ChatView: View {
) )
} }
private func allReactions(_ ci: ChatItem) -> [UIAction] { private func allReactions() -> [UIAction] {
MsgReaction.values.compactMap { r in MsgReaction.values.compactMap { r in
ci.reactions.contains(where: { $0.userReacted && $0.reaction == r }) ci.reactions.contains(where: { $0.userReacted && $0.reaction == r })
? nil ? nil
: UIAction(title: r.text) { _ in setReaction(ci, add: true, reaction: r) } : UIAction(title: r.text) { _ in setReaction(add: true, reaction: r) }
} }
} }
@@ -820,7 +707,7 @@ struct ChatView: View {
rs.count > 4 ? 3 : 4 rs.count > 4 ? 3 : 4
} }
private func setReaction(_ ci: ChatItem, add: Bool, reaction: MsgReaction) { private func setReaction(add: Bool, reaction: MsgReaction) {
Task { Task {
do { do {
let cInfo = chat.chatInfo let cInfo = chat.chatInfo
@@ -832,7 +719,7 @@ struct ChatView: View {
reaction: reaction reaction: reaction
) )
await MainActor.run { await MainActor.run {
m.updateChatItem(chat.chatInfo, chatItem) ChatModel.shared.updateChatItem(chat.chatInfo, chatItem)
} }
} catch let error { } catch let error {
logger.error("apiChatItemReaction error: \(responseError(error))") logger.error("apiChatItemReaction error: \(responseError(error))")
@@ -840,7 +727,7 @@ struct ChatView: View {
} }
} }
private func shareUIAction(_ ci: ChatItem) -> UIAction { private func shareUIAction() -> UIAction {
UIAction( UIAction(
title: NSLocalizedString("Share", comment: "chat item action"), title: NSLocalizedString("Share", comment: "chat item action"),
image: UIImage(systemName: "square.and.arrow.up") image: UIImage(systemName: "square.and.arrow.up")
@@ -853,7 +740,7 @@ struct ChatView: View {
} }
} }
private func copyUIAction(_ ci: ChatItem) -> UIAction { private func copyUIAction() -> UIAction {
UIAction( UIAction(
title: NSLocalizedString("Copy", comment: "chat item action"), title: NSLocalizedString("Copy", comment: "chat item action"),
image: UIImage(systemName: "doc.on.doc") image: UIImage(systemName: "doc.on.doc")
@@ -886,7 +773,7 @@ struct ChatView: View {
} }
} }
private func editAction(_ ci: ChatItem) -> UIAction { private func editAction() -> UIAction {
UIAction( UIAction(
title: NSLocalizedString("Edit", comment: "chat item action"), title: NSLocalizedString("Edit", comment: "chat item action"),
image: UIImage(systemName: "square.and.pencil") image: UIImage(systemName: "square.and.pencil")
@@ -897,7 +784,7 @@ struct ChatView: View {
} }
} }
private func viewInfoUIAction(_ ci: ChatItem) -> UIAction { private func viewInfoUIAction() -> UIAction {
UIAction( UIAction(
title: NSLocalizedString("Info", comment: "chat item action"), title: NSLocalizedString("Info", comment: "chat item action"),
image: UIImage(systemName: "info.circle") image: UIImage(systemName: "info.circle")
@@ -910,7 +797,10 @@ struct ChatView: View {
chatItemInfo = ciInfo chatItemInfo = ciInfo
} }
if case let .group(gInfo) = chat.chatInfo { if case let .group(gInfo) = chat.chatInfo {
await chatView.loadGroupMembers(gInfo) let groupMembers = await apiListMembers(gInfo.groupId)
await MainActor.run {
ChatModel.shared.groupMembers = groupMembers
}
} }
} catch let error { } catch let error {
logger.error("apiGetChatItemInfo error: \(responseError(error))") logger.error("apiGetChatItemInfo error: \(responseError(error))")
@@ -931,7 +821,7 @@ struct ChatView: View {
message: Text(cancelAction.alert.message), message: Text(cancelAction.alert.message),
primaryButton: .destructive(Text(cancelAction.alert.confirm)) { primaryButton: .destructive(Text(cancelAction.alert.confirm)) {
Task { Task {
if let user = m.currentUser { if let user = ChatModel.shared.currentUser {
await cancelFile(user: user, fileId: fileId) await cancelFile(user: user, fileId: fileId)
} }
} }
@@ -952,45 +842,18 @@ struct ChatView: View {
} }
} }
private func deleteUIAction(_ ci: ChatItem) -> UIAction { private func deleteUIAction() -> UIAction {
UIAction( UIAction(
title: NSLocalizedString("Delete", comment: "chat item action"), title: NSLocalizedString("Delete", comment: "chat item action"),
image: UIImage(systemName: "trash"), image: UIImage(systemName: "trash"),
attributes: [.destructive] attributes: [.destructive]
) { _ in ) { _ in
if !revealed && ci.meta.itemDeleted != nil, showDeleteMessage = true
let currIndex = m.getChatItemIndex(ci), deletingItem = ci
let ciCategory = ci.mergeCategory {
let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory)
if let range = itemsRange(currIndex, prevHidden) {
var itemIds: [Int64] = []
for i in range {
itemIds.append(m.reversedChatItems[i].id)
}
showDeleteMessages = true
deletingItems = itemIds
} else {
showDeleteMessage = true
deletingItem = ci
}
} else {
showDeleteMessage = true
deletingItem = ci
}
} }
} }
private func itemsRange(_ currIndex: Int?, _ prevHidden: Int?) -> ClosedRange<Int>? { private func moderateUIAction(_ groupInfo: GroupInfo) -> UIAction {
if let currIndex = currIndex,
let prevHidden = prevHidden,
prevHidden > currIndex {
currIndex...prevHidden
} else {
nil
}
}
private func moderateUIAction(_ ci: ChatItem, _ groupInfo: GroupInfo) -> UIAction {
UIAction( UIAction(
title: NSLocalizedString("Moderate", comment: "chat item action"), title: NSLocalizedString("Moderate", comment: "chat item action"),
image: UIImage(systemName: "flag"), image: UIImage(systemName: "flag"),
@@ -1022,105 +885,20 @@ struct ChatView: View {
} }
} }
} }
private func expandUIAction() -> UIAction {
UIAction(
title: NSLocalizedString("Expand", comment: "chat item action"),
image: UIImage(systemName: "arrow.up.and.line.horizontal.and.arrow.down")
) { _ in
withAnimation {
revealed = true
}
}
}
private func shrinkUIAction() -> UIAction {
UIAction(
title: NSLocalizedString("Hide", comment: "chat item action"),
image: UIImage(systemName: "arrow.down.and.line.horizontal.and.arrow.up")
) { _ in
withAnimation {
revealed = false
}
}
}
private var broadcastDeleteButtonText: LocalizedStringKey { private var broadcastDeleteButtonText: LocalizedStringKey {
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
} }
var deleteMessagesTitle: LocalizedStringKey {
let n = deletingItems.count
return n == 1 ? "Delete message?" : "Delete \(n) messages?"
}
private func deleteMessages() {
let itemIds = deletingItems
if itemIds.count > 0 {
let chatInfo = chat.chatInfo
Task {
var deletedItems: [ChatItem] = []
for itemId in itemIds {
do {
let (di, _) = try await apiDeleteChatItem(
type: chatInfo.chatType,
id: chatInfo.apiId,
itemId: itemId,
mode: .cidmInternal
)
deletedItems.append(di)
} catch {
logger.error("ChatView.deleteMessage error: \(error.localizedDescription)")
}
}
await MainActor.run {
for di in deletedItems {
m.removeChatItem(chatInfo, di)
}
}
}
}
}
private func deleteMessage(_ mode: CIDeleteMode) {
logger.debug("ChatView deleteMessage")
Task {
logger.debug("ChatView deleteMessage: in Task")
do {
if let di = deletingItem {
var deletedItem: ChatItem
var toItem: ChatItem?
if case .cidmBroadcast = mode,
let (groupInfo, groupMember) = di.memberToModerate(chat.chatInfo) {
(deletedItem, toItem) = try await apiDeleteMemberChatItem(
groupId: groupInfo.apiId,
groupMemberId: groupMember.groupMemberId,
itemId: di.id
)
} else {
(deletedItem, toItem) = try await apiDeleteChatItem(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemId: di.id,
mode: mode
)
}
DispatchQueue.main.async {
deletingItem = nil
if let toItem = toItem {
_ = m.upsertChatItem(chat.chatInfo, toItem)
} else {
m.removeChatItem(chat.chatInfo, deletedItem)
}
}
}
} catch {
logger.error("ChatView.deleteMessage error: \(error.localizedDescription)")
}
}
}
} }
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
switch (prevItem?.chatDir) {
case .groupSnd: return true
case let .groupRcv(prevMember): return prevMember.groupMemberId != member.groupMemberId
default: return false
}
}
private func scrollToBottom(_ proxy: ScrollViewProxy) { private func scrollToBottom(_ proxy: ScrollViewProxy) {
if let ci = chatModel.reversedChatItems.first { if let ci = chatModel.reversedChatItems.first {
withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) }
@@ -1132,6 +910,44 @@ struct ChatView: View {
withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) }
} }
} }
private func deleteMessage(_ mode: CIDeleteMode) {
logger.debug("ChatView deleteMessage")
Task {
logger.debug("ChatView deleteMessage: in Task")
do {
if let di = deletingItem {
var deletedItem: ChatItem
var toItem: ChatItem?
if case .cidmBroadcast = mode,
let (groupInfo, groupMember) = di.memberToModerate(chat.chatInfo) {
(deletedItem, toItem) = try await apiDeleteMemberChatItem(
groupId: groupInfo.apiId,
groupMemberId: groupMember.groupMemberId,
itemId: di.id
)
} else {
(deletedItem, toItem) = try await apiDeleteChatItem(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemId: di.id,
mode: mode
)
}
DispatchQueue.main.async {
deletingItem = nil
if let toItem = toItem {
_ = chatModel.upsertChatItem(chat.chatInfo, toItem)
} else {
chatModel.removeChatItem(chat.chatInfo, deletedItem)
}
}
}
} catch {
logger.error("ChatView.deleteMessage error: \(error.localizedDescription)")
}
}
}
} }
@ViewBuilder func toggleNtfsButton(_ chat: Chat) -> some View { @ViewBuilder func toggleNtfsButton(_ chat: Chat) -> some View {
@@ -1148,7 +964,7 @@ struct ChatView: View {
func toggleNotifications(_ chat: Chat, enableNtfs: Bool) { func toggleNotifications(_ chat: Chat, enableNtfs: Bool) {
var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults
chatSettings.enableNtfs = enableNtfs ? .all : .none chatSettings.enableNtfs = enableNtfs
updateChatSettings(chat, chatSettings: chatSettings) updateChatSettings(chat, chatSettings: chatSettings)
} }
@@ -592,14 +592,12 @@ struct ComposeView: View {
EmptyView() EmptyView()
case let .quotedItem(chatItem: quotedItem): case let .quotedItem(chatItem: quotedItem):
ContextItemView( ContextItemView(
chat: chat,
contextItem: quotedItem, contextItem: quotedItem,
contextIcon: "arrowshape.turn.up.left", contextIcon: "arrowshape.turn.up.left",
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) } cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
) )
case let .editingItem(chatItem: editingItem): case let .editingItem(chatItem: editingItem):
ContextItemView( ContextItemView(
chat: chat,
contextItem: editingItem, contextItem: editingItem,
contextIcon: "pencil", contextIcon: "pencil",
cancelContextItem: { clearState() } cancelContextItem: { clearState() }
@@ -11,7 +11,6 @@ import SimpleXChat
struct ContextItemView: View { struct ContextItemView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
let contextItem: ChatItem let contextItem: ChatItem
let contextIcon: String let contextIcon: String
let cancelContextItem: () -> Void let cancelContextItem: () -> Void
@@ -49,7 +48,6 @@ struct ContextItemView: View {
private func msgContentView(lines: Int) -> some View { private func msgContentView(lines: Int) -> some View {
MsgContentView( MsgContentView(
chat: chat,
text: contextItem.text, text: contextItem.text,
formattedText: contextItem.formattedText formattedText: contextItem.formattedText
) )
@@ -61,6 +59,6 @@ struct ContextItemView: View {
struct ContextItemView_Previews: PreviewProvider { struct ContextItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello") let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
return ContextItemView(chat: Chat.sampleData, contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {}) return ContextItemView(contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {})
} }
} }
@@ -14,28 +14,20 @@ import PhotosUI
struct NativeTextEditor: UIViewRepresentable { struct NativeTextEditor: UIViewRepresentable {
@Binding var text: String @Binding var text: String
@Binding var disableEditing: Bool @Binding var disableEditing: Bool
@Binding var height: CGFloat let height: CGFloat
let font: UIFont
@Binding var focused: Bool @Binding var focused: Bool
let alignment: TextAlignment let alignment: TextAlignment
let onImagesAdded: ([UploadContent]) -> Void let onImagesAdded: ([UploadContent]) -> Void
private let minHeight: CGFloat = 37
private let defaultHeight: CGFloat = {
let field = CustomUITextField(height: Binding.constant(0))
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
return min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, 37), 360).rounded(.down)
}()
func makeUIView(context: Context) -> UITextView { func makeUIView(context: Context) -> UITextView {
let field = CustomUITextField(height: _height) let field = CustomUITextField()
field.text = text field.text = text
field.font = font
field.textAlignment = alignment == .leading ? .left : .right field.textAlignment = alignment == .leading ? .left : .right
field.autocapitalizationType = .sentences field.autocapitalizationType = .sentences
field.setOnTextChangedListener { newText, images in field.setOnTextChangedListener { newText, images in
if !disableEditing { if !disableEditing {
// Speed up the process of updating layout, reduce jumping content on screen
if !isShortEmoji(newText) { updateHeight(field) }
text = newText text = newText
} else { } else {
field.text = text field.text = text
@@ -47,72 +39,24 @@ struct NativeTextEditor: UIViewRepresentable {
field.setOnFocusChangedListener { focused = $0 } field.setOnFocusChangedListener { focused = $0 }
field.delegate = field field.delegate = field
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4) field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
updateFont(field)
updateHeight(field)
return field return field
} }
func updateUIView(_ field: UITextView, context: Context) { func updateUIView(_ field: UITextView, context: Context) {
field.text = text field.text = text
field.font = font
field.textAlignment = alignment == .leading ? .left : .right field.textAlignment = alignment == .leading ? .left : .right
updateFont(field)
updateHeight(field)
}
private func updateHeight(_ field: UITextView) {
let maxHeight = min(360, field.font!.lineHeight * 12)
// When having emoji in text view and then removing it, sizeThatFits shows previous size (too big for empty text view), so using work around with default size
let newHeight = field.text == ""
? defaultHeight
: min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, minHeight), maxHeight).rounded(.down)
if field.frame.size.height != newHeight {
field.frame.size = CGSizeMake(field.frame.size.width, newHeight)
(field as! CustomUITextField).invalidateIntrinsicContentHeight(newHeight)
}
}
private func updateFont(_ field: UITextView) {
field.font = isShortEmoji(field.text)
? (field.text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont)
: UIFont.preferredFont(forTextStyle: .body)
} }
} }
private class CustomUITextField: UITextView, UITextViewDelegate { private class CustomUITextField: UITextView, UITextViewDelegate {
var height: Binding<CGFloat>
var newHeight: CGFloat = 0
var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in } var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in }
var onFocusChanged: (Bool) -> Void = { focused in } var onFocusChanged: (Bool) -> Void = { focused in }
init(height: Binding<CGFloat>) {
self.height = height
super.init(frame: .zero, textContainer: nil)
}
required init?(coder: NSCoder) {
fatalError("Not implemented")
}
// This func here needed because using frame.size.height in intrinsicContentSize while loading a screen with text (for example. when you have a draft),
// produces incorrect height because at that point intrinsicContentSize has old value of frame.size.height even if it was set to new value right before the call
// (who knows why...)
func invalidateIntrinsicContentHeight(_ newHeight: CGFloat) {
self.newHeight = newHeight
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
if height.wrappedValue != newHeight {
DispatchQueue.main.asyncAfter(deadline: .now(), execute: { self.height.wrappedValue = self.newHeight })
}
return CGSizeMake(0, newHeight)
}
func setOnTextChangedListener(onTextChanged: @escaping (String, [UploadContent]) -> Void) { func setOnTextChangedListener(onTextChanged: @escaping (String, [UploadContent]) -> Void) {
self.onTextChanged = onTextChanged self.onTextChanged = onTextChanged
} }
func setOnFocusChangedListener(onFocusChanged: @escaping (Bool) -> Void) { func setOnFocusChangedListener(onFocusChanged: @escaping (Bool) -> Void) {
self.onFocusChanged = onFocusChanged self.onFocusChanged = onFocusChanged
} }
@@ -200,14 +144,14 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
struct NativeTextEditor_Previews: PreviewProvider{ struct NativeTextEditor_Previews: PreviewProvider{
static var previews: some View { static var previews: some View {
NativeTextEditor( return NativeTextEditor(
text: Binding.constant("Hello, world!"), text: Binding.constant("Hello, world!"),
disableEditing: Binding.constant(false), disableEditing: Binding.constant(false),
height: Binding.constant(100), height: 100,
font: UIFont.preferredFont(forTextStyle: .body),
focused: Binding.constant(false), focused: Binding.constant(false),
alignment: TextAlignment.leading, alignment: TextAlignment.leading,
onImagesAdded: { _ in } onImagesAdded: { _ in }
) )
.fixedSize(horizontal: false, vertical: true)
} }
} }
@@ -32,12 +32,15 @@ struct SendMessageView: View {
var sendButtonColor = Color.accentColor var sendButtonColor = Color.accentColor
@State private var teHeight: CGFloat = 42 @State private var teHeight: CGFloat = 42
@State private var teFont: Font = .body @State private var teFont: Font = .body
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
@State private var sendButtonSize: CGFloat = 29 @State private var sendButtonSize: CGFloat = 29
@State private var sendButtonOpacity: CGFloat = 1 @State private var sendButtonOpacity: CGFloat = 1
@State private var showCustomDisappearingMessageDialogue = false @State private var showCustomDisappearingMessageDialogue = false
@State private var showCustomTimePicker = false @State private var showCustomTimePicker = false
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get() @State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
@State private var progressByTimeout = false @State private var progressByTimeout = false
var maxHeight: CGFloat = 360
var minHeight: CGFloat = 37
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false @AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
var body: some View { var body: some View {
@@ -54,16 +57,30 @@ struct SendMessageView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} else { } else {
let alignment: TextAlignment = isRightToLeft(composeState.message) ? .trailing : .leading let alignment: TextAlignment = isRightToLeft(composeState.message) ? .trailing : .leading
Text(composeState.message)
.lineLimit(10)
.font(teFont)
.multilineTextAlignment(alignment)
// put text on top (after NativeTextEditor) and set color to precisely align it on changes
// .foregroundColor(.red)
.foregroundColor(.clear)
.padding(.horizontal, 10)
.padding(.top, 8)
.padding(.bottom, 6)
.matchedGeometryEffect(id: "te", in: namespace)
.background(GeometryReader(content: updateHeight))
NativeTextEditor( NativeTextEditor(
text: $composeState.message, text: $composeState.message,
disableEditing: $composeState.inProgress, disableEditing: $composeState.inProgress,
height: $teHeight, height: teHeight,
font: teUiFont,
focused: $keyboardVisible, focused: $keyboardVisible,
alignment: alignment, alignment: alignment,
onImagesAdded: onMediaAdded onImagesAdded: onMediaAdded
) )
.allowsTightening(false) .allowsTightening(false)
.fixedSize(horizontal: false, vertical: true) .frame(height: teHeight)
} }
} }
@@ -83,13 +100,11 @@ struct SendMessageView: View {
.frame(height: teHeight, alignment: .bottom) .frame(height: teHeight, alignment: .bottom)
} }
} }
.padding(.vertical, 1)
.overlay(
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)
) .frame(height: teHeight)
} }
.onChange(of: composeState.message, perform: { text in updateFont(text) })
.onChange(of: composeState.inProgress) { inProgress in .onChange(of: composeState.inProgress) { inProgress in
if inProgress { if inProgress {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
@@ -400,12 +415,16 @@ struct SendMessageView: View {
.padding([.bottom, .trailing], 4) .padding([.bottom, .trailing], 4)
} }
private func updateFont(_ text: String) { private func updateHeight(_ g: GeometryProxy) -> Color {
DispatchQueue.main.async { DispatchQueue.main.async {
teFont = isShortEmoji(text) teHeight = min(max(g.frame(in: .local).size.height, minHeight), maxHeight)
? (text.count < 4 ? largeEmojiFont : mediumEmojiFont) (teFont, teUiFont) = isShortEmoji(composeState.message)
: .body ? composeState.message.count < 4
? (largeEmojiFont, largeEmojiUIFont)
: (mediumEmojiFont, mediumEmojiUIFont)
: (.body, UIFont.preferredFont(forTextStyle: .body))
} }
return Color.clear
} }
} }
@@ -144,7 +144,7 @@ struct AddGroupMembersViewCommon: View {
do { do {
for contactId in selectedContacts { for contactId in selectedContacts {
let member = try await apiAddMember(groupInfo.groupId, contactId, selectedRole) let member = try await apiAddMember(groupInfo.groupId, contactId, selectedRole)
await MainActor.run { _ = chatModel.upsertGroupMember(groupInfo, member) } await MainActor.run { _ = ChatModel.shared.upsertGroupMember(groupInfo, member) }
} }
addedMembersCb(selectedContacts) addedMembersCb(selectedContacts)
} catch { } catch {
@@ -157,7 +157,7 @@ struct AddGroupMembersViewCommon: View {
private func rolePicker() -> some View { private func rolePicker() -> some View {
Picker("New member role", selection: $selectedRole) { Picker("New member role", selection: $selectedRole) {
ForEach(GroupMemberRole.allCases) { role in ForEach(GroupMemberRole.allCases) { role in
if role <= groupInfo.membership.memberRole && role != .author { if role <= groupInfo.membership.memberRole {
Text(role.text) Text(role.text)
} }
} }
@@ -15,7 +15,7 @@ struct GroupChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@Binding var groupInfo: GroupInfo @State var groupInfo: GroupInfo
@ObservedObject private var alertManager = AlertManager.shared @ObservedObject private var alertManager = AlertManager.shared
@State private var alert: GroupChatInfoViewAlert? = nil @State private var alert: GroupChatInfoViewAlert? = nil
@State private var groupLink: String? @State private var groupLink: String?
@@ -35,30 +35,14 @@ struct GroupChatInfoView: View {
case leaveGroupAlert case leaveGroupAlert
case cantInviteIncognitoAlert case cantInviteIncognitoAlert
case largeGroupReceiptsDisabled case largeGroupReceiptsDisabled
case blockMemberAlert(mem: GroupMember)
case unblockMemberAlert(mem: GroupMember)
case removeMemberAlert(mem: GroupMember)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
var id: String { var id: GroupChatInfoViewAlert { get { self } }
switch self {
case .deleteGroupAlert: return "deleteGroupAlert"
case .clearChatAlert: return "clearChatAlert"
case .leaveGroupAlert: return "leaveGroupAlert"
case .cantInviteIncognitoAlert: return "cantInviteIncognitoAlert"
case .largeGroupReceiptsDisabled: return "largeGroupReceiptsDisabled"
case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)"
case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)"
case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)"
case let .error(title, _): return "error \(title)"
}
}
} }
var body: some View { var body: some View {
NavigationView { NavigationView {
let members = chatModel.groupMembers let members = chatModel.groupMembers
.filter { m in let status = m.wrapped.memberStatus; return status != .memLeft && status != .memRemoved } .filter { $0.memberStatus != .memLeft && $0.memberStatus != .memRemoved }
.sorted { $0.displayName.lowercased() < $1.displayName.lowercased() } .sorted { $0.displayName.lowercased() < $1.displayName.lowercased() }
List { List {
@@ -73,7 +57,7 @@ struct GroupChatInfoView: View {
addOrEditWelcomeMessage() addOrEditWelcomeMessage()
} }
groupPreferencesButton($groupInfo) groupPreferencesButton($groupInfo)
if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { if members.filter({ $0.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
sendReceiptsOption() sendReceiptsOption()
} else { } else {
sendReceiptsOptionDisabled() sendReceiptsOptionDisabled()
@@ -100,17 +84,17 @@ struct GroupChatInfoView: View {
.padding(.leading, 8) .padding(.leading, 8)
} }
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let filteredMembers = s == "" ? members : members.filter { $0.wrapped.chatViewName.localizedLowercase.contains(s) } let filteredMembers = s == "" ? members : members.filter { $0.chatViewName.localizedLowercase.contains(s) }
MemberRowView(groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert) memberView(groupInfo.membership, user: true)
ForEach(filteredMembers) { member in ForEach(filteredMembers) { member in
ZStack { ZStack {
NavigationLink { NavigationLink {
memberInfoView(member) memberInfoView(member.groupMemberId)
} label: { } label: {
EmptyView() EmptyView()
} }
.opacity(0) .opacity(0)
MemberRowView(groupInfo: groupInfo, groupMember: member, alert: $alert) memberView(member)
} }
} }
} }
@@ -142,10 +126,6 @@ struct GroupChatInfoView: View {
case .leaveGroupAlert: return leaveGroupAlert() case .leaveGroupAlert: return leaveGroupAlert()
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert() case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert() case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert()
case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem)
case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem)
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
} }
} }
.onAppear { .onAppear {
@@ -194,7 +174,7 @@ struct GroupChatInfoView: View {
Task { Task {
let groupMembers = await apiListMembers(groupInfo.groupId) let groupMembers = await apiListMembers(groupInfo.groupId)
await MainActor.run { await MainActor.run {
chatModel.groupMembers = groupMembers.map { GMember.init($0) } ChatModel.shared.groupMembers = groupMembers
} }
} }
} }
@@ -203,92 +183,51 @@ struct GroupChatInfoView: View {
} }
} }
private struct MemberRowView: View { private func memberView(_ member: GroupMember, user: Bool = false) -> some View {
var groupInfo: GroupInfo HStack{
@ObservedObject var groupMember: GMember ProfileImage(imageStr: member.image)
var user: Bool = false .frame(width: 38, height: 38)
@Binding var alert: GroupChatInfoViewAlert? .padding(.trailing, 2)
// TODO server connection status
var body: some View { VStack(alignment: .leading) {
let member = groupMember.wrapped let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : .primary)
let v = HStack{ (member.verified ? memberVerifiedShield + t : t)
ProfileImage(imageStr: member.image) .lineLimit(1)
.frame(width: 38, height: 38) let s = Text(member.memberStatus.shortText)
.padding(.trailing, 2) (user ? Text ("you: ") + s : s)
// TODO server connection status .lineLimit(1)
VStack(alignment: .leading) { .font(.caption)
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : .primary) .foregroundColor(.secondary)
(member.verified ? memberVerifiedShield + t : t)
.lineLimit(1)
let s = Text(member.memberStatus.shortText)
(user ? Text ("you: ") + s : s)
.lineLimit(1)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
let role = member.memberRole
if role == .owner || role == .admin {
Text(member.memberRole.text)
.foregroundColor(.secondary)
}
} }
Spacer()
if user { let role = member.memberRole
v if role == .owner || role == .admin {
} else if member.canBeRemoved(groupInfo: groupInfo) { Text(member.memberRole.text)
removeSwipe(member, blockSwipe(member, v)) .foregroundColor(.secondary)
} else {
blockSwipe(member, v)
}
}
private func blockSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View {
v.swipeActions(edge: .leading) {
if member.memberSettings.showMessages {
Button {
alert = .blockMemberAlert(mem: member)
} label: {
Label("Block member", systemImage: "hand.raised").foregroundColor(.secondary)
}
} else {
Button {
alert = .unblockMemberAlert(mem: member)
} label: {
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
}
}
}
}
private func removeSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View {
v.swipeActions(edge: .trailing) {
Button(role: .destructive) {
alert = .removeMemberAlert(mem: member)
} label: {
Label("Remove member", systemImage: "trash")
.foregroundColor(Color.red)
}
} }
} }
} }
private func memberInfoView(_ groupMember: GMember) -> some View { private var memberVerifiedShield: Text {
GroupMemberInfoView(groupInfo: groupInfo, groupMember: groupMember) (Text(Image(systemName: "checkmark.shield")) + Text(" "))
.navigationBarHidden(false) .font(.caption)
.baselineOffset(2)
.kerning(-2)
.foregroundColor(.secondary)
}
@ViewBuilder private func memberInfoView(_ groupMemberId: Int64?) -> some View {
if let mId = groupMemberId, let member = chatModel.groupMembers.first(where: { $0.groupMemberId == mId }) {
GroupMemberInfoView(groupInfo: groupInfo, member: member)
.navigationBarHidden(false)
}
} }
private func groupLinkButton() -> some View { private func groupLinkButton() -> some View {
NavigationLink { NavigationLink {
GroupLinkView( GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink, groupLinkMemberRole: $groupLinkMemberRole)
groupId: groupInfo.groupId, .navigationBarTitle("Group link")
groupLink: $groupLink, .navigationBarTitleDisplayMode(.large)
groupLinkMemberRole: $groupLinkMemberRole,
showTitle: false,
creatingGroup: false
)
.navigationBarTitle("Group link")
.navigationBarTitleDisplayMode(.large)
} label: { } label: {
if groupLink == nil { if groupLink == nil {
Label("Create group link", systemImage: "link.badge.plus") Label("Create group link", systemImage: "link.badge.plus")
@@ -436,28 +375,6 @@ struct GroupChatInfoView: View {
alert = .largeGroupReceiptsDisabled alert = .largeGroupReceiptsDisabled
} }
} }
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
Alert(
title: Text("Remove member?"),
message: Text("Member will be removed from group - this cannot be undone!"),
primaryButton: .destructive(Text("Remove")) {
Task {
do {
let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId)
await MainActor.run {
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
}
} catch let error {
logger.error("apiRemoveMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error removing member")
alert = .error(title: a.title, error: a.message)
}
}
},
secondaryButton: .cancel()
)
}
} }
func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View { func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View {
@@ -479,14 +396,6 @@ func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bo
} }
} }
private var memberVerifiedShield: Text {
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
.font(.caption)
.baselineOffset(2)
.kerning(-2)
.foregroundColor(.secondary)
}
func cantInviteIncognitoAlert() -> Alert { func cantInviteIncognitoAlert() -> Alert {
Alert( Alert(
title: Text("Can't invite contacts!"), title: Text("Can't invite contacts!"),
@@ -503,9 +412,6 @@ func largeGroupReceiptsDisabledAlert() -> Alert {
struct GroupChatInfoView_Previews: PreviewProvider { struct GroupChatInfoView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
GroupChatInfoView( GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData)
chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []),
groupInfo: Binding.constant(GroupInfo.sampleData)
)
} }
} }
@@ -13,9 +13,6 @@ struct GroupLinkView: View {
var groupId: Int64 var groupId: Int64
@Binding var groupLink: String? @Binding var groupLink: String?
@Binding var groupLinkMemberRole: GroupMemberRole @Binding var groupLinkMemberRole: GroupMemberRole
var showTitle: Bool = false
var creatingGroup: Bool = false
var linkCreatedCb: (() -> Void)? = nil
@State private var creatingLink = false @State private var creatingLink = false
@State private var alert: GroupLinkAlert? @State private var alert: GroupLinkAlert?
@@ -32,35 +29,10 @@ struct GroupLinkView: View {
} }
var body: some View { var body: some View {
if creatingGroup {
NavigationView {
groupLinkView()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button ("Continue") { linkCreatedCb?() }
}
}
}
} else {
groupLinkView()
}
}
private func groupLinkView() -> some View {
List { List {
Group { Text("You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.")
if showTitle { .listRowBackground(Color.clear)
Text("Group link") .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
}
Text("You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.")
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Section { Section {
if let groupLink = groupLink { if let groupLink = groupLink {
Picker("Initial role", selection: $groupLinkMemberRole) { Picker("Initial role", selection: $groupLinkMemberRole) {
@@ -69,17 +41,15 @@ struct GroupLinkView: View {
} }
} }
.frame(height: 36) .frame(height: 36)
SimpleXLinkQRCode(uri: groupLink) QRCode(uri: groupLink)
Button { Button {
showShareSheet(items: [simplexChatLink(groupLink)]) showShareSheet(items: [groupLink])
} label: { } label: {
Label("Share link", systemImage: "square.and.arrow.up") Label("Share link", systemImage: "square.and.arrow.up")
} }
if !creatingGroup { Button(role: .destructive) { alert = .deleteLink } label: {
Button(role: .destructive) { alert = .deleteLink } label: { Label("Delete link", systemImage: "trash")
Label("Delete link", systemImage: "trash")
}
} }
} else { } else {
Button(action: createGroupLink) { Button(action: createGroupLink) {
@@ -12,40 +12,38 @@ import SimpleXChat
struct GroupMemberInfoView: View { struct GroupMemberInfoView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@State var groupInfo: GroupInfo var groupInfo: GroupInfo
@ObservedObject var groupMember: GMember @State var member: GroupMember
var navigation: Bool = false var navigation: Bool = false
@State private var connectionStats: ConnectionStats? = nil @State private var connectionStats: ConnectionStats? = nil
@State private var connectionCode: String? = nil @State private var connectionCode: String? = nil
@State private var newRole: GroupMemberRole = .member @State private var newRole: GroupMemberRole = .member
@State private var alert: GroupMemberInfoViewAlert? @State private var alert: GroupMemberInfoViewAlert?
@State private var sheet: PlanAndConnectActionSheet? @State private var connectToMemberDialog: Bool = false
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var justOpened = true @State private var justOpened = true
@State private var progressIndicator = false @State private var progressIndicator = false
enum GroupMemberInfoViewAlert: Identifiable { enum GroupMemberInfoViewAlert: Identifiable {
case blockMemberAlert(mem: GroupMember)
case unblockMemberAlert(mem: GroupMember)
case removeMemberAlert(mem: GroupMember) case removeMemberAlert(mem: GroupMember)
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole) case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
case switchAddressAlert case switchAddressAlert
case abortSwitchAddressAlert case abortSwitchAddressAlert
case syncConnectionForceAlert case syncConnectionForceAlert
case planAndConnectAlert(alert: PlanAndConnectAlert) case connRequestSentAlert(type: ConnReqType)
case error(title: LocalizedStringKey, error: LocalizedStringKey) case error(title: LocalizedStringKey, error: LocalizedStringKey)
case other(alert: Alert)
var id: String { var id: String {
switch self { switch self {
case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)" case .removeMemberAlert: return "removeMemberAlert"
case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)" case let .changeMemberRoleAlert(_, role): return "changeMemberRoleAlert \(role.rawValue)"
case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)"
case let .changeMemberRoleAlert(mem, role): return "changeMemberRoleAlert \(mem.groupMemberId) \(role.rawValue)"
case .switchAddressAlert: return "switchAddressAlert" case .switchAddressAlert: return "switchAddressAlert"
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
case .syncConnectionForceAlert: return "syncConnectionForceAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert"
case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" case .connRequestSentAlert: return "connRequestSentAlert"
case let .error(title, _): return "error \(title)" case let .error(title, _): return "error \(title)"
case let .other(alert): return "other \(alert)"
} }
} }
} }
@@ -70,7 +68,6 @@ struct GroupMemberInfoView: View {
private func groupMemberInfoView() -> some View { private func groupMemberInfoView() -> some View {
ZStack { ZStack {
VStack { VStack {
let member = groupMember.wrapped
List { List {
groupMemberInfoHeader(member) groupMemberInfoHeader(member)
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
@@ -99,9 +96,9 @@ struct GroupMemberInfoView: View {
if let contactLink = member.contactLink { if let contactLink = member.contactLink {
Section { Section {
SimpleXLinkQRCode(uri: contactLink) QRCode(uri: contactLink)
Button { Button {
showShareSheet(items: [simplexChatLink(contactLink)]) showShareSheet(items: [contactLink])
} label: { } label: {
Label("Share address", systemImage: "square.and.arrow.up") Label("Share address", systemImage: "square.and.arrow.up")
} }
@@ -164,14 +161,9 @@ struct GroupMemberInfoView: View {
} }
} }
Section { if member.canBeRemoved(groupInfo: groupInfo) {
if member.memberSettings.showMessages { Section {
blockMemberButton(member) removeMemberButton(member)
} else {
unblockMemberButton(member)
}
if member.canBeRemoved(groupInfo: groupInfo) {
removeMemberButton(member)
} }
} }
@@ -192,7 +184,7 @@ struct GroupMemberInfoView: View {
do { do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
_ = chatModel.upsertGroupMember(groupInfo, mem) member = mem
connectionStats = stats connectionStats = stats
connectionCode = code connectionCode = code
} catch let error { } catch let error {
@@ -200,30 +192,25 @@ struct GroupMemberInfoView: View {
} }
justOpened = false justOpened = false
} }
.onChange(of: newRole) { newRole in .onChange(of: newRole) { _ in
if newRole != member.memberRole { if newRole != member.memberRole {
alert = .changeMemberRoleAlert(mem: member, role: newRole) alert = .changeMemberRoleAlert(mem: member, role: newRole)
} }
} }
.onChange(of: member.memberRole) { role in
newRole = role
}
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.alert(item: $alert) { alertItem in .alert(item: $alert) { alertItem in
switch(alertItem) { switch(alertItem) {
case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem)
case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem)
case let .removeMemberAlert(mem): return removeMemberAlert(mem) case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem) case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress) case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) case let .connRequestSentAlert(type): return connReqSentAlert(type)
case let .error(title, error): return Alert(title: Text(title), message: Text(error)) case let .error(title, error): return Alert(title: Text(title), message: Text(error))
case let .other(alert): return alert
} }
} }
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
if progressIndicator { if progressIndicator {
ProgressView().scaleEffect(2) ProgressView().scaleEffect(2)
@@ -233,16 +220,25 @@ struct GroupMemberInfoView: View {
func connectViaAddressButton(_ contactLink: String) -> some View { func connectViaAddressButton(_ contactLink: String) -> some View {
Button { Button {
planAndConnect( connectToMemberDialog = true
contactLink,
showAlert: { alert = .planAndConnectAlert(alert: $0) },
showActionSheet: { sheet = $0 },
dismiss: true,
incognito: nil
)
} label: { } label: {
Label("Connect", systemImage: "link") Label("Connect", systemImage: "link")
} }
.confirmationDialog("Connect directly", isPresented: $connectToMemberDialog, titleVisibility: .visible) {
Button("Use current profile") { connectViaAddress(incognito: false, contactLink: contactLink) }
Button("Use new incognito profile") { connectViaAddress(incognito: true, contactLink: contactLink) }
}
}
func connectViaAddress(incognito: Bool, contactLink: String) {
Task {
let (connReqType, connectAlert) = await apiConnect_(incognito: incognito, connReq: contactLink)
if let connReqType = connReqType {
alert = .connRequestSentAlert(type: connReqType)
} else if let connectAlert = connectAlert {
alert = .other(alert: connectAlert)
}
}
} }
func knownDirectChatButton(_ chat: Chat) -> some View { func knownDirectChatButton(_ chat: Chat) -> some View {
@@ -278,7 +274,7 @@ struct GroupMemberInfoView: View {
progressIndicator = true progressIndicator = true
Task { Task {
do { do {
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId) let memberContact = try await apiCreateMemberContact(groupInfo.apiId, member.groupMemberId)
await MainActor.run { await MainActor.run {
progressIndicator = false progressIndicator = false
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
@@ -336,20 +332,20 @@ struct GroupMemberInfoView: View {
} }
private func verifyCodeButton(_ code: String) -> some View { private func verifyCodeButton(_ code: String) -> some View {
let member = groupMember.wrapped NavigationLink {
return NavigationLink {
VerifyCodeView( VerifyCodeView(
displayName: member.displayName, displayName: member.displayName,
connectionCode: code, connectionCode: code,
connectionVerified: member.verified, connectionVerified: member.verified,
verify: { code in verify: { code in
var member = groupMember.wrapped
if let r = apiVerifyGroupMember(member.groupId, member.groupMemberId, connectionCode: code) { if let r = apiVerifyGroupMember(member.groupId, member.groupMemberId, connectionCode: code) {
let (verified, existingCode) = r let (verified, existingCode) = r
let connCode = verified ? SecurityCode(securityCode: existingCode, verifiedAt: .now) : nil let connCode = verified ? SecurityCode(securityCode: existingCode, verifiedAt: .now) : nil
connectionCode = existingCode connectionCode = existingCode
member.activeConn?.connectionCode = connCode member.activeConn?.connectionCode = connCode
_ = chatModel.upsertGroupMember(groupInfo, member) if let i = chatModel.groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) {
chatModel.groupMembers[i].activeConn?.connectionCode = connCode
}
return r return r
} }
return nil return nil
@@ -383,29 +379,12 @@ struct GroupMemberInfoView: View {
} }
} }
private func blockMemberButton(_ mem: GroupMember) -> some View {
Button(role: .destructive) {
alert = .blockMemberAlert(mem: mem)
} label: {
Label("Block member", systemImage: "hand.raised")
.foregroundColor(.red)
}
}
private func unblockMemberButton(_ mem: GroupMember) -> some View {
Button {
alert = .unblockMemberAlert(mem: mem)
} label: {
Label("Unblock member", systemImage: "hand.raised.slash")
}
}
private func removeMemberButton(_ mem: GroupMember) -> some View { private func removeMemberButton(_ mem: GroupMember) -> some View {
Button(role: .destructive) { Button(role: .destructive) {
alert = .removeMemberAlert(mem: mem) alert = .removeMemberAlert(mem: mem)
} label: { } label: {
Label("Remove member", systemImage: "trash") Label("Remove member", systemImage: "trash")
.foregroundColor(.red) .foregroundColor(Color.red)
} }
} }
@@ -441,6 +420,7 @@ struct GroupMemberInfoView: View {
do { do {
let updatedMember = try await apiMemberRole(groupInfo.groupId, mem.groupMemberId, newRole) let updatedMember = try await apiMemberRole(groupInfo.groupId, mem.groupMemberId, newRole)
await MainActor.run { await MainActor.run {
member = updatedMember
_ = chatModel.upsertGroupMember(groupInfo, updatedMember) _ = chatModel.upsertGroupMember(groupInfo, updatedMember)
} }
@@ -461,10 +441,10 @@ struct GroupMemberInfoView: View {
private func switchMemberAddress() { private func switchMemberAddress() {
Task { Task {
do { do {
let stats = try apiSwitchGroupMember(groupInfo.apiId, groupMember.groupMemberId) let stats = try apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
connectionStats = stats connectionStats = stats
await MainActor.run { await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, groupMember.wrapped, stats) chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
dismiss() dismiss()
} }
} catch let error { } catch let error {
@@ -480,10 +460,10 @@ struct GroupMemberInfoView: View {
private func abortSwitchMemberAddress() { private func abortSwitchMemberAddress() {
Task { Task {
do { do {
let stats = try apiAbortSwitchGroupMember(groupInfo.apiId, groupMember.groupMemberId) let stats = try apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
connectionStats = stats connectionStats = stats
await MainActor.run { await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, groupMember.wrapped, stats) chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
} }
} catch let error { } catch let error {
logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))") logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))")
@@ -498,7 +478,7 @@ struct GroupMemberInfoView: View {
private func syncMemberConnection(force: Bool) { private func syncMemberConnection(force: Bool) {
Task { Task {
do { do {
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, groupMember.groupMemberId, force) let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force)
connectionStats = stats connectionStats = stats
await MainActor.run { await MainActor.run {
chatModel.updateGroupMemberConnectionStats(groupInfo, mem, stats) chatModel.updateGroupMemberConnectionStats(groupInfo, mem, stats)
@@ -515,54 +495,11 @@ struct GroupMemberInfoView: View {
} }
} }
func blockMemberAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
Alert(
title: Text("Block member?"),
message: Text("All new messages from \(mem.chatViewName) will be hidden!"),
primaryButton: .destructive(Text("Block")) {
toggleShowMemberMessages(gInfo, mem, false)
},
secondaryButton: .cancel()
)
}
func unblockMemberAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
Alert(
title: Text("Unblock member?"),
message: Text("Messages from \(mem.chatViewName) will be shown!"),
primaryButton: .default(Text("Unblock")) {
toggleShowMemberMessages(gInfo, mem, true)
},
secondaryButton: .cancel()
)
}
func toggleShowMemberMessages(_ gInfo: GroupInfo, _ member: GroupMember, _ showMessages: Bool) {
var memberSettings = member.memberSettings
memberSettings.showMessages = showMessages
updateMemberSettings(gInfo, member, memberSettings)
}
func updateMemberSettings(_ gInfo: GroupInfo, _ member: GroupMember, _ memberSettings: GroupMemberSettings) {
Task {
do {
try await apiSetMemberSettings(gInfo.groupId, member.groupMemberId, memberSettings)
await MainActor.run {
var mem = member
mem.memberSettings = memberSettings
_ = ChatModel.shared.upsertGroupMember(gInfo, mem)
}
} catch let error {
logger.error("apiSetMemberSettings error \(responseError(error))")
}
}
}
struct GroupMemberInfoView_Previews: PreviewProvider { struct GroupMemberInfoView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
GroupMemberInfoView( GroupMemberInfoView(
groupInfo: GroupInfo.sampleData, groupInfo: GroupInfo.sampleData,
groupMember: GMember.sampleData member: GroupMember.sampleData
) )
} }
} }
@@ -27,7 +27,8 @@ struct GroupPreferencesView: View {
featureSection(.directMessages, $preferences.directMessages.enable) featureSection(.directMessages, $preferences.directMessages.enable)
featureSection(.reactions, $preferences.reactions.enable) featureSection(.reactions, $preferences.reactions.enable)
featureSection(.voice, $preferences.voice.enable) featureSection(.voice, $preferences.voice.enable)
featureSection(.files, $preferences.files.enable) // TODO uncomment in 5.3
// featureSection(.files, $preferences.files.enable)
if groupInfo.canEdit { if groupInfo.canEdit {
Section { Section {
@@ -9,18 +9,6 @@
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChat
enum GroupProfileAlert: Identifiable {
case saveError(err: String)
case invalidName(validName: String)
var id: String {
switch self {
case let .saveError(err): return "saveError \(err)"
case let .invalidName(validName): return "invalidName \(validName)"
}
}
}
struct GroupProfileView: View { struct GroupProfileView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@@ -30,7 +18,8 @@ struct GroupProfileView: View {
@State private var showImagePicker = false @State private var showImagePicker = false
@State private var showTakePhoto = false @State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil @State private var chosenImage: UIImage? = nil
@State private var alert: GroupProfileAlert? @State private var showSaveErrorAlert = false
@State private var saveGroupError: String? = nil
@FocusState private var focusDisplayName @FocusState private var focusDisplayName
var body: some View { var body: some View {
@@ -58,29 +47,20 @@ struct GroupProfileView: View {
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
VStack(alignment: .leading) { VStack(alignment: .leading) {
ZStack(alignment: .topLeading) { ZStack(alignment: .leading) {
if !validNewProfileName() { if !validDisplayName(groupProfile.displayName) {
Button { Image(systemName: "exclamationmark.circle")
alert = .invalidName(validName: mkValidName(groupProfile.displayName)) .foregroundColor(.red)
} label: { .padding(.bottom, 10)
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
} }
profileNameTextEdit("Group display name", $groupProfile.displayName) profileNameTextEdit("Group display name", $groupProfile.displayName)
.focused($focusDisplayName) .focused($focusDisplayName)
} }
.padding(.bottom) profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
let fullName = groupInfo.groupProfile.fullName
if fullName != "" && fullName != groupProfile.displayName {
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
.padding(.bottom)
}
HStack(spacing: 20) { HStack(spacing: 20) {
Button("Cancel") { dismiss() } Button("Cancel") { dismiss() }
Button("Save group profile") { saveProfile() } Button("Save group profile") { saveProfile() }
.disabled(!canUpdateProfile()) .disabled(groupProfile.displayName == "" || !validDisplayName(groupProfile.displayName))
} }
} }
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@@ -119,39 +99,27 @@ struct GroupProfileView: View {
focusDisplayName = true focusDisplayName = true
} }
} }
.alert(item: $alert) { a in .alert(isPresented: $showSaveErrorAlert) {
switch a { Alert(
case let .saveError(err): title: Text("Error saving group profile"),
return Alert( message: Text("\(saveGroupError ?? "Unexpected error")")
title: Text("Error saving group profile"), )
message: Text(err)
)
case let .invalidName(name):
return createInvalidNameAlert(name, $groupProfile.displayName)
}
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { hideKeyboard() } .onTapGesture { hideKeyboard() }
} }
private func canUpdateProfile() -> Bool {
groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName()
}
private func validNewProfileName() -> Bool {
groupProfile.displayName == groupInfo.groupProfile.displayName
|| validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces))
}
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View { func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
TextField(label, text: name) TextField(label, text: name)
.padding(.leading, 32) .textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.bottom)
.padding(.leading, 28)
} }
func saveProfile() { func saveProfile() {
Task { Task {
do { do {
groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces)
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile) let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
await MainActor.run { await MainActor.run {
groupInfo = gInfo groupInfo = gInfo
@@ -160,7 +128,8 @@ struct GroupProfileView: View {
} }
} catch let error { } catch let error {
let err = responseError(error) let err = responseError(error)
alert = .saveError(err: err) saveGroupError = err
showSaveErrorAlert = true
logger.error("GroupProfile apiUpdateGroup error: \(err)") logger.error("GroupProfile apiUpdateGroup error: \(err)")
} }
} }
@@ -32,110 +32,55 @@ struct ChatListNavLink: View {
@State private var showJoinGroupDialog = false @State private var showJoinGroupDialog = false
@State private var showContactConnectionInfo = false @State private var showContactConnectionInfo = false
@State private var showInvalidJSON = false @State private var showInvalidJSON = false
@State private var showDeleteContactActionSheet = false
@State private var showConnectContactViaAddressDialog = false
@State private var inProgress = false
@State private var progressByTimeout = false
var body: some View { var body: some View {
Group { switch chat.chatInfo {
switch chat.chatInfo { case let .direct(contact):
case let .direct(contact): contactNavLink(contact)
contactNavLink(contact) case let .group(groupInfo):
case let .group(groupInfo): groupNavLink(groupInfo)
groupNavLink(groupInfo) case let .contactRequest(cReq):
case let .contactRequest(cReq): contactRequestNavLink(cReq)
contactRequestNavLink(cReq) case let .contactConnection(cConn):
case let .contactConnection(cConn): contactConnectionNavLink(cConn)
contactConnectionNavLink(cConn) case let .invalidJSON(json):
case let .invalidJSON(json): invalidJSONPreview(json)
invalidJSONPreview(json)
}
}
.onChange(of: inProgress) { inProgress in
if inProgress {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
progressByTimeout = inProgress
}
} else {
progressByTimeout = false
}
} }
} }
@ViewBuilder private func contactNavLink(_ contact: Contact) -> some View { @ViewBuilder private func contactNavLink(_ contact: Contact) -> some View {
Group { NavLinkPlain(
if contact.activeConn == nil && contact.profile.contactLink != nil { tag: chat.chatInfo.id,
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) selection: $chatModel.chatId,
.frame(height: rowHeights[dynamicTypeSize]) label: { ChatPreviewView(chat: chat) }
.swipeActions(edge: .trailing, allowsFullSwipe: true) { )
Button { .swipeActions(edge: .leading, allowsFullSwipe: true) {
showDeleteContactActionSheet = true markReadButton()
} label: { toggleFavoriteButton()
Label("Delete", systemImage: "trash") toggleNtfsButton(chat)
}
.tint(.red)
}
.onTapGesture { showConnectContactViaAddressDialog = true }
.confirmationDialog("Connect with \(contact.chatViewName)", isPresented: $showConnectContactViaAddressDialog, titleVisibility: .visible) {
Button("Use current profile") { connectContactViaAddress_(contact, false) }
Button("Use new incognito profile") { connectContactViaAddress_(contact, true) }
}
} else {
NavLinkPlain(
tag: chat.chatInfo.id,
selection: $chatModel.chatId,
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }
)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
markReadButton()
toggleFavoriteButton()
toggleNtfsButton(chat)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if !chat.chatItems.isEmpty {
clearChatButton()
}
Button {
if contact.ready || !contact.active {
showDeleteContactActionSheet = true
} else {
AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact))
}
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
.frame(height: rowHeights[dynamicTypeSize])
}
} }
.actionSheet(isPresented: $showDeleteContactActionSheet) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
if contact.ready && contact.active { if !chat.chatItems.isEmpty {
return ActionSheet( clearChatButton()
title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [
.destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } },
.destructive(Text("Delete")) { Task { await deleteChat(chat, notify: false) } },
.cancel()
]
)
} else {
return ActionSheet(
title: Text("Delete contact?\nThis cannot be undone!"),
buttons: [
.destructive(Text("Delete")) { Task { await deleteChat(chat) } },
.cancel()
]
)
} }
Button {
AlertManager.shared.showAlert(
contact.ready
? deleteContactAlert(chat.chatInfo)
: deletePendingContactAlert(chat, contact)
)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
} }
.frame(height: rowHeights[dynamicTypeSize])
} }
@ViewBuilder private func groupNavLink(_ groupInfo: GroupInfo) -> some View { @ViewBuilder private func groupNavLink(_ groupInfo: GroupInfo) -> some View {
switch (groupInfo.membership.memberStatus) { switch (groupInfo.membership.memberStatus) {
case .memInvited: case .memInvited:
ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout) ChatPreviewView(chat: chat)
.frame(height: rowHeights[dynamicTypeSize]) .frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
joinGroupButton() joinGroupButton()
@@ -146,16 +91,12 @@ struct ChatListNavLink: View {
.onTapGesture { showJoinGroupDialog = true } .onTapGesture { showJoinGroupDialog = true }
.confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) { .confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) {
Button(chat.chatInfo.incognito ? "Join incognito" : "Join group") { Button(chat.chatInfo.incognito ? "Join incognito" : "Join group") {
inProgress = true joinGroup(groupInfo.groupId)
joinGroup(groupInfo.groupId) {
await MainActor.run { inProgress = false }
}
} }
Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } } Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } }
} }
.disabled(inProgress)
case .memAccepted: case .memAccepted:
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) ChatPreviewView(chat: chat)
.frame(height: rowHeights[dynamicTypeSize]) .frame(height: rowHeights[dynamicTypeSize])
.onTapGesture { .onTapGesture {
AlertManager.shared.showAlert(groupInvitationAcceptedAlert()) AlertManager.shared.showAlert(groupInvitationAcceptedAlert())
@@ -172,7 +113,7 @@ struct ChatListNavLink: View {
NavLinkPlain( NavLinkPlain(
tag: chat.chatInfo.id, tag: chat.chatInfo.id,
selection: $chatModel.chatId, selection: $chatModel.chatId,
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, label: { ChatPreviewView(chat: chat) },
disabled: !groupInfo.ready disabled: !groupInfo.ready
) )
.frame(height: rowHeights[dynamicTypeSize]) .frame(height: rowHeights[dynamicTypeSize])
@@ -197,10 +138,7 @@ struct ChatListNavLink: View {
private func joinGroupButton() -> some View { private func joinGroupButton() -> some View {
Button { Button {
inProgress = true joinGroup(chat.chatInfo.apiId)
joinGroup(chat.chatInfo.apiId) {
await MainActor.run { inProgress = false }
}
} label: { } label: {
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward") Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
} }
@@ -331,6 +269,17 @@ struct ChatListNavLink: View {
} }
} }
private func deleteContactAlert(_ chatInfo: ChatInfo) -> Alert {
Alert(
title: Text("Delete contact?"),
message: Text("Contact and all messages will be deleted - this cannot be undone!"),
primaryButton: .destructive(Text("Delete")) {
Task { await deleteChat(chat) }
},
secondaryButton: .cancel()
)
}
private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert { private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert {
Alert( Alert(
title: Text("Delete group?"), title: Text("Delete group?"),
@@ -432,17 +381,6 @@ struct ChatListNavLink: View {
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) .environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
} }
} }
private func connectContactViaAddress_(_ contact: Contact, _ incognito: Bool) {
Task {
let ok = await connectContactViaAddress(contact.contactId, incognito)
if ok {
await MainActor.run {
chatModel.chatId = contact.id
}
}
}
}
} }
func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert { func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert {
@@ -471,22 +409,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection,
) )
} }
func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool) async -> Bool { func joinGroup(_ groupId: Int64) {
let (contact, alert) = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId)
if let alert = alert {
AlertManager.shared.showAlert(alert)
return false
} else if let contact = contact {
await MainActor.run {
ChatModel.shared.updateContact(contact)
AlertManager.shared.showAlert(connReqSentAlert(.contact))
}
return true
}
return false
}
func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) {
Task { Task {
logger.debug("joinGroup") logger.debug("joinGroup")
do { do {
@@ -501,9 +424,7 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) {
AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.")
await deleteGroup() await deleteGroup()
} }
await onComplete()
} catch let error { } catch let error {
await onComplete()
let a = getErrorAlert(error, "Error joining group") let a = getErrorAlert(error, "Error joining group")
AlertManager.shared.showAlertMsg(title: a.title, message: a.message) AlertManager.shared.showAlertMsg(title: a.title, message: a.message)
} }
@@ -15,7 +15,6 @@ struct ChatListView: View {
@State private var searchText = "" @State private var searchText = ""
@State private var showAddChat = false @State private var showAddChat = false
@State private var userPickerVisible = false @State private var userPickerVisible = false
@State private var showConnectDesktop = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View { var body: some View {
@@ -30,7 +29,7 @@ struct ChatListView: View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
NavStackCompat( NavStackCompat(
isActive: Binding( isActive: Binding(
get: { chatModel.chatId != nil }, get: { ChatModel.shared.chatId != nil },
set: { _ in } set: { _ in }
), ),
destination: chatView destination: chatView
@@ -49,14 +48,7 @@ struct ChatListView: View {
} }
} }
} }
UserPicker( UserPicker(showSettings: $showSettings, userPickerVisible: $userPickerVisible)
showSettings: $showSettings,
showConnectDesktop: $showConnectDesktop,
userPickerVisible: $userPickerVisible
)
}
.sheet(isPresented: $showConnectDesktop) {
ConnectDesktopView()
} }
} }
@@ -185,6 +177,13 @@ struct ChatListView: View {
showAddChat = true showAddChat = true
} }
connectButton("or chat with the developers") {
DispatchQueue.main.async {
UIApplication.shared.open(simplexTeamURL)
}
}
.padding(.top, 10)
Spacer() Spacer()
Text("You have no chats") Text("You have no chats")
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -12,7 +12,6 @@ import SimpleXChat
struct ChatPreviewView: View { struct ChatPreviewView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@Binding var progressByTimeout: Bool
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var darkGreen = Color(red: 0, green: 0.5, blue: 0) var darkGreen = Color(red: 0, green: 0.5, blue: 0)
@@ -58,26 +57,19 @@ struct ChatPreviewView: View {
} }
@ViewBuilder private func chatPreviewImageOverlayIcon() -> some View { @ViewBuilder private func chatPreviewImageOverlayIcon() -> some View {
switch chat.chatInfo { if case let .group(groupInfo) = chat.chatInfo {
case let .direct(contact):
if !contact.active {
inactiveIcon()
} else {
EmptyView()
}
case let .group(groupInfo):
switch (groupInfo.membership.memberStatus) { switch (groupInfo.membership.memberStatus) {
case .memLeft: inactiveIcon() case .memLeft: groupInactiveIcon()
case .memRemoved: inactiveIcon() case .memRemoved: groupInactiveIcon()
case .memGroupDeleted: inactiveIcon() case .memGroupDeleted: groupInactiveIcon()
default: EmptyView() default: EmptyView()
} }
default: } else {
EmptyView() EmptyView()
} }
} }
@ViewBuilder private func inactiveIcon() -> some View { @ViewBuilder private func groupInactiveIcon() -> some View {
Image(systemName: "multiply.circle.fill") Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary.opacity(0.65)) .foregroundColor(.secondary.opacity(0.65))
.background(Circle().foregroundColor(Color(uiColor: .systemBackground))) .background(Circle().foregroundColor(Color(uiColor: .systemBackground)))
@@ -88,6 +80,7 @@ struct ChatPreviewView: View {
switch chat.chatInfo { switch chat.chatInfo {
case let .direct(contact): case let .direct(contact):
previewTitle(contact.verified == true ? verifiedIcon + t : t) previewTitle(contact.verified == true ? verifiedIcon + t : t)
.foregroundColor(chat.chatInfo.ready ? .primary : .secondary)
case let .group(groupInfo): case let .group(groupInfo):
let v = previewTitle(t) let v = previewTitle(t)
switch (groupInfo.membership.memberStatus) { switch (groupInfo.membership.memberStatus) {
@@ -112,17 +105,14 @@ struct ChatPreviewView: View {
private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View { private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View {
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
let t = text text
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, 8) .padding(.leading, 8)
.padding(.trailing, 36) .padding(.trailing, 36)
if !showChatPreviews && !draft { .privacySensitive(!showChatPreviews && !draft)
t.privacySensitive(true).redacted(reason: .privacy) .redacted(reason: .privacy)
} else {
t
}
let s = chat.chatStats let s = chat.chatStats
if s.unreadCount > 0 || s.unreadChat { if s.unreadCount > 0 || s.unreadChat {
unreadCountText(s.unreadCount) unreadCountText(s.unreadCount)
@@ -190,13 +180,10 @@ struct ChatPreviewView: View {
} else { } else {
switch (chat.chatInfo) { switch (chat.chatInfo) {
case let .direct(contact): case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil { if !contact.ready {
chatPreviewInfoText("Tap to Connect")
.foregroundColor(.accentColor)
} else if !contact.ready && contact.activeConn != nil {
if contact.nextSendGrpInv { if contact.nextSendGrpInv {
chatPreviewInfoText("send direct message") chatPreviewInfoText("send direct message")
} else if contact.active { } else {
chatPreviewInfoText("connecting…") chatPreviewInfoText("connecting…")
} }
} }
@@ -241,26 +228,16 @@ struct ChatPreviewView: View {
@ViewBuilder private func chatStatusImage() -> some View { @ViewBuilder private func chatStatusImage() -> some View {
switch chat.chatInfo { switch chat.chatInfo {
case let .direct(contact): case let .direct(contact):
if contact.active && contact.activeConn != nil { switch (chatModel.contactNetworkStatus(contact)) {
switch (chatModel.contactNetworkStatus(contact)) { case .connected: incognitoIcon(chat.chatInfo.incognito)
case .connected: incognitoIcon(chat.chatInfo.incognito) case .error:
case .error: Image(systemName: "exclamationmark.circle")
Image(systemName: "exclamationmark.circle") .resizable()
.resizable() .scaledToFit()
.scaledToFit() .frame(width: 17, height: 17)
.frame(width: 17, height: 17) .foregroundColor(.secondary)
.foregroundColor(.secondary) default:
default:
ProgressView()
}
} else {
incognitoIcon(chat.chatInfo.incognito)
}
case .group:
if progressByTimeout {
ProgressView() ProgressView()
} else {
incognitoIcon(chat.chatInfo.incognito)
} }
default: default:
incognitoIcon(chat.chatInfo.incognito) incognitoIcon(chat.chatInfo.incognito)
@@ -290,30 +267,30 @@ struct ChatPreviewView_Previews: PreviewProvider {
ChatPreviewView(chat: Chat( ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct, chatInfo: ChatInfo.sampleData.direct,
chatItems: [] chatItems: []
), progressByTimeout: Binding.constant(false)) ))
ChatPreviewView(chat: Chat( ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct, chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))] chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))]
), progressByTimeout: Binding.constant(false)) ))
ChatPreviewView(chat: Chat( ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct, chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))], chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))],
chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0) chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0)
), progressByTimeout: Binding.constant(false)) ))
ChatPreviewView(chat: Chat( ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct, chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))] chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))]
), progressByTimeout: Binding.constant(false)) ))
ChatPreviewView(chat: Chat( ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct, chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))], chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))],
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0) chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0)
), progressByTimeout: Binding.constant(false)) ))
ChatPreviewView(chat: Chat( ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.group, chatInfo: ChatInfo.sampleData.group,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, d. consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")], chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, d. consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")],
chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0) chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0)
), progressByTimeout: Binding.constant(false)) ))
} }
.previewLayout(.fixed(width: 360, height: 78)) .previewLayout(.fixed(width: 360, height: 78))
} }
@@ -61,7 +61,7 @@ struct ContactConnectionInfo: View {
if contactConnection.initiated, if contactConnection.initiated,
let connReqInv = contactConnection.connReqInv { let connReqInv = contactConnection.connReqInv {
SimpleXLinkQRCode(uri: simplexChatLink(connReqInv)) QRCode(uri: connReqInv)
incognitoEnabled() incognitoEnabled()
shareLinkButton(connReqInv) shareLinkButton(connReqInv)
oneTimeLinkLearnMoreButton() oneTimeLinkLearnMoreButton()
@@ -119,7 +119,7 @@ struct ContactConnectionInfo: View {
if let conn = try await apiSetConnectionAlias(connId: contactConnection.pccConnId, localAlias: localAlias) { if let conn = try await apiSetConnectionAlias(connId: contactConnection.pccConnId, localAlias: localAlias) {
await MainActor.run { await MainActor.run {
contactConnection = conn contactConnection = conn
m.updateContactConnection(conn) ChatModel.shared.updateContactConnection(conn)
dismiss() dismiss()
} }
} }
@@ -13,7 +13,6 @@ struct UserPicker: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Binding var showSettings: Bool @Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool @Binding var userPickerVisible: Bool
@State var scrollViewContentSize: CGSize = .zero @State var scrollViewContentSize: CGSize = .zero
@State var disableScrolling: Bool = true @State var disableScrolling: Bool = true
@@ -63,13 +62,6 @@ struct UserPicker: View {
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000)) .simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
.frame(maxHeight: scrollViewContentSize.height) .frame(maxHeight: scrollViewContentSize.height)
menuButton("Use from desktop", icon: "desktopcomputer") {
showConnectDesktop = true
withAnimation {
userPickerVisible.toggle()
}
}
Divider()
menuButton("Settings", icon: "gearshape") { menuButton("Settings", icon: "gearshape") {
showSettings = true showSettings = true
withAnimation { withAnimation {
@@ -93,7 +85,7 @@ struct UserPicker: View {
do { do {
m.users = try listUsers() m.users = try listUsers()
} catch let error { } catch let error {
logger.error("Error loading users \(responseError(error))") logger.error("Error updating users \(responseError(error))")
} }
} }
} }
@@ -152,8 +144,7 @@ struct UserPicker: View {
.overlay(DetermineWidth()) .overlay(DetermineWidth())
Spacer() Spacer()
Image(systemName: icon) Image(systemName: icon)
.symbolRenderingMode(.monochrome) // .frame(width: 24, alignment: .center)
.foregroundColor(.secondary)
} }
.padding(.horizontal) .padding(.horizontal)
.padding(.vertical, 22) .padding(.vertical, 22)
@@ -179,7 +170,6 @@ struct UserPicker_Previews: PreviewProvider {
m.users = [UserInfo.sampleData, UserInfo.sampleData] m.users = [UserInfo.sampleData, UserInfo.sampleData]
return UserPicker( return UserPicker(
showSettings: Binding.constant(false), showSettings: Binding.constant(false),
showConnectDesktop: Binding.constant(false),
userPickerVisible: Binding.constant(true) userPickerVisible: Binding.constant(true)
) )
.environmentObject(m) .environmentObject(m)
@@ -6,7 +6,6 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AVKit import AVKit
import Combine
struct VideoPlayerView: UIViewRepresentable { struct VideoPlayerView: UIViewRepresentable {
@@ -38,14 +37,6 @@ struct VideoPlayerView: UIViewRepresentable {
player.seek(to: CMTime.zero) player.seek(to: CMTime.zero)
player.play() player.play()
} }
var played = false
context.coordinator.publisher = player.publisher(for: \.timeControlStatus).sink { status in
if played || status == .playing {
AppDelegate.keepScreenOn(status == .playing)
AudioPlayer.changeAudioSession(status == .playing)
}
played = status == .playing
}
return controller.view return controller.view
} }
@@ -59,13 +50,12 @@ struct VideoPlayerView: UIViewRepresentable {
class Coordinator: NSObject { class Coordinator: NSObject {
var controller: AVPlayerViewController? var controller: AVPlayerViewController?
var timeObserver: Any? = nil var timeObserver: Any? = nil
var publisher: AnyCancellable? = nil
deinit { deinit {
print("deinit coordinator of VideoPlayer")
if let timeObserver = timeObserver { if let timeObserver = timeObserver {
NotificationCenter.default.removeObserver(timeObserver) NotificationCenter.default.removeObserver(timeObserver)
} }
publisher?.cancel()
} }
} }
} }
@@ -21,7 +21,7 @@ struct AddContactView: View {
List { List {
Section { Section {
if connReqInvitation != "" { if connReqInvitation != "" {
SimpleXLinkQRCode(uri: connReqInvitation) QRCode(uri: connReqInvitation)
} else { } else {
ProgressView() ProgressView()
.progressViewStyle(.circular) .progressViewStyle(.circular)
@@ -48,7 +48,7 @@ struct AddContactView: View {
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) { let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run { await MainActor.run {
contactConnection = conn contactConnection = conn
chatModel.updateContactConnection(conn) ChatModel.shared.updateContactConnection(conn)
} }
} }
} catch { } catch {
@@ -99,7 +99,7 @@ func sharedProfileInfo(_ incognito: Bool) -> Text {
func shareLinkButton(_ connReqInvitation: String) -> some View { func shareLinkButton(_ connReqInvitation: String) -> some View {
Button { Button {
showShareSheet(items: [simplexChatLink(connReqInvitation)]) showShareSheet(items: [connReqInvitation])
} label: { } label: {
settingsRow("square.and.arrow.up") { settingsRow("square.and.arrow.up") {
Text("Share 1-time link") Text("Share 1-time link")
+78 -109
View File
@@ -12,45 +12,27 @@ import SimpleXChat
struct AddGroupView: View { struct AddGroupView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@State private var chat: Chat? @State private var chat: Chat?
@State private var groupInfo: GroupInfo? @State private var groupInfo: GroupInfo?
@State private var profile = GroupProfile(displayName: "", fullName: "") @State private var profile = GroupProfile(displayName: "", fullName: "")
@FocusState private var focusDisplayName @FocusState private var focusDisplayName
@FocusState private var focusFullName
@State private var showChooseSource = false @State private var showChooseSource = false
@State private var showImagePicker = false @State private var showImagePicker = false
@State private var showTakePhoto = false @State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil @State private var chosenImage: UIImage? = nil
@State private var showInvalidNameAlert = false
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
var body: some View { var body: some View {
if let chat = chat, let groupInfo = groupInfo { if let chat = chat, let groupInfo = groupInfo {
if !groupInfo.membership.memberIncognito { AddGroupMembersViewCommon(
AddGroupMembersViewCommon( chat: chat,
chat: chat, groupInfo: groupInfo,
groupInfo: groupInfo, creatingGroup: true,
creatingGroup: true, showFooterCounter: false
showFooterCounter: false ) { _ in
) { _ in dismiss()
dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { m.chatId = groupInfo.id
m.chatId = groupInfo.id
}
}
} else {
GroupLinkView(
groupId: groupInfo.groupId,
groupLink: $groupLink,
groupLinkMemberRole: $groupLinkMemberRole,
showTitle: true,
creatingGroup: true
) {
dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
m.chatId = groupInfo.id
}
} }
} }
} else { } else {
@@ -59,62 +41,79 @@ struct AddGroupView: View {
} }
func createGroupView() -> some View { func createGroupView() -> some View {
List { VStack(alignment: .leading) {
Group { Text("Create secret group")
Text("Create secret group") .font(.largeTitle)
.font(.largeTitle) .padding(.vertical, 4)
.bold() Text("The group is fully decentralized it is visible only to the members.")
.fixedSize(horizontal: false, vertical: true) .padding(.bottom, 4)
.padding(.bottom, 24)
.onTapGesture(perform: hideKeyboard)
ZStack(alignment: .center) { HStack {
ZStack(alignment: .topTrailing) { Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
ProfileImage(imageStr: profile.image, color: Color(uiColor: .secondarySystemGroupedBackground)) Spacer().frame(width: 8)
.aspectRatio(1, contentMode: .fit) Text("Your chat profile will be sent to group members").font(.footnote)
.frame(maxWidth: 128, maxHeight: 128) }
if profile.image != nil { .padding(.bottom)
Button {
profile.image = nil ZStack(alignment: .center) {
} label: { ZStack(alignment: .topTrailing) {
Image(systemName: "multiply") profileImageView(profile.image)
.resizable() if profile.image != nil {
.aspectRatio(contentMode: .fit) Button {
.frame(width: 12) profile.image = nil
} } label: {
Image(systemName: "multiply")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 12)
} }
} }
}
editImageButton { showChooseSource = true } editImageButton { showChooseSource = true }
.buttonStyle(BorderlessButtonStyle()) // otherwise whole "list row" is clickable
}
.frame(maxWidth: .infinity, alignment: .center)
} }
.listRowBackground(Color.clear) .frame(maxWidth: .infinity, alignment: .center)
.listRowSeparator(.hidden) .padding(.bottom, 4)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Section { ZStack(alignment: .topLeading) {
groupNameTextField() if !validDisplayName(profile.displayName) {
Button(action: createGroup) { Image(systemName: "exclamationmark.circle")
settingsRow("checkmark", color: .accentColor) { Text("Create group") } .foregroundColor(.red)
.padding(.top, 4)
} }
.disabled(!canCreateProfile()) textField("Group display name", text: $profile.displayName)
IncognitoToggle(incognitoEnabled: $incognitoDefault) .focused($focusDisplayName)
} footer: { .submitLabel(.next)
VStack(alignment: .leading, spacing: 4) { .onSubmit {
sharedGroupProfileInfo(incognitoDefault) if canCreateProfile() { focusFullName = true }
Text("Fully decentralized visible only to members.") else { focusDisplayName = true }
} }
.frame(maxWidth: .infinity, alignment: .leading)
.onTapGesture(perform: hideKeyboard)
} }
textField("Group full name (optional)", text: $profile.fullName)
.focused($focusFullName)
.submitLabel(.go)
.onSubmit {
if canCreateProfile() { createGroup() }
else { focusFullName = true }
}
Spacer()
Button {
createGroup()
} label: {
Text("Create")
Image(systemName: "greaterthan")
}
.disabled(!canCreateProfile())
.frame(maxWidth: .infinity, alignment: .trailing)
} }
.onAppear() { .onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
focusDisplayName = true focusDisplayName = true
} }
} }
.padding()
.confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) { .confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) {
Button("Take picture") { Button("Take picture") {
showTakePhoto = true showTakePhoto = true
@@ -134,9 +133,6 @@ struct AddGroupView: View {
didSelectItem in showImagePicker = false didSelectItem in showImagePicker = false
} }
} }
.alert(isPresented: $showInvalidNameAlert) {
createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName)
}
.onChange(of: chosenImage) { image in .onChange(of: chosenImage) { image in
if let image = image { if let image = image {
profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
@@ -144,52 +140,26 @@ struct AddGroupView: View {
profile.image = nil profile.image = nil
} }
} }
} .contentShape(Rectangle())
.onTapGesture { hideKeyboard() }
func groupNameTextField() -> some View {
ZStack(alignment: .leading) {
let name = profile.displayName.trimmingCharacters(in: .whitespaces)
if name != mkValidName(name) {
Button {
showInvalidNameAlert = true
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "pencil").foregroundColor(.secondary)
}
textField("Enter group name…", text: $profile.displayName)
.focused($focusDisplayName)
.submitLabel(.continue)
.onSubmit {
if canCreateProfile() { createGroup() }
}
}
} }
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View { func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
TextField(placeholder, text: text) TextField(placeholder, text: text)
.padding(.leading, 36) .textInputAutocapitalization(.never)
} .disableAutocorrection(true)
.padding(.leading, 28)
func sharedGroupProfileInfo(_ incognito: Bool) -> Text { .padding(.bottom)
let name = ChatModel.shared.currentUser?.displayName ?? ""
return Text(
incognito
? "A new random profile will be shared."
: "Your profile **\(name)** will be shared."
)
} }
func createGroup() { func createGroup() {
hideKeyboard() hideKeyboard()
do { do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) let gInfo = try apiNewGroup(profile)
let gInfo = try apiNewGroup(incognito: incognitoDefault, groupProfile: profile)
Task { Task {
let groupMembers = await apiListMembers(gInfo.groupId) let groupMembers = await apiListMembers(gInfo.groupId)
await MainActor.run { await MainActor.run {
m.groupMembers = groupMembers.map { GMember.init($0) } ChatModel.shared.groupMembers = groupMembers
} }
} }
let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: []) let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: [])
@@ -210,8 +180,7 @@ struct AddGroupView: View {
} }
func canCreateProfile() -> Bool { func canCreateProfile() -> Bool {
let name = profile.displayName.trimmingCharacters(in: .whitespaces) profile.displayName != "" && validDisplayName(profile.displayName)
return name != "" && validDisplayName(name)
} }
} }
+37 -370
View File
@@ -58,398 +58,65 @@ struct NewChatButton: View {
} }
} }
enum PlanAndConnectAlert: Identifiable { enum ConnReqType: Equatable {
case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case contact
case invitationLinkConnecting(connectionLink: String) case invitation
case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
var id: String {
switch self {
case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)"
case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)"
case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
}
}
} }
func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert { func connectViaLink(_ connectionLink: String, dismiss: DismissAction? = nil, incognito: Bool) {
switch alert {
case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Connect to yourself?"),
message: Text("This is your own one-time link!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case .invitationLinkConnecting:
return Alert(
title: Text("Already connecting!"),
message: Text("You are already connecting via this one-time link!")
)
case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Connect to yourself?"),
message: Text("This is your own SimpleX address!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Repeat connection request?"),
message: Text("You have already requested connection via this address!"),
primaryButton: .destructive(
Text(incognito ? "Connect incognito" : "Connect"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Join group?"),
message: Text("You will connect to all group members."),
primaryButton: .default(
Text(incognito ? "Join incognito" : "Join"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
return Alert(
title: Text("Repeat join request?"),
message: Text("You are already joining the group via this link!"),
primaryButton: .destructive(
Text(incognito ? "Join incognito" : "Join"),
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
),
secondaryButton: .cancel()
)
case let .groupLinkConnecting(_, groupInfo):
if let groupInfo = groupInfo {
return Alert(
title: Text("Group already exists!"),
message: Text("You are already joining the group \(groupInfo.displayName).")
)
} else {
return Alert(
title: Text("Already joining the group!"),
message: Text("You are already joining the group via this link.")
)
}
}
}
enum PlanAndConnectActionSheet: Identifiable {
case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact)
case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
var id: String {
switch self {
case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)"
case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)"
case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
}
}
}
func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool) -> ActionSheet {
switch sheet {
case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
return ActionSheet(
title: Text(title),
buttons: [
.default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
.default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
.cancel()
]
)
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title):
return ActionSheet(
title: Text(title),
buttons: [
.destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
.destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
.cancel()
]
)
case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact):
return ActionSheet(
title: Text("Connect with \(contact.chatViewName)"),
buttons: [
.default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false) },
.default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true) },
.cancel()
]
)
case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo):
if let incognito = incognito {
return ActionSheet(
title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
buttons: [
.default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
.destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) },
.cancel()
]
)
} else {
return ActionSheet(
title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
buttons: [
.default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
.destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
.destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
.cancel()
]
)
}
}
}
func planAndConnect(
_ connectionLink: String,
showAlert: @escaping (PlanAndConnectAlert) -> Void,
showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
dismiss: Bool,
incognito: Bool?
) {
Task {
do {
let connectionPlan = try await apiConnectPlan(connReq: connectionLink)
switch connectionPlan {
case let .invitationLink(ilp):
switch ilp {
case .ok:
logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
}
case .ownLink:
logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
}
case let .connecting(contact_):
logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
if let contact = contact_ {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
} else {
showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
}
case let .known(contact):
logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
}
case let .contactAddress(cap):
switch cap {
case .ok:
logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
}
case .ownLink:
logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
}
case .connectingConfirmReconnect:
logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
}
case let .connectingProhibit(contact):
logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
case let .known(contact):
logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
case let .contactViaAddress(contact):
logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito)
} else {
showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact))
}
}
case let .groupLink(glp):
switch glp {
case .ok:
if let incognito = incognito {
showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
}
case let .ownLink(groupInfo):
logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
case .connectingConfirmReconnect:
logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito {
showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
}
case let .connectingProhibit(groupInfo_):
logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
case let .known(groupInfo):
logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
}
}
} catch {
logger.debug("planAndConnect, plan error")
if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito)
} else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
}
}
}
}
private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incognito: Bool) {
Task {
if dismiss {
DispatchQueue.main.async {
dismissAllSheets(animated: true)
}
}
_ = await connectContactViaAddress(contact.contactId, incognito)
}
}
private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
Task { Task {
if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) { if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
let crt: ConnReqType
if let plan = connectionPlan {
crt = planToConnReqType(plan)
} else {
crt = connReqType
}
DispatchQueue.main.async { DispatchQueue.main.async {
if dismiss { dismiss?()
dismissAllSheets(animated: true) { AlertManager.shared.showAlert(connReqSentAlert(connReqType))
AlertManager.shared.showAlert(connReqSentAlert(crt))
}
} else {
AlertManager.shared.showAlert(connReqSentAlert(crt))
}
} }
} else { } else {
if dismiss {
DispatchQueue.main.async {
dismissAllSheets(animated: true)
}
}
}
}
}
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
Task {
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
DispatchQueue.main.async { DispatchQueue.main.async {
if dismiss { dismiss?()
dismissAllSheets(animated: true) {
m.chatId = c.id
showAlreadyExistsAlert?()
}
} else {
m.chatId = c.id
showAlreadyExistsAlert?()
}
} }
} }
} }
} }
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { struct CReqClientData: Decodable {
Task { var type: String
let m = ChatModel.shared var groupLinkId: String?
if let g = m.getGroupChat(groupInfo.groupId) { }
DispatchQueue.main.async {
if dismiss { func parseLinkQueryData(_ connectionLink: String) -> CReqClientData? {
dismissAllSheets(animated: true) { if let hashIndex = connectionLink.firstIndex(of: "#"),
m.chatId = g.id let urlQuery = URL(string: String(connectionLink[connectionLink.index(after: hashIndex)...])),
showAlreadyExistsAlert?() let components = URLComponents(url: urlQuery, resolvingAgainstBaseURL: false),
} let data = components.queryItems?.first(where: { $0.name == "data" })?.value,
} else { let d = data.data(using: .utf8),
m.chatId = g.id let crData = try? getJSONDecoder().decode(CReqClientData.self, from: d) {
showAlreadyExistsAlert?() return crData
} } else {
} return nil
}
} }
} }
func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert { func checkCRDataGroup(_ crData: CReqClientData) -> Bool {
mkAlert( return crData.type == "group" && crData.groupLinkId != nil
title: "Contact already exists", }
message: "You are already connecting to \(contact.displayName)."
func groupLinkAlert(_ connectionLink: String, incognito: Bool) -> Alert {
return Alert(
title: Text("Connect via group link?"),
message: Text("You will join a group this link refers to and connect to its group members."),
primaryButton: .default(Text(incognito ? "Connect incognito" : "Connect")) {
connectViaLink(connectionLink, incognito: incognito)
},
secondaryButton: .cancel()
) )
} }
func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
mkAlert(
title: "Group already exists",
message: "You are already in group \(groupInfo.displayName)."
)
}
enum ConnReqType: Equatable {
case invitation
case contact
case groupLink
var connReqSentText: LocalizedStringKey {
switch self {
case .invitation: return "You will be connected when your contact's device is online, please wait or check later!"
case .contact: return "You will be connected when your connection request is accepted, please wait or check later!"
case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!"
}
}
}
private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType {
switch connectionPlan {
case .invitationLink: return .invitation
case .contactAddress: return .contact
case .groupLink: return .groupLink
}
}
func connReqSentAlert(_ type: ConnReqType) -> Alert { func connReqSentAlert(_ type: ConnReqType) -> Alert {
return mkAlert( return mkAlert(
title: "Connection request sent!", title: "Connection request sent!",
message: type.connReqSentText message: type == .contact
? "You will be connected when your connection request is accepted, please wait or check later!"
: "You will be connected when your contact's device is online, please wait or check later!"
) )
} }
@@ -14,8 +14,6 @@ struct PasteToConnectView: View {
@State private var connectionLink: String = "" @State private var connectionLink: String = ""
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@FocusState private var linkEditorFocused: Bool @FocusState private var linkEditorFocused: Bool
@State private var alert: PlanAndConnectAlert?
@State private var sheet: PlanAndConnectActionSheet?
var body: some View { var body: some View {
List { List {
@@ -54,15 +52,11 @@ struct PasteToConnectView: View {
IncognitoToggle(incognitoEnabled: $incognitoDefault) IncognitoToggle(incognitoEnabled: $incognitoDefault)
} footer: { } footer: {
VStack(alignment: .leading, spacing: 4) { sharedProfileInfo(incognitoDefault)
sharedProfileInfo(incognitoDefault) + Text(String("\n\n"))
Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.") + Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
}
.frame(maxWidth: .infinity, alignment: .leading)
} }
} }
.alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
} }
private func linkEditor() -> some View { private func linkEditor() -> some View {
@@ -89,13 +83,13 @@ struct PasteToConnectView: View {
private func connect() { private func connect() {
let link = connectionLink.trimmingCharacters(in: .whitespaces) let link = connectionLink.trimmingCharacters(in: .whitespaces)
planAndConnect( if let crData = parseLinkQueryData(link),
link, checkCRDataGroup(crData) {
showAlert: { alert = $0 }, dismiss()
showActionSheet: { sheet = $0 }, AlertManager.shared.showAlert(groupLinkAlert(link, incognito: incognitoDefault))
dismiss: true, } else {
incognito: incognitoDefault connectViaLink(link, dismiss: dismiss, incognito: incognitoDefault)
) }
} }
} }
@@ -28,22 +28,6 @@ struct MutableQRCode: View {
} }
} }
struct SimpleXLinkQRCode: View {
let uri: String
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var body: some View {
QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor)
}
}
func simplexChatLink(_ uri: String) -> String {
uri.starts(with: "simplex:/")
? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/")
: uri
}
struct QRCode: View { struct QRCode: View {
let uri: String let uri: String
var withLogo: Bool = true var withLogo: Bool = true
@@ -13,8 +13,6 @@ import CodeScanner
struct ScanToConnectView: View { struct ScanToConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@State private var alert: PlanAndConnectAlert?
@State private var sheet: PlanAndConnectActionSheet?
var body: some View { var body: some View {
ScrollView { ScrollView {
@@ -38,11 +36,11 @@ struct ScanToConnectView: View {
) )
.padding(.top) .padding(.top)
VStack(alignment: .leading, spacing: 4) { Group {
sharedProfileInfo(incognitoDefault) sharedProfileInfo(incognitoDefault)
Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.") + Text(String("\n\n"))
+ Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.")
} }
.frame(maxWidth: .infinity, alignment: .leading)
.font(.footnote) .font(.footnote)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(.horizontal) .padding(.horizontal)
@@ -51,20 +49,18 @@ struct ScanToConnectView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
} }
.background(Color(.systemGroupedBackground)) .background(Color(.systemGroupedBackground))
.alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
} }
func processQRCode(_ resp: Result<ScanResult, ScanError>) { func processQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp { switch resp {
case let .success(r): case let .success(r):
planAndConnect( if let crData = parseLinkQueryData(r.string),
r.string, checkCRDataGroup(crData) {
showAlert: { alert = $0 }, dismiss()
showActionSheet: { sheet = $0 }, AlertManager.shared.showAlert(groupLinkAlert(r.string, incognito: incognitoDefault))
dismiss: true, } else {
incognito: incognitoDefault Task { connectViaLink(r.string, dismiss: dismiss, incognito: incognitoDefault) }
) }
case let .failure(e): case let .failure(e):
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)") logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
dismiss() dismiss()
@@ -9,244 +9,175 @@
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChat
enum UserProfileAlert: Identifiable {
case duplicateUserError
case createUserError(error: LocalizedStringKey)
case invalidNameError(validName: String)
var id: String {
switch self {
case .duplicateUserError: return "duplicateUserError"
case .createUserError: return "createUserError"
case let .invalidNameError(validName): return "invalidNameError \(validName)"
}
}
}
struct CreateProfile: View { struct CreateProfile: View {
@Environment(\.dismiss) var dismiss
@State private var displayName: String = ""
@FocusState private var focusDisplayName
@State private var alert: UserProfileAlert?
var body: some View {
List {
Section {
TextField("Enter your name…", text: $displayName)
.focused($focusDisplayName)
Button {
createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss)
} label: {
Label("Create profile", systemImage: "checkmark")
}
.disabled(!canCreateProfile(displayName))
} header: {
HStack {
Text("Your profile")
let name = displayName.trimmingCharacters(in: .whitespaces)
let validName = mkValidName(name)
if name != validName {
Spacer()
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.onTapGesture {
alert = .invalidNameError(validName: validName)
}
}
}
.frame(height: 20)
} footer: {
VStack(alignment: .leading, spacing: 8) {
Text("Your profile, contacts and delivered messages are stored on your device.")
Text("The profile is only shared with your contacts.")
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.navigationTitle("Create your profile")
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
focusDisplayName = true
}
}
.keyboardPadding()
}
}
struct CreateFirstProfile: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@State private var displayName: String = "" @State private var displayName: String = ""
@State private var fullName: String = ""
@FocusState private var focusDisplayName @FocusState private var focusDisplayName
@FocusState private var focusFullName
@State private var alert: CreateProfileAlert?
private enum CreateProfileAlert: Identifiable {
case duplicateUserError
case createUserError(error: LocalizedStringKey)
var id: String {
switch self {
case .duplicateUserError: return "duplicateUserError"
case .createUserError: return "createUserError"
}
}
}
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Group { Text("Create your profile")
Text("Create your profile") .font(.largeTitle)
.font(.largeTitle) .bold()
.bold() .padding(.bottom, 4)
Text("Your profile, contacts and delivered messages are stored on your device.") .frame(maxWidth: .infinity)
.foregroundColor(.secondary) Text("Your profile, contacts and delivered messages are stored on your device.")
Text("The profile is only shared with your contacts.") .padding(.bottom, 4)
.foregroundColor(.secondary) Text("The profile is only shared with your contacts.")
.padding(.bottom) .padding(.bottom)
}
.padding(.bottom)
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
let name = displayName.trimmingCharacters(in: .whitespaces) if !validDisplayName(displayName) {
let validName = mkValidName(name) Image(systemName: "exclamationmark.circle")
if name != validName { .foregroundColor(.red)
Button { .padding(.top, 4)
showAlert(.invalidNameError(validName: validName))
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
} }
TextField("Enter your name", text: $displayName) textField("Display name", text: $displayName)
.focused($focusDisplayName) .focused($focusDisplayName)
.padding(.leading, 32) .submitLabel(.next)
.onSubmit {
if canCreateProfile() { focusFullName = true }
else { focusDisplayName = true }
}
} }
.padding(.bottom) textField("Full name (optional)", text: $fullName)
.focused($focusFullName)
.submitLabel(.go)
.onSubmit {
if canCreateProfile() { createProfile() }
else { focusFullName = true }
}
Spacer() Spacer()
onboardingButtons()
HStack {
if m.users.isEmpty {
Button {
hideKeyboard()
withAnimation {
m.onboardingStage = .step1_SimpleXInfo
}
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
}
}
}
Spacer()
HStack {
Button {
createProfile()
} label: {
Text("Create")
Image(systemName: "greaterthan")
}
.disabled(!canCreateProfile())
}
}
} }
.onAppear() { .onAppear() {
focusDisplayName = true focusDisplayName = true
setLastVersionDefault() setLastVersionDefault()
} }
.alert(item: $alert) { a in
switch a {
case .duplicateUserError: return duplicateUserAlert
case let .createUserError(err): return creatUserErrorAlert(err)
}
}
.padding() .padding()
.frame(maxWidth: .infinity, alignment: .leading)
.keyboardPadding() .keyboardPadding()
} }
func onboardingButtons() -> some View { func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
HStack { TextField(placeholder, text: text)
Button { .textInputAutocapitalization(.never)
hideKeyboard() .disableAutocorrection(true)
.padding(.leading, 28)
.padding(.bottom)
}
func createProfile() {
hideKeyboard()
let profile = Profile(
displayName: displayName,
fullName: fullName
)
do {
m.currentUser = try apiCreateActiveUser(profile)
if m.users.isEmpty {
try startChat()
withAnimation { withAnimation {
m.onboardingStage = .step1_SimpleXInfo onboardingStageDefault.set(.step3_CreateSimpleXAddress)
m.onboardingStage = .step3_CreateSimpleXAddress
} }
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
}
}
Spacer()
Button {
createProfile(displayName, showAlert: showAlert, dismiss: dismiss)
} label: {
HStack {
Text("Create")
Image(systemName: "greaterthan")
}
}
.disabled(!canCreateProfile(displayName))
}
}
private func showAlert(_ alert: UserProfileAlert) {
AlertManager.shared.showAlert(userProfileAlert(alert, $displayName))
}
}
private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) {
hideKeyboard()
let profile = Profile(
displayName: displayName.trimmingCharacters(in: .whitespaces),
fullName: ""
)
let m = ChatModel.shared
do {
m.currentUser = try apiCreateActiveUser(profile)
if m.users.isEmpty {
try startChat()
withAnimation {
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
m.onboardingStage = .step3_CreateSimpleXAddress
}
} else {
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss()
m.users = try listUsers()
try getUserChatData()
}
} catch let error {
switch error as? ChatResponse {
case .chatCmdError(_, .errorStore(.duplicateName)),
.chatCmdError(_, .error(.userExists)):
if m.currentUser == nil {
AlertManager.shared.showAlert(duplicateUserAlert)
} else { } else {
showAlert(.duplicateUserError) onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss()
m.users = try listUsers()
try getUserChatData()
} }
default: } catch let error {
let err: LocalizedStringKey = "Error: \(responseError(error))" switch error as? ChatResponse {
if m.currentUser == nil { case .chatCmdError(_, .errorStore(.duplicateName)),
AlertManager.shared.showAlert(creatUserErrorAlert(err)) .chatCmdError(_, .error(.userExists)):
} else { if m.currentUser == nil {
showAlert(.createUserError(error: err)) AlertManager.shared.showAlert(duplicateUserAlert)
} else {
alert = .duplicateUserError
}
default:
let err: LocalizedStringKey = "Error: \(responseError(error))"
if m.currentUser == nil {
AlertManager.shared.showAlert(creatUserErrorAlert(err))
} else {
alert = .createUserError(error: err)
}
} }
logger.error("Failed to create user or start chat: \(responseError(error))")
} }
logger.error("Failed to create user or start chat: \(responseError(error))")
} }
}
private func canCreateProfile(_ displayName: String) -> Bool { func canCreateProfile() -> Bool {
let name = displayName.trimmingCharacters(in: .whitespaces) displayName != "" && validDisplayName(displayName)
return name != "" && mkValidName(name) == name
}
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert {
switch alert {
case .duplicateUserError: return duplicateUserAlert
case let .createUserError(err): return creatUserErrorAlert(err)
case let .invalidNameError(name): return createInvalidNameAlert(name, displayName)
} }
}
private var duplicateUserAlert: Alert { private var duplicateUserAlert: Alert {
Alert( Alert(
title: Text("Duplicate display name!"), title: Text("Duplicate display name!"),
message: Text("You already have a chat profile with the same display name. Please choose another name.") message: Text("You already have a chat profile with the same display name. Please choose another name.")
) )
} }
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert { private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
Alert( Alert(
title: Text("Error creating profile!"), title: Text("Error creating profile!"),
message: Text(err) message: Text(err)
) )
} }
func createInvalidNameAlert(_ name: String, _ displayName: Binding<String>) -> Alert {
name == ""
? Alert(title: Text("Invalid name!"))
: Alert(
title: Text("Invalid name!"),
message: Text("Correct name to \(name)?"),
primaryButton: .default(
Text("Ok"),
action: { displayName.wrappedValue = name }
),
secondaryButton: .cancel()
)
} }
func validDisplayName(_ name: String) -> Bool { func validDisplayName(_ name: String) -> Bool {
mkValidName(name.trimmingCharacters(in: .whitespaces)) == name name.firstIndex(of: " ") == nil && name.first != "@" && name.first != "#"
}
func mkValidName(_ s: String) -> String {
var c = s.cString(using: .utf8)!
return fromCString(chat_valid_name(&c)!)
} }
struct CreateProfile_Previews: PreviewProvider { struct CreateProfile_Previews: PreviewProvider {
@@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View {
Spacer() Spacer()
if let userAddress = m.userAddress { if let userAddress = m.userAddress {
SimpleXLinkQRCode(uri: userAddress.connReqContact) QRCode(uri: userAddress.connReqContact)
.frame(maxHeight: g.size.width) .frame(maxHeight: g.size.width)
shareQRCodeButton(userAddress) shareQRCodeButton(userAddress)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -126,7 +126,7 @@ struct CreateSimpleXAddress: View {
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
Button { Button {
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) showShareSheet(items: [userAddress.connReqContact])
} label: { } label: {
Label("Share", systemImage: "square.and.arrow.up") Label("Share", systemImage: "square.and.arrow.up")
} }
@@ -194,7 +194,7 @@ struct SendAddressMailView: View {
let messageBody = String(format: NSLocalizedString(""" let messageBody = String(format: NSLocalizedString("""
<p>Hi!</p> <p>Hi!</p>
<p><a href="%@">Connect to me via SimpleX Chat</a></p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>
""", comment: "email text"), simplexChatLink(userAddress.connReqContact)) """, comment: "email text"), userAddress.connReqContact)
MailView( MailView(
isShowing: self.$showMailView, isShowing: self.$showMailView,
result: $mailViewResult, result: $mailViewResult,
@@ -14,7 +14,7 @@ struct OnboardingView: View {
var body: some View { var body: some View {
switch onboarding { switch onboarding {
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
case .step2_CreateProfile: CreateFirstProfile() case .step2_CreateProfile: CreateProfile()
case .step3_CreateSimpleXAddress: CreateSimpleXAddress() case .step3_CreateSimpleXAddress: CreateSimpleXAddress()
case .step4_SetNotificationsMode: SetNotificationsMode() case .step4_SetNotificationsMode: SetNotificationsMode()
case .onboardingComplete: EmptyView() case .onboardingComplete: EmptyView()
@@ -283,37 +283,6 @@ private let versionDescriptions: [VersionDescription] = [
), ),
] ]
), ),
VersionDescription(
version: "v5.4",
post: URL(string: "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html"),
features: [
FeatureDescription(
icon: "desktopcomputer",
title: "Link mobile and desktop apps! 🔗",
description: "Via secure quantum resistant protocol."
),
FeatureDescription(
icon: "person.2",
title: "Better groups",
description: "Faster joining and more reliable messages."
),
FeatureDescription(
icon: "theatermasks",
title: "Incognito groups",
description: "Create a group using a random profile."
),
FeatureDescription(
icon: "hand.raised",
title: "Block group members",
description: "To hide unwanted messages."
),
FeatureDescription(
icon: "gift",
title: "A few more things",
description: "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!"
),
]
),
] ]
private let lastVersion = versionDescriptions.last!.version private let lastVersion = versionDescriptions.last!.version
@@ -1,556 +0,0 @@
//
// ConnectDesktopView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 13/10/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
import CodeScanner
struct ConnectDesktopView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
var viaSettings = false
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
@AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = true
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO) private var connectRemoteViaMulticastAuto = true
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var sessionAddress: String = ""
@State private var remoteCtrls: [RemoteCtrlInfo] = []
@State private var alert: ConnectDesktopAlert?
@State private var showConnectScreen = true
@State private var showQRCodeScanner = true
@State private var firstAppearance = true
private var useMulticast: Bool {
connectRemoteViaMulticast && !remoteCtrls.isEmpty
}
private enum ConnectDesktopAlert: Identifiable {
case unlinkDesktop(rc: RemoteCtrlInfo)
case disconnectDesktop(action: UserDisconnectAction)
case badInvitationError
case badVersionError(version: String?)
case desktopDisconnectedError
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
case let .unlinkDesktop(rc): "unlinkDesktop \(rc.remoteCtrlId)"
case let .disconnectDesktop(action): "disconnectDecktop \(action)"
case .badInvitationError: "badInvitationError"
case let .badVersionError(v): "badVersionError \(v ?? "")"
case .desktopDisconnectedError: "desktopDisconnectedError"
case let .error(title, _): "error \(title)"
}
}
}
private enum UserDisconnectAction: String {
case back
case dismiss // TODO dismiss settings after confirmation
}
var body: some View {
if viaSettings {
viewBody
.modifier(BackButton(label: "Back") {
if m.activeRemoteCtrl {
alert = .disconnectDesktop(action: .back)
} else {
dismiss()
}
})
} else {
NavigationView {
viewBody
}
}
}
var viewBody: some View {
Group {
let discovery = m.remoteCtrlSession?.discovery
if discovery == true || (discovery == nil && !showConnectScreen) {
searchingDesktopView()
} else if let session = m.remoteCtrlSession {
switch session.sessionState {
case .starting: connectingDesktopView(session, nil)
case .searching: searchingDesktopView()
case let .found(rc, compatible): foundDesktopView(session, rc, compatible)
case let .connecting(rc_): connectingDesktopView(session, rc_)
case let .pendingConfirmation(rc_, sessCode):
if confirmRemoteSessions || rc_ == nil {
verifySessionView(session, rc_, sessCode)
} else {
connectingDesktopView(session, rc_).onAppear {
verifyDesktopSessionCode(sessCode)
}
}
case let .connected(rc, _): activeSessionView(session, rc)
}
// The hack below prevents camera freezing when exiting linked devices view.
// Using showQRCodeScanner inside connectDesktopView or passing it as parameter still results in freezing.
} else if showQRCodeScanner || firstAppearance {
connectDesktopView()
} else {
connectDesktopView(showScanner: false)
}
}
.onAppear {
setDeviceName(deviceName)
updateRemoteCtrls()
showConnectScreen = !useMulticast
if m.remoteCtrlSession != nil {
disconnectDesktop()
} else if useMulticast {
findKnownDesktop()
}
// The hack below prevents camera freezing when exiting linked devices view.
// `firstAppearance` prevents camera flicker when the view first opens.
// moving `showQRCodeScanner = false` to `onDisappear` (to avoid `firstAppearance`) does not prevent freeze.
showQRCodeScanner = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
firstAppearance = false
showQRCodeScanner = true
}
}
.onDisappear {
if m.remoteCtrlSession != nil {
showConnectScreen = false
disconnectDesktop()
}
}
.onChange(of: deviceName) {
setDeviceName($0)
}
.onChange(of: m.activeRemoteCtrl) {
UIApplication.shared.isIdleTimerDisabled = $0
}
.alert(item: $alert) { a in
switch a {
case let .unlinkDesktop(rc):
Alert(
title: Text("Unlink desktop?"),
primaryButton: .destructive(Text("Unlink")) {
unlinkDesktop(rc)
},
secondaryButton: .cancel()
)
case let .disconnectDesktop(action):
Alert(
title: Text("Disconnect desktop?"),
primaryButton: .destructive(Text("Disconnect")) {
disconnectDesktop(action)
},
secondaryButton: .cancel()
)
case .badInvitationError:
Alert(title: Text("Bad desktop address"))
case let .badVersionError(v):
Alert(
title: Text("Incompatible version"),
message: Text("Desktop app version \(v ?? "") is not compatible with this app.")
)
case .desktopDisconnectedError:
Alert(title: Text("Connection terminated"))
case let .error(title, error):
Alert(title: Text(title), message: Text(error))
}
}
.interactiveDismissDisabled(m.activeRemoteCtrl)
}
private func connectDesktopView(showScanner: Bool = true) -> some View {
List {
Section("This device name") {
devicesView()
}
if showScanner {
scanDesctopAddressView()
}
if developerTools {
desktopAddressView()
}
}
.navigationTitle("Connect to desktop")
}
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
List {
Section("Connecting to desktop") {
ctrlDeviceNameText(session, rc)
ctrlDeviceVersionText(session)
}
if let sessCode = session.sessionCode {
Section("Session code") {
sessionCodeText(sessCode)
}
}
Section {
disconnectButton()
}
}
.navigationTitle("Connecting to desktop")
}
private func searchingDesktopView() -> some View {
List {
Section("This device name") {
devicesView()
}
Section("Found desktop") {
Text("Waiting for desktop...").italic()
Button {
disconnectDesktop()
} label: {
Label("Scan QR code", systemImage: "qrcode")
}
}
}
.navigationTitle("Connecting to desktop")
}
@ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View {
let v = List {
Section("This device name") {
devicesView()
}
Section("Found desktop") {
ctrlDeviceNameText(session, rc)
ctrlDeviceVersionText(session)
if !compatible {
Text("Not compatible!").foregroundColor(.red)
} else if !connectRemoteViaMulticastAuto {
Button {
confirmKnownDesktop(rc)
} label: {
Label("Connect", systemImage: "checkmark")
}
}
}
if !compatible && !connectRemoteViaMulticastAuto {
Section {
disconnectButton("Cancel")
}
}
}
.navigationTitle("Found desktop")
if compatible && connectRemoteViaMulticastAuto {
v.onAppear { confirmKnownDesktop(rc) }
} else {
v
}
}
private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View {
List {
Section("Connected to desktop") {
ctrlDeviceNameText(session, rc)
ctrlDeviceVersionText(session)
}
Section("Verify code with desktop") {
sessionCodeText(sessCode)
Button {
verifyDesktopSessionCode(sessCode)
} label: {
Label("Confirm", systemImage: "checkmark")
}
}
Section {
disconnectButton()
}
}
.navigationTitle("Verify connection")
}
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "")
if (rc == nil) {
t = t + Text(" ") + Text("(new)").italic()
}
return t
}
private func ctrlDeviceVersionText(_ session: RemoteCtrlSession) -> Text {
let v = session.ctrlAppInfo?.appVersionRange.maxVersion
var t = Text("v\(v ?? "")")
if v != session.appVersion {
t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic()
}
return t
}
private func activeSessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo) -> some View {
List {
Section("Connected desktop") {
Text(rc.deviceViewName)
ctrlDeviceVersionText(session)
}
if let sessCode = session.sessionCode {
Section("Session code") {
sessionCodeText(sessCode)
}
}
Section {
disconnectButton()
} footer: {
// This is specific to iOS
Text("Keep the app open to use it from desktop")
}
}
.navigationTitle("Connected to desktop")
}
private func sessionCodeText(_ code: String) -> some View {
Text(code.prefix(23))
}
private func devicesView() -> some View {
Group {
TextField("Enter this device name…", text: $deviceName)
if !remoteCtrls.isEmpty {
NavigationLink {
linkedDesktopsView()
} label: {
Text("Linked desktops")
}
}
}
}
private func scanDesctopAddressView() -> some View {
Section("Scan QR code from desktop") {
CodeScannerView(codeTypes: [.qr], completion: processDesktopQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.padding(.horizontal)
}
}
private func desktopAddressView() -> some View {
Section("Desktop address") {
if sessionAddress.isEmpty {
Button {
sessionAddress = UIPasteboard.general.string ?? ""
} label: {
Label("Paste desktop address", systemImage: "doc.plaintext")
}
.disabled(!UIPasteboard.general.hasStrings)
} else {
HStack {
Text(sessionAddress).lineLimit(1)
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
.onTapGesture { sessionAddress = "" }
}
}
Button {
connectDesktopAddress(sessionAddress)
} label: {
Label("Connect to desktop", systemImage: "rectangle.connected.to.line.below")
}
.disabled(sessionAddress.isEmpty)
}
}
private func linkedDesktopsView() -> some View {
List {
Section("Desktop devices") {
ForEach(remoteCtrls, id: \.remoteCtrlId) { rc in
remoteCtrlView(rc)
}
.onDelete { indexSet in
if let i = indexSet.first, i < remoteCtrls.count {
alert = .unlinkDesktop(rc: remoteCtrls[i])
}
}
}
Section("Linked desktop options") {
Toggle("Verify connections", isOn: $confirmRemoteSessions)
Toggle("Discover via local network", isOn: $connectRemoteViaMulticast)
if connectRemoteViaMulticast {
Toggle("Connect automatically", isOn: $connectRemoteViaMulticastAuto)
}
}
}
.navigationTitle("Linked desktops")
}
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
Text(rc.deviceViewName)
}
private func setDeviceName(_ name: String) {
do {
try setLocalDeviceName(deviceName)
} catch let e {
errorAlert(e)
}
}
private func updateRemoteCtrls() {
do {
remoteCtrls = try listRemoteCtrls()
} catch let e {
errorAlert(e)
}
}
private func processDesktopQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp {
case let .success(r): connectDesktopAddress(r.string)
case let .failure(e): errorAlert(e)
}
}
private func findKnownDesktop() {
Task {
do {
try await findKnownRemoteCtrl()
await MainActor.run {
m.remoteCtrlSession = RemoteCtrlSession(
ctrlAppInfo: nil,
appVersion: "",
sessionState: .searching
)
showConnectScreen = true
}
} catch let e {
await MainActor.run {
errorAlert(e)
}
}
}
}
private func confirmKnownDesktop(_ rc: RemoteCtrlInfo) {
connectDesktop_ {
try await confirmRemoteCtrl(rc.remoteCtrlId)
}
}
private func connectDesktopAddress(_ addr: String) {
connectDesktop_ {
try await connectRemoteCtrl(desktopAddress: addr)
}
}
private func connectDesktop_(_ connect: @escaping () async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String)) {
Task {
do {
let (rc_, ctrlAppInfo, v) = try await connect()
await MainActor.run {
sessionAddress = ""
m.remoteCtrlSession = RemoteCtrlSession(
ctrlAppInfo: ctrlAppInfo,
appVersion: v,
sessionState: .connecting(remoteCtrl_: rc_)
)
}
} catch let e {
await MainActor.run {
switch e as? ChatResponse {
case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError
case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError
case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v)
case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil)
case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError
default: errorAlert(e)
}
}
}
}
}
private func verifyDesktopSessionCode(_ sessCode: String) {
Task {
do {
let rc = try await verifyRemoteCtrlSession(sessCode)
await MainActor.run {
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(.connected(remoteCtrl: rc, sessionCode: sessCode))
}
await MainActor.run {
updateRemoteCtrls()
}
} catch let error {
await MainActor.run {
errorAlert(error)
}
}
}
}
private func disconnectButton(_ label: LocalizedStringKey = "Disconnect") -> some View {
Button {
disconnectDesktop(.dismiss)
} label: {
Label(label, systemImage: "multiply")
}
}
private func disconnectDesktop(_ action: UserDisconnectAction? = nil) {
Task {
do {
try await stopRemoteCtrl()
await MainActor.run {
if case .connected = m.remoteCtrlSession?.sessionState {
switchToLocalSession()
} else {
m.remoteCtrlSession = nil
}
switch action {
case .back: dismiss()
case .dismiss: dismiss()
case .none: ()
}
}
} catch let e {
await MainActor.run {
errorAlert(e)
}
}
}
}
private func unlinkDesktop(_ rc: RemoteCtrlInfo) {
Task {
do {
try await deleteRemoteCtrl(rc.remoteCtrlId)
await MainActor.run {
remoteCtrls.removeAll(where: { $0.remoteCtrlId == rc.remoteCtrlId })
}
} catch let e {
await MainActor.run {
errorAlert(e)
}
}
}
}
private func errorAlert(_ error: Error) {
let a = getErrorAlert(error, "Error")
alert = .error(title: a.title, error: a.message)
}
}
#Preview {
ConnectDesktopView()
}
@@ -66,9 +66,6 @@ struct PrivacySettings: View {
Section { Section {
settingsRow("lock.doc") { settingsRow("lock.doc") {
Toggle("Encrypt local files", isOn: $encryptLocalFiles) Toggle("Encrypt local files", isOn: $encryptLocalFiles)
.onChange(of: encryptLocalFiles) {
setEncryptLocalFiles($0)
}
} }
settingsRow("photo") { settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages) Toggle("Auto-accept images", isOn: $autoAcceptImages)
@@ -93,9 +90,7 @@ struct PrivacySettings: View {
} }
settingsRow("link") { settingsRow("link") {
Picker("SimpleX links", selection: $simplexLinkMode) { Picker("SimpleX links", selection: $simplexLinkMode) {
ForEach( ForEach(SimpleXLinkMode.values) { mode in
SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode])
) { mode in
Text(mode.text) Text(mode.text)
} }
} }
@@ -106,6 +101,10 @@ struct PrivacySettings: View {
} }
} header: { } header: {
Text("Chats") Text("Chats")
} footer: {
if case .browser = simplexLinkMode {
Text("Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.")
}
} }
Section { Section {
@@ -119,7 +118,7 @@ struct PrivacySettings: View {
Text("Send delivery receipts to") Text("Send delivery receipts to")
} footer: { } footer: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.") Text("These settings are for your current profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.")
Text("They can be overridden in contact and group settings.") Text("They can be overridden in contact and group settings.")
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@@ -184,16 +183,6 @@ struct PrivacySettings: View {
} }
} }
private func setEncryptLocalFiles(_ enable: Bool) {
do {
try apiSetEncryptLocalFiles(enable)
} catch let error {
let err = responseError(error)
logger.error("apiSetEncryptLocalFiles \(err)")
alert = .error(title: "Error", error: "\(err)")
}
}
private func setOrAskSendReceiptsContacts(_ enable: Bool) { private func setOrAskSendReceiptsContacts(_ enable: Bool) {
contactReceiptsOverrides = m.chats.reduce(0) { count, chat in contactReceiptsOverrides = m.chats.reduce(0) { count, chat in
let sendRcpts = chat.chatInfo.contact?.chatSettings.sendRcpts let sendRcpts = chat.chatInfo.contact?.chatSettings.sendRcpts
@@ -356,7 +345,7 @@ struct SimplexLockView: View {
var id: Self { self } var id: Self { self }
} }
let laDelays: [Int] = [10, 30, 60, 180, 600, 0] let laDelays: [Int] = [10, 30, 60, 180, 0]
func laDelayText(_ t: Int) -> LocalizedStringKey { func laDelayText(_ t: Int) -> LocalizedStringKey {
let m = t / 60 let m = t / 60
@@ -378,7 +367,6 @@ struct SimplexLockView: View {
Text(mode.text) Text(mode.text)
} }
} }
.frame(height: 36)
if performLA { if performLA {
Picker("Lock after", selection: $laLockDelay) { Picker("Lock after", selection: $laLockDelay) {
let delays = laDelays.contains(laLockDelay) ? laDelays : [laLockDelay] + laDelays let delays = laDelays.contains(laLockDelay) ? laDelays : [laLockDelay] + laDelays
@@ -386,7 +374,6 @@ struct SimplexLockView: View {
Text(laDelayText(t)) Text(laDelayText(t))
} }
} }
.frame(height: 36)
if showChangePassword && laMode == .passcode { if showChangePassword && laMode == .passcode {
Button("Change passcode") { Button("Change passcode") {
changeLAPassword() changeLAPassword()
@@ -53,10 +53,6 @@ let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
let DEFAULT_ONBOARDING_STAGE = "onboardingStage" let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime" let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime"
let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites" let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites"
let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
let appDefaults: [String: Any] = [ let appDefaults: [String: Any] = [
DEFAULT_SHOW_LA_NOTICE: false, DEFAULT_SHOW_LA_NOTICE: false,
@@ -89,10 +85,7 @@ let appDefaults: [String: Any] = [
DEFAULT_SHOW_MUTE_PROFILE_ALERT: true, DEFAULT_SHOW_MUTE_PROFILE_ALERT: true,
DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue, DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue,
DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300, DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300,
DEFAULT_SHOW_UNREAD_AND_FAVORITES: false, DEFAULT_SHOW_UNREAD_AND_FAVORITES: false
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
] ]
enum SimpleXLinkMode: String, Identifiable { enum SimpleXLinkMode: String, Identifiable {
@@ -100,7 +93,7 @@ enum SimpleXLinkMode: String, Identifiable {
case full case full
case browser case browser
static var values: [SimpleXLinkMode] = [.description, .full] static var values: [SimpleXLinkMode] = [.description, .full, .browser]
public var id: Self { self } public var id: Self { self }
@@ -185,12 +178,6 @@ struct SettingsView: View {
} label: { } label: {
settingsRow("switch.2") { Text("Chat preferences") } settingsRow("switch.2") { Text("Chat preferences") }
} }
NavigationLink {
ConnectDesktopView(viaSettings: true)
} label: {
settingsRow("desktopcomputer") { Text("Use from desktop") }
}
} }
.disabled(chatModel.chatRunning != true) .disabled(chatModel.chatRunning != true)
@@ -375,9 +362,7 @@ struct SettingsView: View {
func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View { func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center) Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(color)
.symbolRenderingMode(.monochrome)
.foregroundColor(color)
content().padding(.leading, indent) content().padding(.leading, indent)
} }
} }
@@ -396,9 +381,7 @@ struct ProfilePreview: View {
Text(profileOf.displayName) Text(profileOf.displayName)
.fontWeight(.bold) .fontWeight(.bold)
.font(.title2) .font(.title2)
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { Text(profileOf.fullName)
Text(profileOf.fullName)
}
} }
} }
} }
@@ -190,7 +190,7 @@ struct UserAddressView: View {
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
Section { Section {
MutableQRCode(uri: Binding.constant(simplexChatLink(userAddress.connReqContact))) MutableQRCode(uri: Binding.constant(userAddress.connReqContact))
shareQRCodeButton(userAddress) shareQRCodeButton(userAddress)
if MFMailComposeViewController.canSendMail() { if MFMailComposeViewController.canSendMail() {
shareViaEmailButton(userAddress) shareViaEmailButton(userAddress)
@@ -248,7 +248,7 @@ struct UserAddressView: View {
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
Button { Button {
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) showShareSheet(items: [userAddress.connReqContact])
} label: { } label: {
settingsRow("square.and.arrow.up") { settingsRow("square.and.arrow.up") {
Text("Share address") Text("Share address")
@@ -17,8 +17,6 @@ struct UserProfile: View {
@State private var showImagePicker = false @State private var showImagePicker = false
@State private var showTakePhoto = false @State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil @State private var chosenImage: UIImage? = nil
@State private var alert: UserProfileAlert?
@FocusState private var focusDisplayName
var body: some View { var body: some View {
let user: User = chatModel.currentUser! let user: User = chatModel.currentUser!
@@ -49,27 +47,18 @@ struct UserProfile: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
if !validNewProfileName(user) { if !validDisplayName(profile.displayName) {
Button { Image(systemName: "exclamationmark.circle")
alert = .invalidNameError(validName: mkValidName(profile.displayName)) .foregroundColor(.red)
} label: { .padding(.bottom, 10)
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
} }
profileNameTextEdit("Profile name", $profile.displayName) profileNameTextEdit("Display name", $profile.displayName)
.focused($focusDisplayName)
}
.padding(.bottom)
if showFullName(user) {
profileNameTextEdit("Full name (optional)", $profile.fullName)
.padding(.bottom)
} }
profileNameTextEdit("Full name (optional)", $profile.fullName)
HStack(spacing: 20) { HStack(spacing: 20) {
Button("Cancel") { editProfile = false } Button("Cancel") { editProfile = false }
Button("Save (and notify contacts)") { saveProfile() } Button("Save (and notify contacts)") { saveProfile() }
.disabled(!canSaveProfile(user)) .disabled(profile.displayName == "" || !validDisplayName(profile.displayName))
} }
} }
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@@ -85,14 +74,11 @@ struct UserProfile: View {
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
VStack(alignment: .leading) { VStack(alignment: .leading) {
profileNameView("Profile name:", user.profile.displayName) profileNameView("Display name:", user.profile.displayName)
if showFullName(user) { profileNameView("Full name:", user.profile.fullName)
profileNameView("Full name:", user.profile.fullName)
}
Button("Edit") { Button("Edit") {
profile = fromLocalProfile(user.profile) profile = fromLocalProfile(user.profile)
editProfile = true editProfile = true
focusDisplayName = true
} }
} }
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@@ -131,12 +117,14 @@ struct UserProfile: View {
profile.image = nil profile.image = nil
} }
} }
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
} }
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View { func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
TextField(label, text: name) TextField(label, text: name)
.padding(.leading, 32) .textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.bottom)
.padding(.leading, 28)
} }
func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View { func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View {
@@ -153,34 +141,19 @@ struct UserProfile: View {
showChooseSource = true showChooseSource = true
} }
private func validNewProfileName(_ user: User) -> Bool {
profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces))
}
private func showFullName(_ user: User) -> Bool {
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
}
private func canSaveProfile(_ user: User) -> Bool {
profile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName(user)
}
func saveProfile() { func saveProfile() {
Task { Task {
do { do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
DispatchQueue.main.async { DispatchQueue.main.async {
chatModel.updateCurrentUser(newProfile) chatModel.updateCurrentUser(newProfile)
profile = newProfile profile = newProfile
} }
editProfile = false
} else {
alert = .duplicateUserError
} }
} catch { } catch {
logger.error("UserProfile apiUpdateProfile error: \(responseError(error))") logger.error("UserProfile apiUpdateProfile error: \(responseError(error))")
} }
editProfile = false
} }
} }
} }
-4
View File
@@ -18,9 +18,5 @@
<array> <array>
<string>$(AppIdentifierPrefix)chat.simplex.app</string> <string>$(AppIdentifierPrefix)chat.simplex.app</string>
</array> </array>
<key>com.apple.developer.networking.multicast</key>
<true/>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
</dict> </dict>
</plist> </plist>
@@ -87,10 +87,6 @@
<target>%@ / %@</target> <target>%@ / %@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve">
<source>%@ and %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve"> <trans-unit id="%@ and %@ connected" xml:space="preserve">
<source>%@ and %@ connected</source> <source>%@ and %@ connected</source>
<target>%@ и %@ са свързани</target> <target>%@ и %@ са свързани</target>
@@ -101,10 +97,6 @@
<target>%1$@ в %2$@:</target> <target>%1$@ в %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note> <note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ connected" xml:space="preserve">
<source>%@ connected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve"> <trans-unit id="%@ is connected!" xml:space="preserve">
<source>%@ is connected!</source> <source>%@ is connected!</source>
<target>%@ е свързан!</target> <target>%@ е свързан!</target>
@@ -130,10 +122,6 @@
<target>%@ иска да се свърже!</target> <target>%@ иска да се свърже!</target>
<note>notification title</note> <note>notification title</note>
</trans-unit> </trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve">
<source>%@, %@ and %lld other members connected</source> <source>%@, %@ and %lld other members connected</source>
<target>%@, %@ и %lld други членове са свързани</target> <target>%@, %@ и %lld други членове са свързани</target>
@@ -199,27 +187,11 @@
<target>%lld файл(а) с общ размер от %@</target> <target>%lld файл(а) с общ размер от %@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld group events" xml:space="preserve">
<source>%lld group events</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld members" xml:space="preserve"> <trans-unit id="%lld members" xml:space="preserve">
<source>%lld members</source> <source>%lld members</source>
<target>%lld членове</target> <target>%lld членове</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked" xml:space="preserve">
<source>%lld messages blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages moderated by %@" xml:space="preserve">
<source>%lld messages moderated by %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld minutes" xml:space="preserve"> <trans-unit id="%lld minutes" xml:space="preserve">
<source>%lld minutes</source> <source>%lld minutes</source>
<target>%lld минути</target> <target>%lld минути</target>
@@ -290,14 +262,6 @@
<target>(</target> <target>(</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve"> <trans-unit id=")" xml:space="preserve">
<source>)</source> <source>)</source>
<target>)</target> <target>)</target>
@@ -386,12 +350,6 @@
- и още!</target> - и още!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve"> <trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes. <source>- voice messages up to 5 minutes.
- custom time to disappear. - custom time to disappear.
@@ -406,10 +364,6 @@
<target>.</target> <target>.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="0 sec" xml:space="preserve">
<source>0 sec</source>
<note>time to disappear</note>
</trans-unit>
<trans-unit id="0s" xml:space="preserve"> <trans-unit id="0s" xml:space="preserve">
<source>0s</source> <source>0s</source>
<target>0s</target> <target>0s</target>
@@ -635,10 +589,6 @@
<target>Всички съобщения ще бъдат изтрити - това не може да бъде отменено! Съобщенията ще бъдат изтрити САМО за вас.</target> <target>Всички съобщения ще бъдат изтрити - това не може да бъде отменено! Съобщенията ще бъдат изтрити САМО за вас.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve">
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve"> <trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source> <source>All your contacts will remain connected.</source>
<target>Всички ваши контакти ще останат свързани.</target> <target>Всички ваши контакти ще останат свързани.</target>
@@ -744,14 +694,6 @@
<target>Вече сте свързани?</target> <target>Вече сте свързани?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Already connecting!" xml:space="preserve">
<source>Already connecting!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Already joining the group!" xml:space="preserve">
<source>Already joining the group!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Always use relay" xml:space="preserve"> <trans-unit id="Always use relay" xml:space="preserve">
<source>Always use relay</source> <source>Always use relay</source>
<target>Винаги използвай реле</target> <target>Винаги използвай реле</target>
@@ -872,10 +814,6 @@
<target>Назад</target> <target>Назад</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve"> <trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source> <source>Bad message ID</source>
<target>Лошо ID на съобщението</target> <target>Лошо ID на съобщението</target>
@@ -886,31 +824,11 @@
<target>Лош хеш на съобщението</target> <target>Лош хеш на съобщението</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source> <source>Better messages</source>
<target>По-добри съобщения</target> <target>По-добри съобщения</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block" xml:space="preserve">
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
<source>Block member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
<source>Both you and your contact can add message reactions.</source> <source>Both you and your contact can add message reactions.</source>
<target>И вие, и вашият контакт можете да добавяте реакции към съобщението.</target> <target>И вие, и вашият контакт можете да добавяте реакции към съобщението.</target>
@@ -1172,8 +1090,9 @@
<target>Свързване</target> <target>Свързване</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve"> <trans-unit id="Connect directly" xml:space="preserve">
<source>Connect automatically</source> <source>Connect directly</source>
<target>Свързване директно</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve"> <trans-unit id="Connect incognito" xml:space="preserve">
@@ -1181,26 +1100,14 @@
<target>Свързване инкогнито</target> <target>Свързване инкогнито</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve"> <trans-unit id="Connect via contact link" xml:space="preserve">
<source>Connect to desktop</source> <source>Connect via contact link</source>
<target>Свързване чрез линк на контакта</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve"> <trans-unit id="Connect via group link?" xml:space="preserve">
<source>Connect to yourself?</source> <source>Connect via group link?</source>
<note>No comment provided by engineer.</note> <target>Свързване чрез групов линк?</target>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own SimpleX address!" xml:space="preserve">
<source>Connect to yourself?
This is your own SimpleX address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own one-time link!" xml:space="preserve">
<source>Connect to yourself?
This is your own one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact address" xml:space="preserve">
<source>Connect via contact address</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect via link" xml:space="preserve"> <trans-unit id="Connect via link" xml:space="preserve">
@@ -1218,18 +1125,6 @@ This is your own one-time link!</source>
<target>Свързване чрез еднократен линк за връзка</target> <target>Свързване чрез еднократен линк за връзка</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect with %@" xml:space="preserve">
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve"> <trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source> <source>Connecting to server…</source>
<target>Свързване със сървъра…</target> <target>Свързване със сървъра…</target>
@@ -1240,10 +1135,6 @@ This is your own one-time link!</source>
<target>Свързване със сървър…(грешка: %@)</target> <target>Свързване със сървър…(грешка: %@)</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve"> <trans-unit id="Connection" xml:space="preserve">
<source>Connection</source> <source>Connection</source>
<target>Връзка</target> <target>Връзка</target>
@@ -1264,10 +1155,6 @@ This is your own one-time link!</source>
<target>Заявката за връзка е изпратена!</target> <target>Заявката за връзка е изпратена!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve"> <trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source> <source>Connection timeout</source>
<target>Времето на изчакване за установяване на връзката изтече</target> <target>Времето на изчакване за установяване на връзката изтече</target>
@@ -1283,6 +1170,11 @@ This is your own one-time link!</source>
<target>Контактът вече съществува</target> <target>Контактът вече съществува</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Contact and all messages will be deleted - this cannot be undone!</source>
<target>Контактът и всички съобщения ще бъдат изтрити - това не може да бъде отменено!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact hidden:" xml:space="preserve"> <trans-unit id="Contact hidden:" xml:space="preserve">
<source>Contact hidden:</source> <source>Contact hidden:</source>
<target>Контактът е скрит:</target> <target>Контактът е скрит:</target>
@@ -1333,10 +1225,6 @@ This is your own one-time link!</source>
<target>Версия на ядрото: v%@</target> <target>Версия на ядрото: v%@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve"> <trans-unit id="Create" xml:space="preserve">
<source>Create</source> <source>Create</source>
<target>Създай</target> <target>Създай</target>
@@ -1347,10 +1235,6 @@ This is your own one-time link!</source>
<target>Създай SimpleX адрес</target> <target>Създай SimpleX адрес</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve"> <trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source> <source>Create an address to let people connect with you.</source>
<target>Създайте адрес, за да позволите на хората да се свързват с вас.</target> <target>Създайте адрес, за да позволите на хората да се свързват с вас.</target>
@@ -1361,10 +1245,6 @@ This is your own one-time link!</source>
<target>Създай файл</target> <target>Създай файл</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Create group" xml:space="preserve">
<source>Create group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create group link" xml:space="preserve"> <trans-unit id="Create group link" xml:space="preserve">
<source>Create group link</source> <source>Create group link</source>
<target>Създай групов линк</target> <target>Създай групов линк</target>
@@ -1385,10 +1265,6 @@ This is your own one-time link!</source>
<target>Създай линк за еднократна покана</target> <target>Създай линк за еднократна покана</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create queue" xml:space="preserve"> <trans-unit id="Create queue" xml:space="preserve">
<source>Create queue</source> <source>Create queue</source>
<target>Създай опашка</target> <target>Създай опашка</target>
@@ -1547,10 +1423,6 @@ This is your own one-time link!</source>
<target>Изтрий</target> <target>Изтрий</target>
<note>chat item action</note> <note>chat item action</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete %lld messages?" xml:space="preserve">
<source>Delete %lld messages?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete Contact" xml:space="preserve"> <trans-unit id="Delete Contact" xml:space="preserve">
<source>Delete Contact</source> <source>Delete Contact</source>
<target>Изтрий контакт</target> <target>Изтрий контакт</target>
@@ -1576,10 +1448,6 @@ This is your own one-time link!</source>
<target>Изтрий всички файлове</target> <target>Изтрий всички файлове</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete and notify contact" xml:space="preserve">
<source>Delete and notify contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete archive" xml:space="preserve"> <trans-unit id="Delete archive" xml:space="preserve">
<source>Delete archive</source> <source>Delete archive</source>
<target>Изтрий архив</target> <target>Изтрий архив</target>
@@ -1610,9 +1478,9 @@ This is your own one-time link!</source>
<target>Изтрий контакт</target> <target>Изтрий контакт</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete contact?&#10;This cannot be undone!" xml:space="preserve"> <trans-unit id="Delete contact?" xml:space="preserve">
<source>Delete contact? <source>Delete contact?</source>
This cannot be undone!</source> <target>Изтрий контакт?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete database" xml:space="preserve"> <trans-unit id="Delete database" xml:space="preserve">
@@ -1755,18 +1623,6 @@ This cannot be undone!</source>
<target>Описание</target> <target>Описание</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve"> <trans-unit id="Develop" xml:space="preserve">
<source>Develop</source> <source>Develop</source>
<target>Разработване</target> <target>Разработване</target>
@@ -1857,17 +1713,19 @@ This cannot be undone!</source>
<target>Прекъсни връзката</target> <target>Прекъсни връзката</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve"> <trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source> <source>Discover and join groups</source>
<target>Открийте и се присъединете към групи</target> <target>Открийте и се присъединете към групи</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve"> <trans-unit id="Display name" xml:space="preserve">
<source>Discover via local network</source> <source>Display name</source>
<target>Показвано Име</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Display name:" xml:space="preserve">
<source>Display name:</source>
<target>Показвано име:</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
@@ -2040,14 +1898,6 @@ This cannot be undone!</source>
<target>Криптирано съобщение: неочаквана грешка</target> <target>Криптирано съобщение: неочаквана грешка</target>
<note>notification</note> <note>notification</note>
</trans-unit> </trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve"> <trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source> <source>Enter Passcode</source>
<target>Въведете kодa за достъп</target> <target>Въведете kодa за достъп</target>
@@ -2058,10 +1908,6 @@ This cannot be undone!</source>
<target>Въведи правилна парола.</target> <target>Въведи правилна парола.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter group name…" xml:space="preserve">
<source>Enter group name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter passphrase…" xml:space="preserve"> <trans-unit id="Enter passphrase…" xml:space="preserve">
<source>Enter passphrase…</source> <source>Enter passphrase…</source>
<target>Въведи парола…</target> <target>Въведи парола…</target>
@@ -2077,10 +1923,6 @@ This cannot be undone!</source>
<target>Въведи сървъра ръчно</target> <target>Въведи сървъра ръчно</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve"> <trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source> <source>Enter welcome message…</source>
<target>Въведи съобщение при посрещане…</target> <target>Въведи съобщение при посрещане…</target>
@@ -2091,10 +1933,6 @@ This cannot be undone!</source>
<target>Въведи съобщение при посрещане…(незадължително)</target> <target>Въведи съобщение при посрещане…(незадължително)</target>
<note>placeholder</note> <note>placeholder</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter your name…" xml:space="preserve">
<source>Enter your name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error" xml:space="preserve"> <trans-unit id="Error" xml:space="preserve">
<source>Error</source> <source>Error</source>
<target>Грешка при свързване със сървъра</target> <target>Грешка при свързване със сървъра</target>
@@ -2152,7 +1990,6 @@ This cannot be undone!</source>
</trans-unit> </trans-unit>
<trans-unit id="Error creating member contact" xml:space="preserve"> <trans-unit id="Error creating member contact" xml:space="preserve">
<source>Error creating member contact</source> <source>Error creating member contact</source>
<target>Грешка при създаване на контакт с член</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve"> <trans-unit id="Error creating profile!" xml:space="preserve">
@@ -2287,7 +2124,6 @@ This cannot be undone!</source>
</trans-unit> </trans-unit>
<trans-unit id="Error sending member contact invitation" xml:space="preserve"> <trans-unit id="Error sending member contact invitation" xml:space="preserve">
<source>Error sending member contact invitation</source> <source>Error sending member contact invitation</source>
<target>Грешка при изпращане на съобщение за покана за контакт</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Error sending message" xml:space="preserve"> <trans-unit id="Error sending message" xml:space="preserve">
@@ -2370,10 +2206,6 @@ This cannot be undone!</source>
<target>Изход без запазване</target> <target>Изход без запазване</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Expand" xml:space="preserve">
<source>Expand</source>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Export database" xml:space="preserve"> <trans-unit id="Export database" xml:space="preserve">
<source>Export database</source> <source>Export database</source>
<target>Експортирай база данни</target> <target>Експортирай база данни</target>
@@ -2404,10 +2236,6 @@ This cannot be undone!</source>
<target>Бързо и без чакане, докато подателят е онлайн!</target> <target>Бързо и без чакане, докато подателят е онлайн!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve"> <trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source> <source>Favorite</source>
<target>Любим</target> <target>Любим</target>
@@ -2503,10 +2331,6 @@ This cannot be undone!</source>
<target>За конзолата</target> <target>За конзолата</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve"> <trans-unit id="French interface" xml:space="preserve">
<source>French interface</source> <source>French interface</source>
<target>Френски интерфейс</target> <target>Френски интерфейс</target>
@@ -2527,10 +2351,6 @@ This cannot be undone!</source>
<target>Пълно име:</target> <target>Пълно име:</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve">
<source>Fully re-implemented - work in background!</source> <source>Fully re-implemented - work in background!</source>
<target>Напълно преработено - работи във фонов режим!</target> <target>Напълно преработено - работи във фонов режим!</target>
@@ -2551,14 +2371,6 @@ This cannot be undone!</source>
<target>Група</target> <target>Група</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Group already exists" xml:space="preserve">
<source>Group already exists</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group already exists!" xml:space="preserve">
<source>Group already exists!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group display name" xml:space="preserve"> <trans-unit id="Group display name" xml:space="preserve">
<source>Group display name</source> <source>Group display name</source>
<target>Показвано име на групата</target> <target>Показвано име на групата</target>
@@ -2829,10 +2641,6 @@ This cannot be undone!</source>
<target>Инкогнито</target> <target>Инкогнито</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve"> <trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source> <source>Incognito mode</source>
<target>Режим инкогнито</target> <target>Режим инкогнито</target>
@@ -2863,10 +2671,6 @@ This cannot be undone!</source>
<target>Несъвместима версия на базата данни</target> <target>Несъвместима версия на базата данни</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve"> <trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source> <source>Incorrect passcode</source>
<target>Неправилен kод за достъп</target> <target>Неправилен kод за достъп</target>
@@ -2914,10 +2718,6 @@ This cannot be undone!</source>
<target>Невалиден линк за връзка</target> <target>Невалиден линк за връзка</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve"> <trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source> <source>Invalid server address!</source>
<target>Невалиден адрес на сървъра!</target> <target>Невалиден адрес на сървъра!</target>
@@ -3009,33 +2809,16 @@ This cannot be undone!</source>
<target>Влез в групата</target> <target>Влез в групата</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
<source>Join group?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join incognito" xml:space="preserve"> <trans-unit id="Join incognito" xml:space="preserve">
<source>Join incognito</source> <source>Join incognito</source>
<target>Влез инкогнито</target> <target>Влез инкогнито</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join with current profile" xml:space="preserve">
<source>Join with current profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join your group?&#10;This is your link for group %@!" xml:space="preserve">
<source>Join your group?
This is your link for group %@!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Joining group" xml:space="preserve"> <trans-unit id="Joining group" xml:space="preserve">
<source>Joining group</source> <source>Joining group</source>
<target>Присъединяване към групата</target> <target>Присъединяване към групата</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve"> <trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source> <source>Keep your connections</source>
<target>Запазете връзките си</target> <target>Запазете връзките си</target>
@@ -3096,18 +2879,6 @@ This is your link for group %@!</source>
<target>Ограничения</target> <target>Ограничения</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve"> <trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source> <source>Live message!</source>
<target>Съобщение на живо!</target> <target>Съобщение на живо!</target>
@@ -3258,10 +3029,6 @@ This is your link for group %@!</source>
<target>Съобщения и файлове</target> <target>Съобщения и файлове</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Messages from %@ will be shown!" xml:space="preserve">
<source>Messages from %@ will be shown!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migrating database archive…" xml:space="preserve"> <trans-unit id="Migrating database archive…" xml:space="preserve">
<source>Migrating database archive…</source> <source>Migrating database archive…</source>
<target>Архивът на базата данни се мигрира…</target> <target>Архивът на базата данни се мигрира…</target>
@@ -3457,10 +3224,6 @@ This is your link for group %@!</source>
<target>Няма получени или изпратени файлове</target> <target>Няма получени или изпратени файлове</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve"> <trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source> <source>Notifications</source>
<target>Известия</target> <target>Известия</target>
@@ -3597,7 +3360,6 @@ This is your link for group %@!</source>
</trans-unit> </trans-unit>
<trans-unit id="Open" xml:space="preserve"> <trans-unit id="Open" xml:space="preserve">
<source>Open</source> <source>Open</source>
<target>Отвори</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Open Settings" xml:space="preserve"> <trans-unit id="Open Settings" xml:space="preserve">
@@ -3615,10 +3377,6 @@ This is your link for group %@!</source>
<target>Отвори конзолата</target> <target>Отвори конзолата</target>
<note>authentication reason</note> <note>authentication reason</note>
</trans-unit> </trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open user profiles" xml:space="preserve"> <trans-unit id="Open user profiles" xml:space="preserve">
<source>Open user profiles</source> <source>Open user profiles</source>
<target>Отвори потребителските профили</target> <target>Отвори потребителските профили</target>
@@ -3634,6 +3392,11 @@ This is your link for group %@!</source>
<target>Отваряне на база данни…</target> <target>Отваряне на база данни…</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
<source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source>
<target>Отварянето на линка в браузъра може да намали поверителността и сигурността на връзката. Несигурните SimpleX линкове ще бъдат червени.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve"> <trans-unit id="PING count" xml:space="preserve">
<source>PING count</source> <source>PING count</source>
<target>PING бройка</target> <target>PING бройка</target>
@@ -3679,10 +3442,6 @@ This is your link for group %@!</source>
<target>Постави</target> <target>Постави</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve"> <trans-unit id="Paste image" xml:space="preserve">
<source>Paste image</source> <source>Paste image</source>
<target>Постави изображение</target> <target>Постави изображение</target>
@@ -3828,14 +3587,6 @@ This is your link for group %@!</source>
<target>Профилно изображение</target> <target>Профилно изображение</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve"> <trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source> <source>Profile password</source>
<target>Профилна парола</target> <target>Профилна парола</target>
@@ -4081,14 +3832,6 @@ This is your link for group %@!</source>
<target>Предоговори криптирането?</target> <target>Предоговори криптирането?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Repeat connection request?" xml:space="preserve">
<source>Repeat connection request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Repeat join request?" xml:space="preserve">
<source>Repeat join request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reply" xml:space="preserve"> <trans-unit id="Reply" xml:space="preserve">
<source>Reply</source> <source>Reply</source>
<target>Отговори</target> <target>Отговори</target>
@@ -4274,10 +4017,6 @@ This is your link for group %@!</source>
<target>Сканирай QR код</target> <target>Сканирай QR код</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve"> <trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source> <source>Scan code</source>
<target>Сканирай код</target> <target>Сканирай код</target>
@@ -4360,7 +4099,6 @@ This is your link for group %@!</source>
</trans-unit> </trans-unit>
<trans-unit id="Send direct message to connect" xml:space="preserve"> <trans-unit id="Send direct message to connect" xml:space="preserve">
<source>Send direct message to connect</source> <source>Send direct message to connect</source>
<target>Изпрати лично съобщение за свързване</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Send disappearing message" xml:space="preserve"> <trans-unit id="Send disappearing message" xml:space="preserve">
@@ -4498,10 +4236,6 @@ This is your link for group %@!</source>
<target>Сървъри</target> <target>Сървъри</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve"> <trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source> <source>Set 1 day</source>
<target>Задай 1 ден</target> <target>Задай 1 ден</target>
@@ -4812,10 +4546,6 @@ This is your link for group %@!</source>
<target>Докосни бутона </target> <target>Докосни бутона </target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Tap to Connect" xml:space="preserve">
<source>Tap to Connect</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to activate profile." xml:space="preserve"> <trans-unit id="Tap to activate profile." xml:space="preserve">
<source>Tap to activate profile.</source> <source>Tap to activate profile.</source>
<target>Докосни за активиране на профил.</target> <target>Докосни за активиране на профил.</target>
@@ -4913,6 +4643,11 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване!</target> <target>Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="The group is fully decentralized it is visible only to the members." xml:space="preserve">
<source>The group is fully decentralized it is visible only to the members.</source>
<target>Групата е напълно децентрализирана – видима е само за членовете.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The hash of the previous message is different." xml:space="preserve"> <trans-unit id="The hash of the previous message is different." xml:space="preserve">
<source>The hash of the previous message is different.</source> <source>The hash of the previous message is different.</source>
<target>Хешът на предишното съобщение е различен.</target> <target>Хешът на предишното съобщение е различен.</target>
@@ -4998,10 +4733,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</target> <target>Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source> <source>This group has over %lld members, delivery receipts are not sent.</source>
<target>Тази група има над %lld членове, потвърждения за доставка не се изпращат.</target> <target>Тази група има над %lld членове, потвърждения за доставка не се изпращат.</target>
@@ -5012,14 +4743,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Тази група вече не съществува.</target> <target>Тази група вече не съществува.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This is your own SimpleX address!" xml:space="preserve">
<source>This is your own SimpleX address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This is your own one-time link!" xml:space="preserve">
<source>This is your own one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve">
<source>This setting applies to messages in your current chat profile **%@**.</source> <source>This setting applies to messages in your current chat profile **%@**.</source>
<target>Тази настройка се прилага за съобщения в текущия ви профил **%@**.</target> <target>Тази настройка се прилага за съобщения в текущия ви профил **%@**.</target>
@@ -5035,10 +4758,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>За да се свърже, вашият контакт може да сканира QR код или да използва линка в приложението.</target> <target>За да се свърже, вашият контакт може да сканира QR код или да използва линка в приложението.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve"> <trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source> <source>To make a new connection</source>
<target>За да направите нова връзка</target> <target>За да направите нова връзка</target>
@@ -5121,18 +4840,6 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>Не може да се запише гласово съобщение</target> <target>Не може да се запише гласово съобщение</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock" xml:space="preserve">
<source>Unblock</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
<source>Unblock member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
<source>Unblock member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unexpected error: %@" xml:space="preserve"> <trans-unit id="Unexpected error: %@" xml:space="preserve">
<source>Unexpected error: %@</source> <source>Unexpected error: %@</source>
<target>Неочаквана грешка: %@</target> <target>Неочаквана грешка: %@</target>
@@ -5195,14 +4902,6 @@ To connect, please ask your contact to create another connection link and check
За да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка.</target> За да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve"> <trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source> <source>Unlock</source>
<target>Отключи</target> <target>Отключи</target>
@@ -5293,10 +4992,6 @@ To connect, please ask your contact to create another connection link and check
<target>Използвай за нови връзки</target> <target>Използвай за нови връзки</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve"> <trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source> <source>Use iOS call interface</source>
<target>Използвай интерфейса за повикване на iOS</target> <target>Използвай интерфейса за повикване на iOS</target>
@@ -5327,23 +5022,11 @@ To connect, please ask your contact to create another connection link and check
<target>Използват се сървърите на SimpleX Chat.</target> <target>Използват се сървърите на SimpleX Chat.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve"> <trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source> <source>Verify connection security</source>
<target>Потвръди сигурността на връзката</target> <target>Потвръди сигурността на връзката</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve"> <trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source> <source>Verify security code</source>
<target>Потвръди кода за сигурност</target> <target>Потвръди кода за сигурност</target>
@@ -5354,10 +5037,6 @@ To connect, please ask your contact to create another connection link and check
<target>Чрез браузър</target> <target>Чрез браузър</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve"> <trans-unit id="Video call" xml:space="preserve">
<source>Video call</source> <source>Video call</source>
<target>Видео разговор</target> <target>Видео разговор</target>
@@ -5408,10 +5087,6 @@ To connect, please ask your contact to create another connection link and check
<target>Гласово съобщение…</target> <target>Гласово съобщение…</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve"> <trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source> <source>Waiting for file</source>
<target>Изчаква се получаването на файла</target> <target>Изчаква се получаването на файла</target>
@@ -5512,35 +5187,6 @@ To connect, please ask your contact to create another connection link and check
<target>Вече сте вече свързани с %@.</target> <target>Вече сте вече свързани с %@.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You are already connecting to %@." xml:space="preserve">
<source>You are already connecting to %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already connecting via this one-time link!" xml:space="preserve">
<source>You are already connecting via this one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already in group %@." xml:space="preserve">
<source>You are already in group %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group %@." xml:space="preserve">
<source>You are already joining the group %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group via this link!" xml:space="preserve">
<source>You are already joining the group via this link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group via this link." xml:space="preserve">
<source>You are already joining the group via this link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group!&#10;Repeat join request?" xml:space="preserve">
<source>You are already joining the group!
Repeat join request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
<source>You are connected to the server used to receive messages from this contact.</source> <source>You are connected to the server used to receive messages from this contact.</source>
<target>Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт.</target> <target>Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт.</target>
@@ -5636,15 +5282,6 @@ Repeat join request?</source>
<target>Не можахте да бъдете потвърдени; Моля, опитайте отново.</target> <target>Не можахте да бъдете потвърдени; Моля, опитайте отново.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You have already requested connection via this address!" xml:space="preserve">
<source>You have already requested connection via this address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You have already requested connection!&#10;Repeat connection request?" xml:space="preserve">
<source>You have already requested connection!
Repeat connection request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You have no chats" xml:space="preserve"> <trans-unit id="You have no chats" xml:space="preserve">
<source>You have no chats</source> <source>You have no chats</source>
<target>Нямате чатове</target> <target>Нямате чатове</target>
@@ -5695,10 +5332,6 @@ Repeat connection request?</source>
<target>Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно!</target> <target>Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve">
<source>You will be connected when group link host's device is online, please wait or check later!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
<source>You will be connected when your connection request is accepted, please wait or check later!</source> <source>You will be connected when your connection request is accepted, please wait or check later!</source>
<target>Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно!</target> <target>Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно!</target>
@@ -5714,8 +5347,9 @@ Repeat connection request?</source>
<target>Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим.</target> <target>Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will connect to all group members." xml:space="preserve"> <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
<source>You will connect to all group members.</source> <source>You will join a group this link refers to and connect to its group members.</source>
<target>Ще се присъедините към групата, към която този линк препраща, и ще се свържете с нейните членове.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve">
@@ -5783,6 +5417,11 @@ Repeat connection request?</source>
<target>Вашата чат база данни не е криптирана - задайте парола, за да я криптирате.</target> <target>Вашата чат база данни не е криптирана - задайте парола, за да я криптирате.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Your chat profile will be sent to group members" xml:space="preserve">
<source>Your chat profile will be sent to group members</source>
<target>Вашият чат профил ще бъде изпратен на членовете на групата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve"> <trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source> <source>Your chat profiles</source>
<target>Вашите чат профили</target> <target>Вашите чат профили</target>
@@ -5837,10 +5476,6 @@ You can change it in Settings.</source>
<target>Вашата поверителност</target> <target>Вашата поверителност</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Your profile" xml:space="preserve">
<source>Your profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve">
<source>Your profile **%@** will be shared.</source> <source>Your profile **%@** will be shared.</source>
<target>Вашият профил **%@** ще бъде споделен.</target> <target>Вашият профил **%@** ще бъде споделен.</target>
@@ -5933,19 +5568,11 @@ SimpleX сървърите не могат да видят вашия профи
<target>винаги</target> <target>винаги</target>
<note>pref value</note> <note>pref value</note>
</trans-unit> </trans-unit>
<trans-unit id="and %lld other events" xml:space="preserve">
<source>and %lld other events</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve">
<source>audio call (not e2e encrypted)</source> <source>audio call (not e2e encrypted)</source>
<target>аудио разговор (не е e2e криптиран)</target> <target>аудио разговор (не е e2e криптиран)</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve"> <trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source> <source>bad message ID</source>
<target>лошо ID на съобщението</target> <target>лошо ID на съобщението</target>
@@ -5956,10 +5583,6 @@ SimpleX сървърите не могат да видят вашия профи
<target>лош хеш на съобщението</target> <target>лош хеш на съобщението</target>
<note>integrity error chat item</note> <note>integrity error chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked" xml:space="preserve">
<source>blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
<source>bold</source> <source>bold</source>
<target>удебелен</target> <target>удебелен</target>
@@ -6032,7 +5655,6 @@ SimpleX сървърите не могат да видят вашия профи
</trans-unit> </trans-unit>
<trans-unit id="connected directly" xml:space="preserve"> <trans-unit id="connected directly" xml:space="preserve">
<source>connected directly</source> <source>connected directly</source>
<target>свързан директно</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="connecting" xml:space="preserve"> <trans-unit id="connecting" xml:space="preserve">
@@ -6130,10 +5752,6 @@ SimpleX сървърите не могат да видят вашия профи
<target>изтрит</target> <target>изтрит</target>
<note>deleted chat item</note> <note>deleted chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="deleted contact" xml:space="preserve">
<source>deleted contact</source>
<note>rcv direct event chat item</note>
</trans-unit>
<trans-unit id="deleted group" xml:space="preserve"> <trans-unit id="deleted group" xml:space="preserve">
<source>deleted group</source> <source>deleted group</source>
<target>групата изтрита</target> <target>групата изтрита</target>
@@ -6418,8 +6036,7 @@ SimpleX сървърите не могат да видят вашия профи
<source>off</source> <source>off</source>
<target>изключено</target> <target>изключено</target>
<note>enabled status <note>enabled status
group pref value group pref value</note>
time to disappear</note>
</trans-unit> </trans-unit>
<trans-unit id="offered %@" xml:space="preserve"> <trans-unit id="offered %@" xml:space="preserve">
<source>offered %@</source> <source>offered %@</source>
@@ -6436,6 +6053,11 @@ SimpleX сървърите не могат да видят вашия профи
<target>включено</target> <target>включено</target>
<note>group pref value</note> <note>group pref value</note>
</trans-unit> </trans-unit>
<trans-unit id="or chat with the developers" xml:space="preserve">
<source>or chat with the developers</source>
<target>или пишете на разработчиците</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="owner" xml:space="preserve"> <trans-unit id="owner" xml:space="preserve">
<source>owner</source> <source>owner</source>
<target>собственик</target> <target>собственик</target>
@@ -6498,7 +6120,6 @@ SimpleX сървърите не могат да видят вашия профи
</trans-unit> </trans-unit>
<trans-unit id="send direct message" xml:space="preserve"> <trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source> <source>send direct message</source>
<target>изпрати лично съобщение</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="starting…" xml:space="preserve"> <trans-unit id="starting…" xml:space="preserve">
@@ -6526,10 +6147,6 @@ SimpleX сървърите не могат да видят вашия профи
<target>актуализиран профил на групата</target> <target>актуализиран профил на групата</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve"> <trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source> <source>v%@ (%@)</source>
<target>v%@ (%@)</target> <target>v%@ (%@)</target>
@@ -6667,10 +6284,6 @@ SimpleX сървърите не могат да видят вашия профи
<target>SimpleX използва Face ID за локалнa идентификация</target> <target>SimpleX използва Face ID за локалнa идентификация</target>
<note>Privacy - Face ID Usage Description</note> <note>Privacy - Face ID Usage Description</note>
</trans-unit> </trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX се нуждае от достъп до микрофона за аудио и видео разговори и за запис на гласови съобщения.</target> <target>SimpleX се нуждае от достъп до микрофона за аудио и видео разговори и за запис на гласови съобщения.</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
@@ -87,10 +87,6 @@
<target>%@ / % @</target> <target>%@ / % @</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve">
<source>%@ and %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve"> <trans-unit id="%@ and %@ connected" xml:space="preserve">
<source>%@ and %@ connected</source> <source>%@ and %@ connected</source>
<target>%@ ja %@ yhdistetty</target> <target>%@ ja %@ yhdistetty</target>
@@ -101,10 +97,6 @@
<target>%1$@ klo %2$@:</target> <target>%1$@ klo %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note> <note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ connected" xml:space="preserve">
<source>%@ connected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve"> <trans-unit id="%@ is connected!" xml:space="preserve">
<source>%@ is connected!</source> <source>%@ is connected!</source>
<target>%@ on yhdistetty!</target> <target>%@ on yhdistetty!</target>
@@ -130,10 +122,6 @@
<target>%@ haluaa muodostaa yhteyden!</target> <target>%@ haluaa muodostaa yhteyden!</target>
<note>notification title</note> <note>notification title</note>
</trans-unit> </trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve">
<source>%@, %@ and %lld other members connected</source> <source>%@, %@ and %lld other members connected</source>
<target>%@, %@ ja %lld muut jäsenet yhdistetty</target> <target>%@, %@ ja %lld muut jäsenet yhdistetty</target>
@@ -199,27 +187,11 @@
<target>%lld tiedosto(a), joiden kokonaiskoko on %@</target> <target>%lld tiedosto(a), joiden kokonaiskoko on %@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld group events" xml:space="preserve">
<source>%lld group events</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld members" xml:space="preserve"> <trans-unit id="%lld members" xml:space="preserve">
<source>%lld members</source> <source>%lld members</source>
<target>%lld jäsenet</target> <target>%lld jäsenet</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked" xml:space="preserve">
<source>%lld messages blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages moderated by %@" xml:space="preserve">
<source>%lld messages moderated by %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld minutes" xml:space="preserve"> <trans-unit id="%lld minutes" xml:space="preserve">
<source>%lld minutes</source> <source>%lld minutes</source>
<target>%lld minuuttia</target> <target>%lld minuuttia</target>
@@ -227,7 +199,6 @@
</trans-unit> </trans-unit>
<trans-unit id="%lld new interface languages" xml:space="preserve"> <trans-unit id="%lld new interface languages" xml:space="preserve">
<source>%lld new interface languages</source> <source>%lld new interface languages</source>
<target>%lld uutta käyttöliittymän kieltä</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld second(s)" xml:space="preserve"> <trans-unit id="%lld second(s)" xml:space="preserve">
@@ -290,14 +261,6 @@
<target>(</target> <target>(</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve"> <trans-unit id=")" xml:space="preserve">
<source>)</source> <source>)</source>
<target>)</target> <target>)</target>
@@ -383,12 +346,6 @@
- ja paljon muuta!</target> - ja paljon muuta!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve"> <trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes. <source>- voice messages up to 5 minutes.
- custom time to disappear. - custom time to disappear.
@@ -403,10 +360,6 @@
<target>.</target> <target>.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="0 sec" xml:space="preserve">
<source>0 sec</source>
<note>time to disappear</note>
</trans-unit>
<trans-unit id="0s" xml:space="preserve"> <trans-unit id="0s" xml:space="preserve">
<source>0s</source> <source>0s</source>
<target>0s</target> <target>0s</target>
@@ -632,10 +585,6 @@
<target>Kaikki viestit poistetaan - tätä ei voi kumota! Viestit poistuvat VAIN sinulta.</target> <target>Kaikki viestit poistetaan - tätä ei voi kumota! Viestit poistuvat VAIN sinulta.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve">
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve"> <trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source> <source>All your contacts will remain connected.</source>
<target>Kaikki kontaktisi pysyvät yhteydessä.</target> <target>Kaikki kontaktisi pysyvät yhteydessä.</target>
@@ -741,14 +690,6 @@
<target>Oletko jo muodostanut yhteyden?</target> <target>Oletko jo muodostanut yhteyden?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Already connecting!" xml:space="preserve">
<source>Already connecting!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Already joining the group!" xml:space="preserve">
<source>Already joining the group!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Always use relay" xml:space="preserve"> <trans-unit id="Always use relay" xml:space="preserve">
<source>Always use relay</source> <source>Always use relay</source>
<target>Käytä aina relettä</target> <target>Käytä aina relettä</target>
@@ -868,10 +809,6 @@
<target>Takaisin</target> <target>Takaisin</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve"> <trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source> <source>Bad message ID</source>
<target>Virheellinen viestin tunniste</target> <target>Virheellinen viestin tunniste</target>
@@ -882,31 +819,11 @@
<target>Virheellinen viestin tarkiste</target> <target>Virheellinen viestin tarkiste</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source> <source>Better messages</source>
<target>Parempia viestejä</target> <target>Parempia viestejä</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block" xml:space="preserve">
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
<source>Block member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
<source>Both you and your contact can add message reactions.</source> <source>Both you and your contact can add message reactions.</source>
<target>Sekä sinä että kontaktisi voivat käyttää viestireaktioita.</target> <target>Sekä sinä että kontaktisi voivat käyttää viestireaktioita.</target>
@@ -1167,8 +1084,9 @@
<target>Yhdistä</target> <target>Yhdistä</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve"> <trans-unit id="Connect directly" xml:space="preserve">
<source>Connect automatically</source> <source>Connect directly</source>
<target>Yhdistä suoraan</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve"> <trans-unit id="Connect incognito" xml:space="preserve">
@@ -1176,26 +1094,14 @@
<target>Yhdistä Incognito</target> <target>Yhdistä Incognito</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve"> <trans-unit id="Connect via contact link" xml:space="preserve">
<source>Connect to desktop</source> <source>Connect via contact link</source>
<target>Yhdistä kontaktilinkillä</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve"> <trans-unit id="Connect via group link?" xml:space="preserve">
<source>Connect to yourself?</source> <source>Connect via group link?</source>
<note>No comment provided by engineer.</note> <target>Yhdistetäänkö ryhmälinkin kautta?</target>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own SimpleX address!" xml:space="preserve">
<source>Connect to yourself?
This is your own SimpleX address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own one-time link!" xml:space="preserve">
<source>Connect to yourself?
This is your own one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact address" xml:space="preserve">
<source>Connect via contact address</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect via link" xml:space="preserve"> <trans-unit id="Connect via link" xml:space="preserve">
@@ -1213,18 +1119,6 @@ This is your own one-time link!</source>
<target>Yhdistä kertalinkillä</target> <target>Yhdistä kertalinkillä</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect with %@" xml:space="preserve">
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve"> <trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source> <source>Connecting to server…</source>
<target>Yhteyden muodostaminen palvelimeen…</target> <target>Yhteyden muodostaminen palvelimeen…</target>
@@ -1235,10 +1129,6 @@ This is your own one-time link!</source>
<target>Yhteyden muodostaminen palvelimeen... (virhe: %@)</target> <target>Yhteyden muodostaminen palvelimeen... (virhe: %@)</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve"> <trans-unit id="Connection" xml:space="preserve">
<source>Connection</source> <source>Connection</source>
<target>Yhteys</target> <target>Yhteys</target>
@@ -1259,10 +1149,6 @@ This is your own one-time link!</source>
<target>Yhteyspyyntö lähetetty!</target> <target>Yhteyspyyntö lähetetty!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve"> <trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source> <source>Connection timeout</source>
<target>Yhteyden aikakatkaisu</target> <target>Yhteyden aikakatkaisu</target>
@@ -1278,6 +1164,11 @@ This is your own one-time link!</source>
<target>Kontakti on jo olemassa</target> <target>Kontakti on jo olemassa</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Contact and all messages will be deleted - this cannot be undone!</source>
<target>Kontakti ja kaikki viestit poistetaan - tätä ei voi perua!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact hidden:" xml:space="preserve"> <trans-unit id="Contact hidden:" xml:space="preserve">
<source>Contact hidden:</source> <source>Contact hidden:</source>
<target>Kontakti piilotettu:</target> <target>Kontakti piilotettu:</target>
@@ -1328,10 +1219,6 @@ This is your own one-time link!</source>
<target>Ydinversio: v%@</target> <target>Ydinversio: v%@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve"> <trans-unit id="Create" xml:space="preserve">
<source>Create</source> <source>Create</source>
<target>Luo</target> <target>Luo</target>
@@ -1342,10 +1229,6 @@ This is your own one-time link!</source>
<target>Luo SimpleX-osoite</target> <target>Luo SimpleX-osoite</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve"> <trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source> <source>Create an address to let people connect with you.</source>
<target>Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä.</target> <target>Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä.</target>
@@ -1356,10 +1239,6 @@ This is your own one-time link!</source>
<target>Luo tiedosto</target> <target>Luo tiedosto</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Create group" xml:space="preserve">
<source>Create group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create group link" xml:space="preserve"> <trans-unit id="Create group link" xml:space="preserve">
<source>Create group link</source> <source>Create group link</source>
<target>Luo ryhmälinkki</target> <target>Luo ryhmälinkki</target>
@@ -1372,7 +1251,6 @@ This is your own one-time link!</source>
</trans-unit> </trans-unit>
<trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve">
<source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source>
<target>Luo uusi profiili [työpöytäsovelluksessa](https://simplex.chat/downloads/). 💻</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve"> <trans-unit id="Create one-time invitation link" xml:space="preserve">
@@ -1380,10 +1258,6 @@ This is your own one-time link!</source>
<target>Luo kertakutsulinkki</target> <target>Luo kertakutsulinkki</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create queue" xml:space="preserve"> <trans-unit id="Create queue" xml:space="preserve">
<source>Create queue</source> <source>Create queue</source>
<target>Luo jono</target> <target>Luo jono</target>
@@ -1542,10 +1416,6 @@ This is your own one-time link!</source>
<target>Poista</target> <target>Poista</target>
<note>chat item action</note> <note>chat item action</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete %lld messages?" xml:space="preserve">
<source>Delete %lld messages?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete Contact" xml:space="preserve"> <trans-unit id="Delete Contact" xml:space="preserve">
<source>Delete Contact</source> <source>Delete Contact</source>
<target>Poista kontakti</target> <target>Poista kontakti</target>
@@ -1571,10 +1441,6 @@ This is your own one-time link!</source>
<target>Poista kaikki tiedostot</target> <target>Poista kaikki tiedostot</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete and notify contact" xml:space="preserve">
<source>Delete and notify contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete archive" xml:space="preserve"> <trans-unit id="Delete archive" xml:space="preserve">
<source>Delete archive</source> <source>Delete archive</source>
<target>Poista arkisto</target> <target>Poista arkisto</target>
@@ -1605,9 +1471,9 @@ This is your own one-time link!</source>
<target>Poista kontakti</target> <target>Poista kontakti</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete contact?&#10;This cannot be undone!" xml:space="preserve"> <trans-unit id="Delete contact?" xml:space="preserve">
<source>Delete contact? <source>Delete contact?</source>
This cannot be undone!</source> <target>Poista kontakti?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete database" xml:space="preserve"> <trans-unit id="Delete database" xml:space="preserve">
@@ -1750,18 +1616,6 @@ This cannot be undone!</source>
<target>Kuvaus</target> <target>Kuvaus</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve"> <trans-unit id="Develop" xml:space="preserve">
<source>Develop</source> <source>Develop</source>
<target>Kehitä</target> <target>Kehitä</target>
@@ -1852,17 +1706,18 @@ This cannot be undone!</source>
<target>Katkaise</target> <target>Katkaise</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve"> <trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source> <source>Discover and join groups</source>
<target>Löydä ryhmiä ja liity niihin</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve"> <trans-unit id="Display name" xml:space="preserve">
<source>Discover via local network</source> <source>Display name</source>
<target>Näyttönimi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Display name:" xml:space="preserve">
<source>Display name:</source>
<target>Näyttönimi:</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
@@ -2034,14 +1889,6 @@ This cannot be undone!</source>
<target>Salattu viesti: odottamaton virhe</target> <target>Salattu viesti: odottamaton virhe</target>
<note>notification</note> <note>notification</note>
</trans-unit> </trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve"> <trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source> <source>Enter Passcode</source>
<target>Syötä pääsykoodi</target> <target>Syötä pääsykoodi</target>
@@ -2052,10 +1899,6 @@ This cannot be undone!</source>
<target>Anna oikea tunnuslause.</target> <target>Anna oikea tunnuslause.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter group name…" xml:space="preserve">
<source>Enter group name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter passphrase…" xml:space="preserve"> <trans-unit id="Enter passphrase…" xml:space="preserve">
<source>Enter passphrase…</source> <source>Enter passphrase…</source>
<target>Syötä tunnuslause…</target> <target>Syötä tunnuslause…</target>
@@ -2071,10 +1914,6 @@ This cannot be undone!</source>
<target>Syötä palvelin manuaalisesti</target> <target>Syötä palvelin manuaalisesti</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve"> <trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source> <source>Enter welcome message…</source>
<target>Kirjoita tervetuloviesti…</target> <target>Kirjoita tervetuloviesti…</target>
@@ -2085,10 +1924,6 @@ This cannot be undone!</source>
<target>Kirjoita tervetuloviesti... (valinnainen)</target> <target>Kirjoita tervetuloviesti... (valinnainen)</target>
<note>placeholder</note> <note>placeholder</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter your name…" xml:space="preserve">
<source>Enter your name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error" xml:space="preserve"> <trans-unit id="Error" xml:space="preserve">
<source>Error</source> <source>Error</source>
<target>Virhe</target> <target>Virhe</target>
@@ -2362,10 +2197,6 @@ This cannot be undone!</source>
<target>Poistu tallentamatta</target> <target>Poistu tallentamatta</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Expand" xml:space="preserve">
<source>Expand</source>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Export database" xml:space="preserve"> <trans-unit id="Export database" xml:space="preserve">
<source>Export database</source> <source>Export database</source>
<target>Vie tietokanta</target> <target>Vie tietokanta</target>
@@ -2396,10 +2227,6 @@ This cannot be undone!</source>
<target>Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!</target> <target>Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve"> <trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source> <source>Favorite</source>
<target>Suosikki</target> <target>Suosikki</target>
@@ -2495,10 +2322,6 @@ This cannot be undone!</source>
<target>Konsoliin</target> <target>Konsoliin</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve"> <trans-unit id="French interface" xml:space="preserve">
<source>French interface</source> <source>French interface</source>
<target>Ranskalainen käyttöliittymä</target> <target>Ranskalainen käyttöliittymä</target>
@@ -2519,10 +2342,6 @@ This cannot be undone!</source>
<target>Koko nimi:</target> <target>Koko nimi:</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve">
<source>Fully re-implemented - work in background!</source> <source>Fully re-implemented - work in background!</source>
<target>Täysin uudistettu - toimii taustalla!</target> <target>Täysin uudistettu - toimii taustalla!</target>
@@ -2543,14 +2362,6 @@ This cannot be undone!</source>
<target>Ryhmä</target> <target>Ryhmä</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Group already exists" xml:space="preserve">
<source>Group already exists</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group already exists!" xml:space="preserve">
<source>Group already exists!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group display name" xml:space="preserve"> <trans-unit id="Group display name" xml:space="preserve">
<source>Group display name</source> <source>Group display name</source>
<target>Ryhmän näyttönimi</target> <target>Ryhmän näyttönimi</target>
@@ -2821,10 +2632,6 @@ This cannot be undone!</source>
<target>Incognito</target> <target>Incognito</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve"> <trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source> <source>Incognito mode</source>
<target>Incognito-tila</target> <target>Incognito-tila</target>
@@ -2855,10 +2662,6 @@ This cannot be undone!</source>
<target>Yhteensopimaton tietokantaversio</target> <target>Yhteensopimaton tietokantaversio</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve"> <trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source> <source>Incorrect passcode</source>
<target>Väärä pääsykoodi</target> <target>Väärä pääsykoodi</target>
@@ -2906,10 +2709,6 @@ This cannot be undone!</source>
<target>Virheellinen yhteyslinkki</target> <target>Virheellinen yhteyslinkki</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve"> <trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source> <source>Invalid server address!</source>
<target>Virheellinen palvelinosoite!</target> <target>Virheellinen palvelinosoite!</target>
@@ -3001,33 +2800,16 @@ This cannot be undone!</source>
<target>Liity ryhmään</target> <target>Liity ryhmään</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
<source>Join group?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join incognito" xml:space="preserve"> <trans-unit id="Join incognito" xml:space="preserve">
<source>Join incognito</source> <source>Join incognito</source>
<target>Liity incognito-tilassa</target> <target>Liity incognito-tilassa</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join with current profile" xml:space="preserve">
<source>Join with current profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join your group?&#10;This is your link for group %@!" xml:space="preserve">
<source>Join your group?
This is your link for group %@!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Joining group" xml:space="preserve"> <trans-unit id="Joining group" xml:space="preserve">
<source>Joining group</source> <source>Joining group</source>
<target>Liittyy ryhmään</target> <target>Liittyy ryhmään</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve"> <trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source> <source>Keep your connections</source>
<target>Pidä kontaktisi</target> <target>Pidä kontaktisi</target>
@@ -3088,18 +2870,6 @@ This is your link for group %@!</source>
<target>Rajoitukset</target> <target>Rajoitukset</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve"> <trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source> <source>Live message!</source>
<target>Live-viesti!</target> <target>Live-viesti!</target>
@@ -3250,10 +3020,6 @@ This is your link for group %@!</source>
<target>Viestit ja tiedostot</target> <target>Viestit ja tiedostot</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Messages from %@ will be shown!" xml:space="preserve">
<source>Messages from %@ will be shown!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migrating database archive…" xml:space="preserve"> <trans-unit id="Migrating database archive…" xml:space="preserve">
<source>Migrating database archive…</source> <source>Migrating database archive…</source>
<target>Siirretään tietokannan arkistoa…</target> <target>Siirretään tietokannan arkistoa…</target>
@@ -3448,10 +3214,6 @@ This is your link for group %@!</source>
<target>Ei vastaanotettuja tai lähetettyjä tiedostoja</target> <target>Ei vastaanotettuja tai lähetettyjä tiedostoja</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve"> <trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source> <source>Notifications</source>
<target>Ilmoitukset</target> <target>Ilmoitukset</target>
@@ -3605,10 +3367,6 @@ This is your link for group %@!</source>
<target>Avaa keskustelukonsoli</target> <target>Avaa keskustelukonsoli</target>
<note>authentication reason</note> <note>authentication reason</note>
</trans-unit> </trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open user profiles" xml:space="preserve"> <trans-unit id="Open user profiles" xml:space="preserve">
<source>Open user profiles</source> <source>Open user profiles</source>
<target>Avaa käyttäjäprofiilit</target> <target>Avaa käyttäjäprofiilit</target>
@@ -3624,6 +3382,11 @@ This is your link for group %@!</source>
<target>Avataan tietokantaa…</target> <target>Avataan tietokantaa…</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
<source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source>
<target>Linkin avaaminen selaimessa voi heikentää yhteyden yksityisyyttä ja turvallisuutta. Epäluotetut SimpleX-linkit näkyvät punaisina.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve"> <trans-unit id="PING count" xml:space="preserve">
<source>PING count</source> <source>PING count</source>
<target>PING-määrä</target> <target>PING-määrä</target>
@@ -3669,10 +3432,6 @@ This is your link for group %@!</source>
<target>Liitä</target> <target>Liitä</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve"> <trans-unit id="Paste image" xml:space="preserve">
<source>Paste image</source> <source>Paste image</source>
<target>Liitä kuva</target> <target>Liitä kuva</target>
@@ -3818,14 +3577,6 @@ This is your link for group %@!</source>
<target>Profiilikuva</target> <target>Profiilikuva</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve"> <trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source> <source>Profile password</source>
<target>Profiilin salasana</target> <target>Profiilin salasana</target>
@@ -4071,14 +3822,6 @@ This is your link for group %@!</source>
<target>Uudelleenneuvottele salaus?</target> <target>Uudelleenneuvottele salaus?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Repeat connection request?" xml:space="preserve">
<source>Repeat connection request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Repeat join request?" xml:space="preserve">
<source>Repeat join request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reply" xml:space="preserve"> <trans-unit id="Reply" xml:space="preserve">
<source>Reply</source> <source>Reply</source>
<target>Vastaa</target> <target>Vastaa</target>
@@ -4264,10 +4007,6 @@ This is your link for group %@!</source>
<target>Skannaa QR-koodi</target> <target>Skannaa QR-koodi</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve"> <trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source> <source>Scan code</source>
<target>Skannaa koodi</target> <target>Skannaa koodi</target>
@@ -4487,10 +4226,6 @@ This is your link for group %@!</source>
<target>Palvelimet</target> <target>Palvelimet</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve"> <trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source> <source>Set 1 day</source>
<target>Aseta 1 päivä</target> <target>Aseta 1 päivä</target>
@@ -4800,10 +4535,6 @@ This is your link for group %@!</source>
<target>Napauta painiketta </target> <target>Napauta painiketta </target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Tap to Connect" xml:space="preserve">
<source>Tap to Connect</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to activate profile." xml:space="preserve"> <trans-unit id="Tap to activate profile." xml:space="preserve">
<source>Tap to activate profile.</source> <source>Tap to activate profile.</source>
<target>Aktivoi profiili napauttamalla.</target> <target>Aktivoi profiili napauttamalla.</target>
@@ -4901,6 +4632,11 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!</target> <target>Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="The group is fully decentralized it is visible only to the members." xml:space="preserve">
<source>The group is fully decentralized it is visible only to the members.</source>
<target>Ryhmä on täysin hajautettu - se näkyy vain jäsenille.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The hash of the previous message is different." xml:space="preserve"> <trans-unit id="The hash of the previous message is different." xml:space="preserve">
<source>The hash of the previous message is different.</source> <source>The hash of the previous message is different.</source>
<target>Edellisen viestin tarkiste on erilainen.</target> <target>Edellisen viestin tarkiste on erilainen.</target>
@@ -4986,10 +4722,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti.</target> <target>Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source> <source>This group has over %lld members, delivery receipts are not sent.</source>
<target>Tässä ryhmässä on yli %lld jäsentä, lähetyskuittauksia ei lähetetä.</target> <target>Tässä ryhmässä on yli %lld jäsentä, lähetyskuittauksia ei lähetetä.</target>
@@ -5000,14 +4732,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Tätä ryhmää ei enää ole olemassa.</target> <target>Tätä ryhmää ei enää ole olemassa.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This is your own SimpleX address!" xml:space="preserve">
<source>This is your own SimpleX address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This is your own one-time link!" xml:space="preserve">
<source>This is your own one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve">
<source>This setting applies to messages in your current chat profile **%@**.</source> <source>This setting applies to messages in your current chat profile **%@**.</source>
<target>Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**.</target> <target>Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**.</target>
@@ -5023,10 +4747,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Kontaktisi voi muodostaa yhteyden skannaamalla QR-koodin tai käyttämällä sovelluksessa olevaa linkkiä.</target> <target>Kontaktisi voi muodostaa yhteyden skannaamalla QR-koodin tai käyttämällä sovelluksessa olevaa linkkiä.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve"> <trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source> <source>To make a new connection</source>
<target>Uuden yhteyden luominen</target> <target>Uuden yhteyden luominen</target>
@@ -5108,18 +4828,6 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote
<target>Ääniviestiä ei voi tallentaa</target> <target>Ääniviestiä ei voi tallentaa</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock" xml:space="preserve">
<source>Unblock</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
<source>Unblock member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
<source>Unblock member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unexpected error: %@" xml:space="preserve"> <trans-unit id="Unexpected error: %@" xml:space="preserve">
<source>Unexpected error: %@</source> <source>Unexpected error: %@</source>
<target>Odottamaton virhe: %@</target> <target>Odottamaton virhe: %@</target>
@@ -5182,14 +4890,6 @@ To connect, please ask your contact to create another connection link and check
Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa.</target> Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve"> <trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source> <source>Unlock</source>
<target>Avaa</target> <target>Avaa</target>
@@ -5280,10 +4980,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Käytä uusiin yhteyksiin</target> <target>Käytä uusiin yhteyksiin</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve"> <trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source> <source>Use iOS call interface</source>
<target>Käytä iOS:n puhelujen käyttöliittymää</target> <target>Käytä iOS:n puhelujen käyttöliittymää</target>
@@ -5314,23 +5010,11 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Käyttää SimpleX Chat -palvelimia.</target> <target>Käyttää SimpleX Chat -palvelimia.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve"> <trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source> <source>Verify connection security</source>
<target>Tarkista yhteyden suojaus</target> <target>Tarkista yhteyden suojaus</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve"> <trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source> <source>Verify security code</source>
<target>Tarkista turvakoodi</target> <target>Tarkista turvakoodi</target>
@@ -5341,10 +5025,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Selaimella</target> <target>Selaimella</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve"> <trans-unit id="Video call" xml:space="preserve">
<source>Video call</source> <source>Video call</source>
<target>Videopuhelu</target> <target>Videopuhelu</target>
@@ -5395,10 +5075,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Ääniviesti…</target> <target>Ääniviesti…</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve"> <trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source> <source>Waiting for file</source>
<target>Odottaa tiedostoa</target> <target>Odottaa tiedostoa</target>
@@ -5499,35 +5175,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Olet jo muodostanut yhteyden %@:n kanssa.</target> <target>Olet jo muodostanut yhteyden %@:n kanssa.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You are already connecting to %@." xml:space="preserve">
<source>You are already connecting to %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already connecting via this one-time link!" xml:space="preserve">
<source>You are already connecting via this one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already in group %@." xml:space="preserve">
<source>You are already in group %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group %@." xml:space="preserve">
<source>You are already joining the group %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group via this link!" xml:space="preserve">
<source>You are already joining the group via this link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group via this link." xml:space="preserve">
<source>You are already joining the group via this link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group!&#10;Repeat join request?" xml:space="preserve">
<source>You are already joining the group!
Repeat join request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
<source>You are connected to the server used to receive messages from this contact.</source> <source>You are connected to the server used to receive messages from this contact.</source>
<target>Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta.</target> <target>Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta.</target>
@@ -5623,15 +5270,6 @@ Repeat join request?</source>
<target>Sinua ei voitu todentaa; yritä uudelleen.</target> <target>Sinua ei voitu todentaa; yritä uudelleen.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You have already requested connection via this address!" xml:space="preserve">
<source>You have already requested connection via this address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You have already requested connection!&#10;Repeat connection request?" xml:space="preserve">
<source>You have already requested connection!
Repeat connection request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You have no chats" xml:space="preserve"> <trans-unit id="You have no chats" xml:space="preserve">
<source>You have no chats</source> <source>You have no chats</source>
<target>Sinulla ei ole keskusteluja</target> <target>Sinulla ei ole keskusteluja</target>
@@ -5682,10 +5320,6 @@ Repeat connection request?</source>
<target>Sinut yhdistetään ryhmään, kun ryhmän isännän laite on online-tilassa, odota tai tarkista myöhemmin!</target> <target>Sinut yhdistetään ryhmään, kun ryhmän isännän laite on online-tilassa, odota tai tarkista myöhemmin!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve">
<source>You will be connected when group link host's device is online, please wait or check later!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
<source>You will be connected when your connection request is accepted, please wait or check later!</source> <source>You will be connected when your connection request is accepted, please wait or check later!</source>
<target>Sinut yhdistetään, kun yhteyspyyntösi on hyväksytty, odota tai tarkista myöhemmin!</target> <target>Sinut yhdistetään, kun yhteyspyyntösi on hyväksytty, odota tai tarkista myöhemmin!</target>
@@ -5701,8 +5335,9 @@ Repeat connection request?</source>
<target>Sinun on tunnistauduttava, kun käynnistät sovelluksen tai jatkat sen käyttöä 30 sekunnin tauon jälkeen.</target> <target>Sinun on tunnistauduttava, kun käynnistät sovelluksen tai jatkat sen käyttöä 30 sekunnin tauon jälkeen.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will connect to all group members." xml:space="preserve"> <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
<source>You will connect to all group members.</source> <source>You will join a group this link refers to and connect to its group members.</source>
<target>Liityt ryhmään, johon tämä linkki viittaa, ja muodostat yhteyden sen ryhmän jäseniin.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve">
@@ -5770,6 +5405,11 @@ Repeat connection request?</source>
<target>Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi.</target> <target>Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Your chat profile will be sent to group members" xml:space="preserve">
<source>Your chat profile will be sent to group members</source>
<target>Keskusteluprofiilisi lähetetään ryhmän jäsenille</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve"> <trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source> <source>Your chat profiles</source>
<target>Keskusteluprofiilisi</target> <target>Keskusteluprofiilisi</target>
@@ -5824,10 +5464,6 @@ Voit muuttaa sitä Asetuksista.</target>
<target>Yksityisyytesi</target> <target>Yksityisyytesi</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Your profile" xml:space="preserve">
<source>Your profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve">
<source>Your profile **%@** will be shared.</source> <source>Your profile **%@** will be shared.</source>
<target>Profiilisi **%@** jaetaan.</target> <target>Profiilisi **%@** jaetaan.</target>
@@ -5920,19 +5556,11 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>aina</target> <target>aina</target>
<note>pref value</note> <note>pref value</note>
</trans-unit> </trans-unit>
<trans-unit id="and %lld other events" xml:space="preserve">
<source>and %lld other events</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve">
<source>audio call (not e2e encrypted)</source> <source>audio call (not e2e encrypted)</source>
<target>äänipuhelu (ei e2e-salattu)</target> <target>äänipuhelu (ei e2e-salattu)</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve"> <trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source> <source>bad message ID</source>
<target>virheellinen viestin tunniste</target> <target>virheellinen viestin tunniste</target>
@@ -5943,10 +5571,6 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>virheellinen viestin tarkiste</target> <target>virheellinen viestin tarkiste</target>
<note>integrity error chat item</note> <note>integrity error chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked" xml:space="preserve">
<source>blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
<source>bold</source> <source>bold</source>
<target>lihavoitu</target> <target>lihavoitu</target>
@@ -6116,10 +5740,6 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>poistettu</target> <target>poistettu</target>
<note>deleted chat item</note> <note>deleted chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="deleted contact" xml:space="preserve">
<source>deleted contact</source>
<note>rcv direct event chat item</note>
</trans-unit>
<trans-unit id="deleted group" xml:space="preserve"> <trans-unit id="deleted group" xml:space="preserve">
<source>deleted group</source> <source>deleted group</source>
<target>poistettu ryhmä</target> <target>poistettu ryhmä</target>
@@ -6404,8 +6024,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<source>off</source> <source>off</source>
<target>pois</target> <target>pois</target>
<note>enabled status <note>enabled status
group pref value group pref value</note>
time to disappear</note>
</trans-unit> </trans-unit>
<trans-unit id="offered %@" xml:space="preserve"> <trans-unit id="offered %@" xml:space="preserve">
<source>offered %@</source> <source>offered %@</source>
@@ -6422,6 +6041,11 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>päällä</target> <target>päällä</target>
<note>group pref value</note> <note>group pref value</note>
</trans-unit> </trans-unit>
<trans-unit id="or chat with the developers" xml:space="preserve">
<source>or chat with the developers</source>
<target>tai keskustele kehittäjien kanssa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="owner" xml:space="preserve"> <trans-unit id="owner" xml:space="preserve">
<source>owner</source> <source>owner</source>
<target>omistaja</target> <target>omistaja</target>
@@ -6511,10 +6135,6 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>päivitetty ryhmäprofiili</target> <target>päivitetty ryhmäprofiili</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve"> <trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source> <source>v%@ (%@)</source>
<target>v%@ (%@)</target> <target>v%@ (%@)</target>
@@ -6652,10 +6272,6 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>SimpleX käyttää Face ID:tä paikalliseen todennukseen</target> <target>SimpleX käyttää Face ID:tä paikalliseen todennukseen</target>
<note>Privacy - Face ID Usage Description</note> <note>Privacy - Face ID Usage Description</note>
</trans-unit> </trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX tarvitsee mikrofonia ääni- ja videopuheluita ja ääniviestien tallentamista varten.</target> <target>SimpleX tarvitsee mikrofonia ääni- ja videopuheluita ja ääniviestien tallentamista varten.</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
@@ -84,10 +84,6 @@
<target>%@ / %@</target> <target>%@ / %@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve">
<source>%@ and %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve"> <trans-unit id="%@ and %@ connected" xml:space="preserve">
<source>%@ and %@ connected</source> <source>%@ and %@ connected</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
@@ -97,10 +93,6 @@
<target>%1$@ ที่ %2$@:</target> <target>%1$@ ที่ %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note> <note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ connected" xml:space="preserve">
<source>%@ connected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve"> <trans-unit id="%@ is connected!" xml:space="preserve">
<source>%@ is connected!</source> <source>%@ is connected!</source>
<target>%@ เชื่อมต่อสำเร็จ!</target> <target>%@ เชื่อมต่อสำเร็จ!</target>
@@ -126,10 +118,6 @@
<target>%@ อยากเชื่อมต่อ!</target> <target>%@ อยากเชื่อมต่อ!</target>
<note>notification title</note> <note>notification title</note>
</trans-unit> </trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve">
<source>%@, %@ and %lld other members connected</source> <source>%@, %@ and %lld other members connected</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
@@ -194,27 +182,11 @@
<target>%lld ไฟล์ที่มีขนาดรวม %@</target> <target>%lld ไฟล์ที่มีขนาดรวม %@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld group events" xml:space="preserve">
<source>%lld group events</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld members" xml:space="preserve"> <trans-unit id="%lld members" xml:space="preserve">
<source>%lld members</source> <source>%lld members</source>
<target>%lld สมาชิก</target> <target>%lld สมาชิก</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked" xml:space="preserve">
<source>%lld messages blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages moderated by %@" xml:space="preserve">
<source>%lld messages moderated by %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld minutes" xml:space="preserve"> <trans-unit id="%lld minutes" xml:space="preserve">
<source>%lld minutes</source> <source>%lld minutes</source>
<target>%lld นาที</target> <target>%lld นาที</target>
@@ -284,14 +256,6 @@
<target>(</target> <target>(</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve"> <trans-unit id=")" xml:space="preserve">
<source>)</source> <source>)</source>
<target>)</target> <target>)</target>
@@ -377,12 +341,6 @@
- และอื่น ๆ!</target> - และอื่น ๆ!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve"> <trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes. <source>- voice messages up to 5 minutes.
- custom time to disappear. - custom time to disappear.
@@ -397,10 +355,6 @@
<target>.</target> <target>.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="0 sec" xml:space="preserve">
<source>0 sec</source>
<note>time to disappear</note>
</trans-unit>
<trans-unit id="0s" xml:space="preserve"> <trans-unit id="0s" xml:space="preserve">
<source>0s</source> <source>0s</source>
<target>0s</target> <target>0s</target>
@@ -624,10 +578,6 @@
<target>ข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น.</target> <target>ข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve">
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve"> <trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source> <source>All your contacts will remain connected.</source>
<target>ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่.</target> <target>ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่.</target>
@@ -733,14 +683,6 @@
<target>เชื่อมต่อสำเร็จแล้ว?</target> <target>เชื่อมต่อสำเร็จแล้ว?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Already connecting!" xml:space="preserve">
<source>Already connecting!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Already joining the group!" xml:space="preserve">
<source>Already joining the group!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Always use relay" xml:space="preserve"> <trans-unit id="Always use relay" xml:space="preserve">
<source>Always use relay</source> <source>Always use relay</source>
<target>ใช้รีเลย์เสมอ</target> <target>ใช้รีเลย์เสมอ</target>
@@ -860,10 +802,6 @@
<target>กลับ</target> <target>กลับ</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve"> <trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source> <source>Bad message ID</source>
<target>ID ข้อความที่ไม่ดี</target> <target>ID ข้อความที่ไม่ดี</target>
@@ -874,31 +812,11 @@
<target>แฮชข้อความไม่ดี</target> <target>แฮชข้อความไม่ดี</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source> <source>Better messages</source>
<target>ข้อความที่ดีขึ้น</target> <target>ข้อความที่ดีขึ้น</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block" xml:space="preserve">
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
<source>Block member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
<source>Both you and your contact can add message reactions.</source> <source>Both you and your contact can add message reactions.</source>
<target>ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้</target> <target>ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้</target>
@@ -1159,34 +1077,21 @@
<target>เชื่อมต่อ</target> <target>เชื่อมต่อ</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve"> <trans-unit id="Connect directly" xml:space="preserve">
<source>Connect automatically</source> <source>Connect directly</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve"> <trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source> <source>Connect incognito</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve"> <trans-unit id="Connect via contact link" xml:space="preserve">
<source>Connect to desktop</source> <source>Connect via contact link</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve"> <trans-unit id="Connect via group link?" xml:space="preserve">
<source>Connect to yourself?</source> <source>Connect via group link?</source>
<note>No comment provided by engineer.</note> <target>เชื่อมต่อผ่านลิงค์กลุ่ม?</target>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own SimpleX address!" xml:space="preserve">
<source>Connect to yourself?
This is your own SimpleX address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own one-time link!" xml:space="preserve">
<source>Connect to yourself?
This is your own one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact address" xml:space="preserve">
<source>Connect via contact address</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect via link" xml:space="preserve"> <trans-unit id="Connect via link" xml:space="preserve">
@@ -1203,18 +1108,6 @@ This is your own one-time link!</source>
<source>Connect via one-time link</source> <source>Connect via one-time link</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect with %@" xml:space="preserve">
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve"> <trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source> <source>Connecting to server…</source>
<target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์…</target> <target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์…</target>
@@ -1225,10 +1118,6 @@ This is your own one-time link!</source>
<target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@)</target> <target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@)</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve"> <trans-unit id="Connection" xml:space="preserve">
<source>Connection</source> <source>Connection</source>
<target>การเชื่อมต่อ</target> <target>การเชื่อมต่อ</target>
@@ -1249,10 +1138,6 @@ This is your own one-time link!</source>
<target>ส่งคําขอเชื่อมต่อแล้ว!</target> <target>ส่งคําขอเชื่อมต่อแล้ว!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve"> <trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source> <source>Connection timeout</source>
<target>หมดเวลาการเชื่อมต่อ</target> <target>หมดเวลาการเชื่อมต่อ</target>
@@ -1268,6 +1153,11 @@ This is your own one-time link!</source>
<target>ผู้ติดต่อรายนี้มีอยู่แล้ว</target> <target>ผู้ติดต่อรายนี้มีอยู่แล้ว</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Contact and all messages will be deleted - this cannot be undone!</source>
<target>ผู้ติดต่อและข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact hidden:" xml:space="preserve"> <trans-unit id="Contact hidden:" xml:space="preserve">
<source>Contact hidden:</source> <source>Contact hidden:</source>
<target>ผู้ติดต่อถูกซ่อน:</target> <target>ผู้ติดต่อถูกซ่อน:</target>
@@ -1318,10 +1208,6 @@ This is your own one-time link!</source>
<target>รุ่นหลัก: v%@</target> <target>รุ่นหลัก: v%@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve"> <trans-unit id="Create" xml:space="preserve">
<source>Create</source> <source>Create</source>
<target>สร้าง</target> <target>สร้าง</target>
@@ -1332,10 +1218,6 @@ This is your own one-time link!</source>
<target>สร้างที่อยู่ SimpleX</target> <target>สร้างที่อยู่ SimpleX</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve"> <trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source> <source>Create an address to let people connect with you.</source>
<target>สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ</target> <target>สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ</target>
@@ -1346,10 +1228,6 @@ This is your own one-time link!</source>
<target>สร้างไฟล์</target> <target>สร้างไฟล์</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Create group" xml:space="preserve">
<source>Create group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create group link" xml:space="preserve"> <trans-unit id="Create group link" xml:space="preserve">
<source>Create group link</source> <source>Create group link</source>
<target>สร้างลิงค์กลุ่ม</target> <target>สร้างลิงค์กลุ่ม</target>
@@ -1369,10 +1247,6 @@ This is your own one-time link!</source>
<target>สร้างลิงก์เชิญแบบใช้ครั้งเดียว</target> <target>สร้างลิงก์เชิญแบบใช้ครั้งเดียว</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create queue" xml:space="preserve"> <trans-unit id="Create queue" xml:space="preserve">
<source>Create queue</source> <source>Create queue</source>
<target>สร้างคิว</target> <target>สร้างคิว</target>
@@ -1531,10 +1405,6 @@ This is your own one-time link!</source>
<target>ลบ</target> <target>ลบ</target>
<note>chat item action</note> <note>chat item action</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete %lld messages?" xml:space="preserve">
<source>Delete %lld messages?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete Contact" xml:space="preserve"> <trans-unit id="Delete Contact" xml:space="preserve">
<source>Delete Contact</source> <source>Delete Contact</source>
<target>ลบผู้ติดต่อ</target> <target>ลบผู้ติดต่อ</target>
@@ -1560,10 +1430,6 @@ This is your own one-time link!</source>
<target>ลบไฟล์ทั้งหมด</target> <target>ลบไฟล์ทั้งหมด</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete and notify contact" xml:space="preserve">
<source>Delete and notify contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete archive" xml:space="preserve"> <trans-unit id="Delete archive" xml:space="preserve">
<source>Delete archive</source> <source>Delete archive</source>
<target>ลบที่เก็บถาวร</target> <target>ลบที่เก็บถาวร</target>
@@ -1594,9 +1460,9 @@ This is your own one-time link!</source>
<target>ลบผู้ติดต่อ</target> <target>ลบผู้ติดต่อ</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete contact?&#10;This cannot be undone!" xml:space="preserve"> <trans-unit id="Delete contact?" xml:space="preserve">
<source>Delete contact? <source>Delete contact?</source>
This cannot be undone!</source> <target>ลบผู้ติดต่อ?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete database" xml:space="preserve"> <trans-unit id="Delete database" xml:space="preserve">
@@ -1738,18 +1604,6 @@ This cannot be undone!</source>
<target>คำอธิบาย</target> <target>คำอธิบาย</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve"> <trans-unit id="Develop" xml:space="preserve">
<source>Develop</source> <source>Develop</source>
<target>พัฒนา</target> <target>พัฒนา</target>
@@ -1840,16 +1694,18 @@ This cannot be undone!</source>
<target>ตัดการเชื่อมต่อ</target> <target>ตัดการเชื่อมต่อ</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve"> <trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source> <source>Discover and join groups</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve"> <trans-unit id="Display name" xml:space="preserve">
<source>Discover via local network</source> <source>Display name</source>
<target>ชื่อที่แสดง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Display name:" xml:space="preserve">
<source>Display name:</source>
<target>ชื่อที่แสดง:</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
@@ -2020,14 +1876,6 @@ This cannot be undone!</source>
<target>ข้อความที่ encrypt: ข้อผิดพลาดที่ไม่คาดคิด</target> <target>ข้อความที่ encrypt: ข้อผิดพลาดที่ไม่คาดคิด</target>
<note>notification</note> <note>notification</note>
</trans-unit> </trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve"> <trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source> <source>Enter Passcode</source>
<target>ใส่รหัสผ่าน</target> <target>ใส่รหัสผ่าน</target>
@@ -2038,10 +1886,6 @@ This cannot be undone!</source>
<target>ใส่รหัสผ่านที่ถูกต้อง</target> <target>ใส่รหัสผ่านที่ถูกต้อง</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter group name…" xml:space="preserve">
<source>Enter group name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter passphrase…" xml:space="preserve"> <trans-unit id="Enter passphrase…" xml:space="preserve">
<source>Enter passphrase…</source> <source>Enter passphrase…</source>
<target>ใส่รหัสผ่าน</target> <target>ใส่รหัสผ่าน</target>
@@ -2057,10 +1901,6 @@ This cannot be undone!</source>
<target>ใส่เซิร์ฟเวอร์ด้วยตนเอง</target> <target>ใส่เซิร์ฟเวอร์ด้วยตนเอง</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve"> <trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source> <source>Enter welcome message…</source>
<target>ใส่ข้อความต้อนรับ…</target> <target>ใส่ข้อความต้อนรับ…</target>
@@ -2071,10 +1911,6 @@ This cannot be undone!</source>
<target>ใส่ข้อความต้อนรับ… (ไม่บังคับ)</target> <target>ใส่ข้อความต้อนรับ… (ไม่บังคับ)</target>
<note>placeholder</note> <note>placeholder</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter your name…" xml:space="preserve">
<source>Enter your name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error" xml:space="preserve"> <trans-unit id="Error" xml:space="preserve">
<source>Error</source> <source>Error</source>
<target>ผิดพลาด</target> <target>ผิดพลาด</target>
@@ -2347,10 +2183,6 @@ This cannot be undone!</source>
<target>ออกโดยไม่บันทึก</target> <target>ออกโดยไม่บันทึก</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Expand" xml:space="preserve">
<source>Expand</source>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Export database" xml:space="preserve"> <trans-unit id="Export database" xml:space="preserve">
<source>Export database</source> <source>Export database</source>
<target>ส่งออกฐานข้อมูล</target> <target>ส่งออกฐานข้อมูล</target>
@@ -2381,10 +2213,6 @@ This cannot be undone!</source>
<target>รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!</target> <target>รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve"> <trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source> <source>Favorite</source>
<target>ที่ชอบ</target> <target>ที่ชอบ</target>
@@ -2480,10 +2308,6 @@ This cannot be undone!</source>
<target>สำหรับคอนโซล</target> <target>สำหรับคอนโซล</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve"> <trans-unit id="French interface" xml:space="preserve">
<source>French interface</source> <source>French interface</source>
<target>อินเทอร์เฟซภาษาฝรั่งเศส</target> <target>อินเทอร์เฟซภาษาฝรั่งเศส</target>
@@ -2504,10 +2328,6 @@ This cannot be undone!</source>
<target>ชื่อเต็ม:</target> <target>ชื่อเต็ม:</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve">
<source>Fully re-implemented - work in background!</source> <source>Fully re-implemented - work in background!</source>
<target>ดำเนินการใหม่อย่างสมบูรณ์ - ทำงานในพื้นหลัง!</target> <target>ดำเนินการใหม่อย่างสมบูรณ์ - ทำงานในพื้นหลัง!</target>
@@ -2528,14 +2348,6 @@ This cannot be undone!</source>
<target>กลุ่ม</target> <target>กลุ่ม</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Group already exists" xml:space="preserve">
<source>Group already exists</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group already exists!" xml:space="preserve">
<source>Group already exists!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group display name" xml:space="preserve"> <trans-unit id="Group display name" xml:space="preserve">
<source>Group display name</source> <source>Group display name</source>
<target>ชื่อกลุ่มที่แสดง</target> <target>ชื่อกลุ่มที่แสดง</target>
@@ -2806,10 +2618,6 @@ This cannot be undone!</source>
<target>ไม่ระบุตัวตน</target> <target>ไม่ระบุตัวตน</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve"> <trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source> <source>Incognito mode</source>
<target>โหมดไม่ระบุตัวตน</target> <target>โหมดไม่ระบุตัวตน</target>
@@ -2839,10 +2647,6 @@ This cannot be undone!</source>
<target>เวอร์ชันฐานข้อมูลที่เข้ากันไม่ได้</target> <target>เวอร์ชันฐานข้อมูลที่เข้ากันไม่ได้</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve"> <trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source> <source>Incorrect passcode</source>
<target>รหัสผ่านไม่ถูกต้อง</target> <target>รหัสผ่านไม่ถูกต้อง</target>
@@ -2890,10 +2694,6 @@ This cannot be undone!</source>
<target>ลิงค์เชื่อมต่อไม่ถูกต้อง</target> <target>ลิงค์เชื่อมต่อไม่ถูกต้อง</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve"> <trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source> <source>Invalid server address!</source>
<target>ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!</target> <target>ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!</target>
@@ -2984,33 +2784,16 @@ This cannot be undone!</source>
<target>เข้าร่วมกลุ่ม</target> <target>เข้าร่วมกลุ่ม</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
<source>Join group?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join incognito" xml:space="preserve"> <trans-unit id="Join incognito" xml:space="preserve">
<source>Join incognito</source> <source>Join incognito</source>
<target>เข้าร่วมแบบไม่ระบุตัวตน</target> <target>เข้าร่วมแบบไม่ระบุตัวตน</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join with current profile" xml:space="preserve">
<source>Join with current profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join your group?&#10;This is your link for group %@!" xml:space="preserve">
<source>Join your group?
This is your link for group %@!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Joining group" xml:space="preserve"> <trans-unit id="Joining group" xml:space="preserve">
<source>Joining group</source> <source>Joining group</source>
<target>กำลังจะเข้าร่วมกลุ่ม</target> <target>กำลังจะเข้าร่วมกลุ่ม</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve"> <trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source> <source>Keep your connections</source>
<target>รักษาการเชื่อมต่อของคุณ</target> <target>รักษาการเชื่อมต่อของคุณ</target>
@@ -3071,18 +2854,6 @@ This is your link for group %@!</source>
<target>ข้อจำกัด</target> <target>ข้อจำกัด</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve"> <trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source> <source>Live message!</source>
<target>ข้อความสด!</target> <target>ข้อความสด!</target>
@@ -3233,10 +3004,6 @@ This is your link for group %@!</source>
<target>ข้อความและไฟล์</target> <target>ข้อความและไฟล์</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Messages from %@ will be shown!" xml:space="preserve">
<source>Messages from %@ will be shown!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migrating database archive…" xml:space="preserve"> <trans-unit id="Migrating database archive…" xml:space="preserve">
<source>Migrating database archive…</source> <source>Migrating database archive…</source>
<target>กำลังย้ายข้อมูลที่เก็บถาวรของฐานข้อมูล…</target> <target>กำลังย้ายข้อมูลที่เก็บถาวรของฐานข้อมูล…</target>
@@ -3429,10 +3196,6 @@ This is your link for group %@!</source>
<target>ไม่มีไฟล์ที่ได้รับหรือส่ง</target> <target>ไม่มีไฟล์ที่ได้รับหรือส่ง</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve"> <trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source> <source>Notifications</source>
<target>การแจ้งเตือน</target> <target>การแจ้งเตือน</target>
@@ -3586,10 +3349,6 @@ This is your link for group %@!</source>
<target>เปิดคอนโซลการแชท</target> <target>เปิดคอนโซลการแชท</target>
<note>authentication reason</note> <note>authentication reason</note>
</trans-unit> </trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open user profiles" xml:space="preserve"> <trans-unit id="Open user profiles" xml:space="preserve">
<source>Open user profiles</source> <source>Open user profiles</source>
<target>เปิดโปรไฟล์ผู้ใช้</target> <target>เปิดโปรไฟล์ผู้ใช้</target>
@@ -3605,6 +3364,11 @@ This is your link for group %@!</source>
<target>กำลังเปิดฐานข้อมูล…</target> <target>กำลังเปิดฐานข้อมูล…</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
<source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source>
<target>การเปิดลิงก์ในเบราว์เซอร์อาจลดความเป็นส่วนตัวและความปลอดภัยของการเชื่อมต่อ ลิงก์ SimpleX ที่ไม่น่าเชื่อถือจะเป็นสีแดง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve"> <trans-unit id="PING count" xml:space="preserve">
<source>PING count</source> <source>PING count</source>
<target>จํานวน PING</target> <target>จํานวน PING</target>
@@ -3650,10 +3414,6 @@ This is your link for group %@!</source>
<target>แปะ</target> <target>แปะ</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve"> <trans-unit id="Paste image" xml:space="preserve">
<source>Paste image</source> <source>Paste image</source>
<target>แปะภาพ</target> <target>แปะภาพ</target>
@@ -3798,14 +3558,6 @@ This is your link for group %@!</source>
<target>รูปโปรไฟล์</target> <target>รูปโปรไฟล์</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve"> <trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source> <source>Profile password</source>
<target>รหัสผ่านโปรไฟล์</target> <target>รหัสผ่านโปรไฟล์</target>
@@ -4049,14 +3801,6 @@ This is your link for group %@!</source>
<target>เจรจา enryption ใหม่หรือไม่?</target> <target>เจรจา enryption ใหม่หรือไม่?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Repeat connection request?" xml:space="preserve">
<source>Repeat connection request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Repeat join request?" xml:space="preserve">
<source>Repeat join request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reply" xml:space="preserve"> <trans-unit id="Reply" xml:space="preserve">
<source>Reply</source> <source>Reply</source>
<target>ตอบ</target> <target>ตอบ</target>
@@ -4242,10 +3986,6 @@ This is your link for group %@!</source>
<target>สแกนคิวอาร์โค้ด</target> <target>สแกนคิวอาร์โค้ด</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve"> <trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source> <source>Scan code</source>
<target>สแกนรหัส</target> <target>สแกนรหัส</target>
@@ -4463,10 +4203,6 @@ This is your link for group %@!</source>
<target>เซิร์ฟเวอร์</target> <target>เซิร์ฟเวอร์</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve"> <trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source> <source>Set 1 day</source>
<target>ตั้ง 1 วัน</target> <target>ตั้ง 1 วัน</target>
@@ -4774,10 +4510,6 @@ This is your link for group %@!</source>
<target>แตะปุ่ม </target> <target>แตะปุ่ม </target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Tap to Connect" xml:space="preserve">
<source>Tap to Connect</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to activate profile." xml:space="preserve"> <trans-unit id="Tap to activate profile." xml:space="preserve">
<source>Tap to activate profile.</source> <source>Tap to activate profile.</source>
<target>แตะเพื่อเปิดใช้งานโปรไฟล์</target> <target>แตะเพื่อเปิดใช้งานโปรไฟล์</target>
@@ -4876,6 +4608,11 @@ It can happen because of some bug or when the connection is compromised.</source
<target>encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้!</target> <target>encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="The group is fully decentralized it is visible only to the members." xml:space="preserve">
<source>The group is fully decentralized it is visible only to the members.</source>
<target>กลุ่มมีการกระจายอำนาจอย่างเต็มที่ – มองเห็นได้เฉพาะสมาชิกเท่านั้น</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The hash of the previous message is different." xml:space="preserve"> <trans-unit id="The hash of the previous message is different." xml:space="preserve">
<source>The hash of the previous message is different.</source> <source>The hash of the previous message is different.</source>
<target>แฮชของข้อความก่อนหน้านี้แตกต่างกัน</target> <target>แฮชของข้อความก่อนหน้านี้แตกต่างกัน</target>
@@ -4960,10 +4697,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร</target> <target>การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source> <source>This group has over %lld members, delivery receipts are not sent.</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
@@ -4973,14 +4706,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>ไม่มีกลุ่มนี้แล้ว</target> <target>ไม่มีกลุ่มนี้แล้ว</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This is your own SimpleX address!" xml:space="preserve">
<source>This is your own SimpleX address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This is your own one-time link!" xml:space="preserve">
<source>This is your own one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve">
<source>This setting applies to messages in your current chat profile **%@**.</source> <source>This setting applies to messages in your current chat profile **%@**.</source>
<target>การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@**</target> <target>การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@**</target>
@@ -4996,10 +4721,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>เพื่อการเชื่อมต่อ ผู้ติดต่อของคุณสามารถสแกนคิวอาร์โค้ดหรือใช้ลิงก์ในแอป</target> <target>เพื่อการเชื่อมต่อ ผู้ติดต่อของคุณสามารถสแกนคิวอาร์โค้ดหรือใช้ลิงก์ในแอป</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve"> <trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source> <source>To make a new connection</source>
<target>เพื่อสร้างการเชื่อมต่อใหม่</target> <target>เพื่อสร้างการเชื่อมต่อใหม่</target>
@@ -5081,18 +4802,6 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>ไม่สามารถบันทึกข้อความเสียง</target> <target>ไม่สามารถบันทึกข้อความเสียง</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock" xml:space="preserve">
<source>Unblock</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
<source>Unblock member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
<source>Unblock member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unexpected error: %@" xml:space="preserve"> <trans-unit id="Unexpected error: %@" xml:space="preserve">
<source>Unexpected error: %@</source> <source>Unexpected error: %@</source>
<target>ข้อผิดพลาดที่ไม่คาดคิด: %@</target> <target>ข้อผิดพลาดที่ไม่คาดคิด: %@</target>
@@ -5155,14 +4864,6 @@ To connect, please ask your contact to create another connection link and check
ในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร</target> ในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve"> <trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source> <source>Unlock</source>
<target>ปลดล็อค</target> <target>ปลดล็อค</target>
@@ -5252,10 +4953,6 @@ To connect, please ask your contact to create another connection link and check
<target>ใช้สำหรับการเชื่อมต่อใหม่</target> <target>ใช้สำหรับการเชื่อมต่อใหม่</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve"> <trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source> <source>Use iOS call interface</source>
<target>ใช้อินเทอร์เฟซการโทร iOS</target> <target>ใช้อินเทอร์เฟซการโทร iOS</target>
@@ -5285,23 +4982,11 @@ To connect, please ask your contact to create another connection link and check
<target>กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่</target> <target>กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve"> <trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source> <source>Verify connection security</source>
<target>ตรวจสอบความปลอดภัยในการเชื่อมต่อ</target> <target>ตรวจสอบความปลอดภัยในการเชื่อมต่อ</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve"> <trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source> <source>Verify security code</source>
<target>ตรวจสอบรหัสความปลอดภัย</target> <target>ตรวจสอบรหัสความปลอดภัย</target>
@@ -5312,10 +4997,6 @@ To connect, please ask your contact to create another connection link and check
<target>ผ่านเบราว์เซอร์</target> <target>ผ่านเบราว์เซอร์</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve"> <trans-unit id="Video call" xml:space="preserve">
<source>Video call</source> <source>Video call</source>
<target>การสนทนาทางวิดีโอ</target> <target>การสนทนาทางวิดีโอ</target>
@@ -5366,10 +5047,6 @@ To connect, please ask your contact to create another connection link and check
<target>ข้อความเสียง…</target> <target>ข้อความเสียง…</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve"> <trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source> <source>Waiting for file</source>
<target>กำลังรอไฟล์</target> <target>กำลังรอไฟล์</target>
@@ -5470,35 +5147,6 @@ To connect, please ask your contact to create another connection link and check
<target>คุณได้เชื่อมต่อกับ %@ แล้ว</target> <target>คุณได้เชื่อมต่อกับ %@ แล้ว</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You are already connecting to %@." xml:space="preserve">
<source>You are already connecting to %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already connecting via this one-time link!" xml:space="preserve">
<source>You are already connecting via this one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already in group %@." xml:space="preserve">
<source>You are already in group %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group %@." xml:space="preserve">
<source>You are already joining the group %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group via this link!" xml:space="preserve">
<source>You are already joining the group via this link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group via this link." xml:space="preserve">
<source>You are already joining the group via this link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group!&#10;Repeat join request?" xml:space="preserve">
<source>You are already joining the group!
Repeat join request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
<source>You are connected to the server used to receive messages from this contact.</source> <source>You are connected to the server used to receive messages from this contact.</source>
<target>คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</target> <target>คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</target>
@@ -5594,15 +5242,6 @@ Repeat join request?</source>
<target>เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง.</target> <target>เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You have already requested connection via this address!" xml:space="preserve">
<source>You have already requested connection via this address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You have already requested connection!&#10;Repeat connection request?" xml:space="preserve">
<source>You have already requested connection!
Repeat connection request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You have no chats" xml:space="preserve"> <trans-unit id="You have no chats" xml:space="preserve">
<source>You have no chats</source> <source>You have no chats</source>
<target>คุณไม่มีการแชท</target> <target>คุณไม่มีการแชท</target>
@@ -5652,10 +5291,6 @@ Repeat connection request?</source>
<target>คุณจะเชื่อมต่อกับกลุ่มเมื่ออุปกรณ์โฮสต์ของกลุ่มออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!</target> <target>คุณจะเชื่อมต่อกับกลุ่มเมื่ออุปกรณ์โฮสต์ของกลุ่มออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve">
<source>You will be connected when group link host's device is online, please wait or check later!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
<source>You will be connected when your connection request is accepted, please wait or check later!</source> <source>You will be connected when your connection request is accepted, please wait or check later!</source>
<target>คุณจะเชื่อมต่อเมื่อคำขอเชื่อมต่อของคุณได้รับการยอมรับ โปรดรอหรือตรวจสอบในภายหลัง!</target> <target>คุณจะเชื่อมต่อเมื่อคำขอเชื่อมต่อของคุณได้รับการยอมรับ โปรดรอหรือตรวจสอบในภายหลัง!</target>
@@ -5671,8 +5306,9 @@ Repeat connection request?</source>
<target>คุณจะต้องตรวจสอบสิทธิ์เมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง</target> <target>คุณจะต้องตรวจสอบสิทธิ์เมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will connect to all group members." xml:space="preserve"> <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
<source>You will connect to all group members.</source> <source>You will join a group this link refers to and connect to its group members.</source>
<target>คุณจะเข้าร่วมกลุ่มที่ลิงก์นี้อ้างถึงและเชื่อมต่อกับสมาชิกในกลุ่ม</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve">
@@ -5740,6 +5376,11 @@ Repeat connection request?</source>
<target>ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt</target> <target>ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Your chat profile will be sent to group members" xml:space="preserve">
<source>Your chat profile will be sent to group members</source>
<target>โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve"> <trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source> <source>Your chat profiles</source>
<target>โปรไฟล์แชทของคุณ</target> <target>โปรไฟล์แชทของคุณ</target>
@@ -5794,10 +5435,6 @@ You can change it in Settings.</source>
<target>ความเป็นส่วนตัวของคุณ</target> <target>ความเป็นส่วนตัวของคุณ</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Your profile" xml:space="preserve">
<source>Your profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve">
<source>Your profile **%@** will be shared.</source> <source>Your profile **%@** will be shared.</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
@@ -5889,19 +5526,11 @@ SimpleX servers cannot see your profile.</source>
<target>เสมอ</target> <target>เสมอ</target>
<note>pref value</note> <note>pref value</note>
</trans-unit> </trans-unit>
<trans-unit id="and %lld other events" xml:space="preserve">
<source>and %lld other events</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve">
<source>audio call (not e2e encrypted)</source> <source>audio call (not e2e encrypted)</source>
<target>การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ)</target> <target>การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ)</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve"> <trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source> <source>bad message ID</source>
<target>ID ข้อความที่ไม่ดี</target> <target>ID ข้อความที่ไม่ดี</target>
@@ -5912,10 +5541,6 @@ SimpleX servers cannot see your profile.</source>
<target>แฮชข้อความไม่ดี</target> <target>แฮชข้อความไม่ดี</target>
<note>integrity error chat item</note> <note>integrity error chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked" xml:space="preserve">
<source>blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
<source>bold</source> <source>bold</source>
<target>ตัวหนา</target> <target>ตัวหนา</target>
@@ -6085,10 +5710,6 @@ SimpleX servers cannot see your profile.</source>
<target>ลบแล้ว</target> <target>ลบแล้ว</target>
<note>deleted chat item</note> <note>deleted chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="deleted contact" xml:space="preserve">
<source>deleted contact</source>
<note>rcv direct event chat item</note>
</trans-unit>
<trans-unit id="deleted group" xml:space="preserve"> <trans-unit id="deleted group" xml:space="preserve">
<source>deleted group</source> <source>deleted group</source>
<target>กลุ่มที่ถูกลบ</target> <target>กลุ่มที่ถูกลบ</target>
@@ -6371,8 +5992,7 @@ SimpleX servers cannot see your profile.</source>
<source>off</source> <source>off</source>
<target>ปิด</target> <target>ปิด</target>
<note>enabled status <note>enabled status
group pref value group pref value</note>
time to disappear</note>
</trans-unit> </trans-unit>
<trans-unit id="offered %@" xml:space="preserve"> <trans-unit id="offered %@" xml:space="preserve">
<source>offered %@</source> <source>offered %@</source>
@@ -6389,6 +6009,11 @@ SimpleX servers cannot see your profile.</source>
<target>เปิด</target> <target>เปิด</target>
<note>group pref value</note> <note>group pref value</note>
</trans-unit> </trans-unit>
<trans-unit id="or chat with the developers" xml:space="preserve">
<source>or chat with the developers</source>
<target>หรือแชทกับนักพัฒนาแอป</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="owner" xml:space="preserve"> <trans-unit id="owner" xml:space="preserve">
<source>owner</source> <source>owner</source>
<target>เจ้าของ</target> <target>เจ้าของ</target>
@@ -6478,10 +6103,6 @@ SimpleX servers cannot see your profile.</source>
<target>อัปเดตโปรไฟล์กลุ่มแล้ว</target> <target>อัปเดตโปรไฟล์กลุ่มแล้ว</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve"> <trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source> <source>v%@ (%@)</source>
<target>v%@ (%@)</target> <target>v%@ (%@)</target>
@@ -6619,10 +6240,6 @@ SimpleX servers cannot see your profile.</source>
<target>SimpleX ใช้ Face ID สำหรับการรับรองความถูกต้องในเครื่อง</target> <target>SimpleX ใช้ Face ID สำหรับการรับรองความถูกต้องในเครื่อง</target>
<note>Privacy - Face ID Usage Description</note> <note>Privacy - Face ID Usage Description</note>
</trans-unit> </trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX ต้องการการเข้าถึงไมโครโฟนสำหรับการโทรด้วยเสียงและวิดีโอ และเพื่อบันทึกข้อความเสียง</target> <target>SimpleX ต้องการการเข้าถึงไมโครโฟนสำหรับการโทรด้วยเสียงและวิดีโอ และเพื่อบันทึกข้อความเสียง</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
@@ -87,10 +87,6 @@
<target>%@ / %@</target> <target>%@ / %@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve">
<source>%@ and %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve"> <trans-unit id="%@ and %@ connected" xml:space="preserve">
<source>%@ and %@ connected</source> <source>%@ and %@ connected</source>
<target>%@ і %@ підключено</target> <target>%@ і %@ підключено</target>
@@ -101,10 +97,6 @@
<target>%1$@ за %2$@:</target> <target>%1$@ за %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note> <note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ connected" xml:space="preserve">
<source>%@ connected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve"> <trans-unit id="%@ is connected!" xml:space="preserve">
<source>%@ is connected!</source> <source>%@ is connected!</source>
<target>%@ підключено!</target> <target>%@ підключено!</target>
@@ -130,10 +122,6 @@
<target>%@ хоче підключитися!</target> <target>%@ хоче підключитися!</target>
<note>notification title</note> <note>notification title</note>
</trans-unit> </trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve">
<source>%@, %@ and %lld other members connected</source> <source>%@, %@ and %lld other members connected</source>
<target>%@, %@ та %lld інші підключені учасники</target> <target>%@, %@ та %lld інші підключені учасники</target>
@@ -199,27 +187,11 @@
<target>%lld файл(и) загальним розміром %@</target> <target>%lld файл(и) загальним розміром %@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld group events" xml:space="preserve">
<source>%lld group events</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld members" xml:space="preserve"> <trans-unit id="%lld members" xml:space="preserve">
<source>%lld members</source> <source>%lld members</source>
<target>%lld учасників</target> <target>%lld учасників</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked" xml:space="preserve">
<source>%lld messages blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages moderated by %@" xml:space="preserve">
<source>%lld messages moderated by %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld minutes" xml:space="preserve"> <trans-unit id="%lld minutes" xml:space="preserve">
<source>%lld minutes</source> <source>%lld minutes</source>
<target>%lld хвилин</target> <target>%lld хвилин</target>
@@ -289,14 +261,6 @@
<target>(</target> <target>(</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve"> <trans-unit id=")" xml:space="preserve">
<source>)</source> <source>)</source>
<target>)</target> <target>)</target>
@@ -382,12 +346,6 @@
- і багато іншого!</target> - і багато іншого!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve"> <trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes. <source>- voice messages up to 5 minutes.
- custom time to disappear. - custom time to disappear.
@@ -402,10 +360,6 @@
<target>.</target> <target>.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="0 sec" xml:space="preserve">
<source>0 sec</source>
<note>time to disappear</note>
</trans-unit>
<trans-unit id="0s" xml:space="preserve"> <trans-unit id="0s" xml:space="preserve">
<source>0s</source> <source>0s</source>
<target>0с</target> <target>0с</target>
@@ -631,10 +585,6 @@
<target>Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас.</target> <target>Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve">
<source>All new messages from %@ will be hidden!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve"> <trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source> <source>All your contacts will remain connected.</source>
<target>Всі ваші контакти залишаться на зв'язку.</target> <target>Всі ваші контакти залишаться на зв'язку.</target>
@@ -740,14 +690,6 @@
<target>Вже підключено?</target> <target>Вже підключено?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Already connecting!" xml:space="preserve">
<source>Already connecting!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Already joining the group!" xml:space="preserve">
<source>Already joining the group!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Always use relay" xml:space="preserve"> <trans-unit id="Always use relay" xml:space="preserve">
<source>Always use relay</source> <source>Always use relay</source>
<target>Завжди використовуйте реле</target> <target>Завжди використовуйте реле</target>
@@ -867,10 +809,6 @@
<target>Назад</target> <target>Назад</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve"> <trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source> <source>Bad message ID</source>
<target>Неправильний ідентифікатор повідомлення</target> <target>Неправильний ідентифікатор повідомлення</target>
@@ -881,31 +819,11 @@
<target>Поганий хеш повідомлення</target> <target>Поганий хеш повідомлення</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source> <source>Better messages</source>
<target>Кращі повідомлення</target> <target>Кращі повідомлення</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block" xml:space="preserve">
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
<source>Block member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
<source>Both you and your contact can add message reactions.</source> <source>Both you and your contact can add message reactions.</source>
<target>Реакції на повідомлення можете додавати як ви, так і ваш контакт.</target> <target>Реакції на повідомлення можете додавати як ви, так і ваш контакт.</target>
@@ -1166,8 +1084,9 @@
<target>Підключіться</target> <target>Підключіться</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve"> <trans-unit id="Connect directly" xml:space="preserve">
<source>Connect automatically</source> <source>Connect directly</source>
<target>Підключіться безпосередньо</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve"> <trans-unit id="Connect incognito" xml:space="preserve">
@@ -1175,26 +1094,14 @@
<target>Підключайтеся інкогніто</target> <target>Підключайтеся інкогніто</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve"> <trans-unit id="Connect via contact link" xml:space="preserve">
<source>Connect to desktop</source> <source>Connect via contact link</source>
<target>Підключіться за контактним посиланням</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve"> <trans-unit id="Connect via group link?" xml:space="preserve">
<source>Connect to yourself?</source> <source>Connect via group link?</source>
<note>No comment provided by engineer.</note> <target>Підключитися за груповим посиланням?</target>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own SimpleX address!" xml:space="preserve">
<source>Connect to yourself?
This is your own SimpleX address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own one-time link!" xml:space="preserve">
<source>Connect to yourself?
This is your own one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via contact address" xml:space="preserve">
<source>Connect via contact address</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect via link" xml:space="preserve"> <trans-unit id="Connect via link" xml:space="preserve">
@@ -1212,18 +1119,6 @@ This is your own one-time link!</source>
<target>Під'єднатися за одноразовим посиланням</target> <target>Під'єднатися за одноразовим посиланням</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connect with %@" xml:space="preserve">
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve"> <trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source> <source>Connecting to server…</source>
<target>Підключення до сервера…</target> <target>Підключення до сервера…</target>
@@ -1234,10 +1129,6 @@ This is your own one-time link!</source>
<target>Підключення до сервера... (помилка: %@)</target> <target>Підключення до сервера... (помилка: %@)</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve"> <trans-unit id="Connection" xml:space="preserve">
<source>Connection</source> <source>Connection</source>
<target>Підключення</target> <target>Підключення</target>
@@ -1258,10 +1149,6 @@ This is your own one-time link!</source>
<target>Запит на підключення відправлено!</target> <target>Запит на підключення відправлено!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve"> <trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source> <source>Connection timeout</source>
<target>Тайм-аут з'єднання</target> <target>Тайм-аут з'єднання</target>
@@ -1277,6 +1164,11 @@ This is your own one-time link!</source>
<target>Контакт вже існує</target> <target>Контакт вже існує</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Contact and all messages will be deleted - this cannot be undone!</source>
<target>Контакт і всі повідомлення будуть видалені - це неможливо скасувати!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact hidden:" xml:space="preserve"> <trans-unit id="Contact hidden:" xml:space="preserve">
<source>Contact hidden:</source> <source>Contact hidden:</source>
<target>Контакт приховано:</target> <target>Контакт приховано:</target>
@@ -1327,10 +1219,6 @@ This is your own one-time link!</source>
<target>Основна версія: v%@</target> <target>Основна версія: v%@</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve"> <trans-unit id="Create" xml:space="preserve">
<source>Create</source> <source>Create</source>
<target>Створити</target> <target>Створити</target>
@@ -1341,10 +1229,6 @@ This is your own one-time link!</source>
<target>Створіть адресу SimpleX</target> <target>Створіть адресу SimpleX</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve"> <trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source> <source>Create an address to let people connect with you.</source>
<target>Створіть адресу, щоб люди могли з вами зв'язатися.</target> <target>Створіть адресу, щоб люди могли з вами зв'язатися.</target>
@@ -1355,10 +1239,6 @@ This is your own one-time link!</source>
<target>Створити файл</target> <target>Створити файл</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Create group" xml:space="preserve">
<source>Create group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create group link" xml:space="preserve"> <trans-unit id="Create group link" xml:space="preserve">
<source>Create group link</source> <source>Create group link</source>
<target>Створити групове посилання</target> <target>Створити групове посилання</target>
@@ -1378,10 +1258,6 @@ This is your own one-time link!</source>
<target>Створіть одноразове посилання-запрошення</target> <target>Створіть одноразове посилання-запрошення</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create queue" xml:space="preserve"> <trans-unit id="Create queue" xml:space="preserve">
<source>Create queue</source> <source>Create queue</source>
<target>Створити чергу</target> <target>Створити чергу</target>
@@ -1540,10 +1416,6 @@ This is your own one-time link!</source>
<target>Видалити</target> <target>Видалити</target>
<note>chat item action</note> <note>chat item action</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete %lld messages?" xml:space="preserve">
<source>Delete %lld messages?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete Contact" xml:space="preserve"> <trans-unit id="Delete Contact" xml:space="preserve">
<source>Delete Contact</source> <source>Delete Contact</source>
<target>Видалити контакт</target> <target>Видалити контакт</target>
@@ -1569,10 +1441,6 @@ This is your own one-time link!</source>
<target>Видалити всі файли</target> <target>Видалити всі файли</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete and notify contact" xml:space="preserve">
<source>Delete and notify contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete archive" xml:space="preserve"> <trans-unit id="Delete archive" xml:space="preserve">
<source>Delete archive</source> <source>Delete archive</source>
<target>Видалити архів</target> <target>Видалити архів</target>
@@ -1603,9 +1471,9 @@ This is your own one-time link!</source>
<target>Видалити контакт</target> <target>Видалити контакт</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete contact?&#10;This cannot be undone!" xml:space="preserve"> <trans-unit id="Delete contact?" xml:space="preserve">
<source>Delete contact? <source>Delete contact?</source>
This cannot be undone!</source> <target>Видалити контакт?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete database" xml:space="preserve"> <trans-unit id="Delete database" xml:space="preserve">
@@ -1748,18 +1616,6 @@ This cannot be undone!</source>
<target>Опис</target> <target>Опис</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve"> <trans-unit id="Develop" xml:space="preserve">
<source>Develop</source> <source>Develop</source>
<target>Розробник</target> <target>Розробник</target>
@@ -1850,16 +1706,18 @@ This cannot be undone!</source>
<target>Від'єднати</target> <target>Від'єднати</target>
<note>server test step</note> <note>server test step</note>
</trans-unit> </trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve"> <trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source> <source>Discover and join groups</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve"> <trans-unit id="Display name" xml:space="preserve">
<source>Discover via local network</source> <source>Display name</source>
<target>Відображуване ім'я</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Display name:" xml:space="preserve">
<source>Display name:</source>
<target>Відображуване ім'я:</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
@@ -2030,14 +1888,6 @@ This cannot be undone!</source>
<target>Зашифроване повідомлення: несподівана помилка</target> <target>Зашифроване повідомлення: несподівана помилка</target>
<note>notification</note> <note>notification</note>
</trans-unit> </trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve"> <trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source> <source>Enter Passcode</source>
<target>Введіть пароль</target> <target>Введіть пароль</target>
@@ -2048,10 +1898,6 @@ This cannot be undone!</source>
<target>Введіть правильну парольну фразу.</target> <target>Введіть правильну парольну фразу.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter group name…" xml:space="preserve">
<source>Enter group name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter passphrase…" xml:space="preserve"> <trans-unit id="Enter passphrase…" xml:space="preserve">
<source>Enter passphrase…</source> <source>Enter passphrase…</source>
<target>Введіть пароль…</target> <target>Введіть пароль…</target>
@@ -2067,10 +1913,6 @@ This cannot be undone!</source>
<target>Увійдіть на сервер вручну</target> <target>Увійдіть на сервер вручну</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve"> <trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source> <source>Enter welcome message…</source>
<target>Введіть вітальне повідомлення…</target> <target>Введіть вітальне повідомлення…</target>
@@ -2081,10 +1923,6 @@ This cannot be undone!</source>
<target>Введіть вітальне повідомлення... (необов'язково)</target> <target>Введіть вітальне повідомлення... (необов'язково)</target>
<note>placeholder</note> <note>placeholder</note>
</trans-unit> </trans-unit>
<trans-unit id="Enter your name…" xml:space="preserve">
<source>Enter your name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error" xml:space="preserve"> <trans-unit id="Error" xml:space="preserve">
<source>Error</source> <source>Error</source>
<target>Помилка</target> <target>Помилка</target>
@@ -2357,10 +2195,6 @@ This cannot be undone!</source>
<target>Вихід без збереження</target> <target>Вихід без збереження</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Expand" xml:space="preserve">
<source>Expand</source>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Export database" xml:space="preserve"> <trans-unit id="Export database" xml:space="preserve">
<source>Export database</source> <source>Export database</source>
<target>Експорт бази даних</target> <target>Експорт бази даних</target>
@@ -2391,10 +2225,6 @@ This cannot be undone!</source>
<target>Швидко і без очікування, поки відправник буде онлайн!</target> <target>Швидко і без очікування, поки відправник буде онлайн!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve"> <trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source> <source>Favorite</source>
<target>Улюблений</target> <target>Улюблений</target>
@@ -2490,10 +2320,6 @@ This cannot be undone!</source>
<target>Для консолі</target> <target>Для консолі</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve"> <trans-unit id="French interface" xml:space="preserve">
<source>French interface</source> <source>French interface</source>
<target>Французький інтерфейс</target> <target>Французький інтерфейс</target>
@@ -2514,10 +2340,6 @@ This cannot be undone!</source>
<target>Повне ім'я:</target> <target>Повне ім'я:</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve">
<source>Fully re-implemented - work in background!</source> <source>Fully re-implemented - work in background!</source>
<target>Повністю перероблено - робота у фоновому режимі!</target> <target>Повністю перероблено - робота у фоновому режимі!</target>
@@ -2538,14 +2360,6 @@ This cannot be undone!</source>
<target>Група</target> <target>Група</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Group already exists" xml:space="preserve">
<source>Group already exists</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group already exists!" xml:space="preserve">
<source>Group already exists!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group display name" xml:space="preserve"> <trans-unit id="Group display name" xml:space="preserve">
<source>Group display name</source> <source>Group display name</source>
<target>Назва групи для відображення</target> <target>Назва групи для відображення</target>
@@ -2816,10 +2630,6 @@ This cannot be undone!</source>
<target>Інкогніто</target> <target>Інкогніто</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve"> <trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source> <source>Incognito mode</source>
<target>Режим інкогніто</target> <target>Режим інкогніто</target>
@@ -2850,10 +2660,6 @@ This cannot be undone!</source>
<target>Несумісна версія бази даних</target> <target>Несумісна версія бази даних</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve"> <trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source> <source>Incorrect passcode</source>
<target>Неправильний пароль</target> <target>Неправильний пароль</target>
@@ -2901,10 +2707,6 @@ This cannot be undone!</source>
<target>Неправильне посилання для підключення</target> <target>Неправильне посилання для підключення</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve"> <trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source> <source>Invalid server address!</source>
<target>Неправильна адреса сервера!</target> <target>Неправильна адреса сервера!</target>
@@ -2996,33 +2798,16 @@ This cannot be undone!</source>
<target>Приєднуйтесь до групи</target> <target>Приєднуйтесь до групи</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
<source>Join group?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join incognito" xml:space="preserve"> <trans-unit id="Join incognito" xml:space="preserve">
<source>Join incognito</source> <source>Join incognito</source>
<target>Приєднуйтесь інкогніто</target> <target>Приєднуйтесь інкогніто</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join with current profile" xml:space="preserve">
<source>Join with current profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join your group?&#10;This is your link for group %@!" xml:space="preserve">
<source>Join your group?
This is your link for group %@!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Joining group" xml:space="preserve"> <trans-unit id="Joining group" xml:space="preserve">
<source>Joining group</source> <source>Joining group</source>
<target>Приєднання до групи</target> <target>Приєднання до групи</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve"> <trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source> <source>Keep your connections</source>
<target>Зберігайте свої зв'язки</target> <target>Зберігайте свої зв'язки</target>
@@ -3083,18 +2868,6 @@ This is your link for group %@!</source>
<target>Обмеження</target> <target>Обмеження</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve"> <trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source> <source>Live message!</source>
<target>Живе повідомлення!</target> <target>Живе повідомлення!</target>
@@ -3245,10 +3018,6 @@ This is your link for group %@!</source>
<target>Повідомлення та файли</target> <target>Повідомлення та файли</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Messages from %@ will be shown!" xml:space="preserve">
<source>Messages from %@ will be shown!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Migrating database archive…" xml:space="preserve"> <trans-unit id="Migrating database archive…" xml:space="preserve">
<source>Migrating database archive…</source> <source>Migrating database archive…</source>
<target>Перенесення архіву бази даних…</target> <target>Перенесення архіву бази даних…</target>
@@ -3443,10 +3212,6 @@ This is your link for group %@!</source>
<target>Немає отриманих або відправлених файлів</target> <target>Немає отриманих або відправлених файлів</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve"> <trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source> <source>Notifications</source>
<target>Сповіщення</target> <target>Сповіщення</target>
@@ -3600,10 +3365,6 @@ This is your link for group %@!</source>
<target>Відкрийте консоль чату</target> <target>Відкрийте консоль чату</target>
<note>authentication reason</note> <note>authentication reason</note>
</trans-unit> </trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open user profiles" xml:space="preserve"> <trans-unit id="Open user profiles" xml:space="preserve">
<source>Open user profiles</source> <source>Open user profiles</source>
<target>Відкрити профілі користувачів</target> <target>Відкрити профілі користувачів</target>
@@ -3619,6 +3380,11 @@ This is your link for group %@!</source>
<target>Відкриття бази даних…</target> <target>Відкриття бази даних…</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
<source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source>
<target>Відкриття посилання в браузері може знизити конфіденційність і безпеку з'єднання. Ненадійні посилання SimpleX будуть червоного кольору.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve"> <trans-unit id="PING count" xml:space="preserve">
<source>PING count</source> <source>PING count</source>
<target>Кількість PING</target> <target>Кількість PING</target>
@@ -3664,10 +3430,6 @@ This is your link for group %@!</source>
<target>Вставити</target> <target>Вставити</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve"> <trans-unit id="Paste image" xml:space="preserve">
<source>Paste image</source> <source>Paste image</source>
<target>Вставити зображення</target> <target>Вставити зображення</target>
@@ -3813,14 +3575,6 @@ This is your link for group %@!</source>
<target>Зображення профілю</target> <target>Зображення профілю</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve"> <trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source> <source>Profile password</source>
<target>Пароль до профілю</target> <target>Пароль до профілю</target>
@@ -4066,14 +3820,6 @@ This is your link for group %@!</source>
<target>Переузгодьте шифрування?</target> <target>Переузгодьте шифрування?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Repeat connection request?" xml:space="preserve">
<source>Repeat connection request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Repeat join request?" xml:space="preserve">
<source>Repeat join request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reply" xml:space="preserve"> <trans-unit id="Reply" xml:space="preserve">
<source>Reply</source> <source>Reply</source>
<target>Відповісти</target> <target>Відповісти</target>
@@ -4259,10 +4005,6 @@ This is your link for group %@!</source>
<target>Відскануйте QR-код</target> <target>Відскануйте QR-код</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve"> <trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source> <source>Scan code</source>
<target>Сканувати код</target> <target>Сканувати код</target>
@@ -4482,10 +4224,6 @@ This is your link for group %@!</source>
<target>Сервери</target> <target>Сервери</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve"> <trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source> <source>Set 1 day</source>
<target>Встановити 1 день</target> <target>Встановити 1 день</target>
@@ -4795,10 +4533,6 @@ This is your link for group %@!</source>
<target>Натисніть кнопку </target> <target>Натисніть кнопку </target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Tap to Connect" xml:space="preserve">
<source>Tap to Connect</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to activate profile." xml:space="preserve"> <trans-unit id="Tap to activate profile." xml:space="preserve">
<source>Tap to activate profile.</source> <source>Tap to activate profile.</source>
<target>Натисніть, щоб активувати профіль.</target> <target>Натисніть, щоб активувати профіль.</target>
@@ -4896,6 +4630,11 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання!</target> <target>Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="The group is fully decentralized it is visible only to the members." xml:space="preserve">
<source>The group is fully decentralized it is visible only to the members.</source>
<target>Група повністю децентралізована - її бачать лише учасники.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The hash of the previous message is different." xml:space="preserve"> <trans-unit id="The hash of the previous message is different." xml:space="preserve">
<source>The hash of the previous message is different.</source> <source>The hash of the previous message is different.</source>
<target>Хеш попереднього повідомлення відрізняється.</target> <target>Хеш попереднього повідомлення відрізняється.</target>
@@ -4981,10 +4720,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені.</target> <target>Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source> <source>This group has over %lld members, delivery receipts are not sent.</source>
<target>У цій групі більше %lld учасників, підтвердження доставки не надсилаються.</target> <target>У цій групі більше %lld учасників, підтвердження доставки не надсилаються.</target>
@@ -4995,14 +4730,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Цієї групи більше не існує.</target> <target>Цієї групи більше не існує.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This is your own SimpleX address!" xml:space="preserve">
<source>This is your own SimpleX address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This is your own one-time link!" xml:space="preserve">
<source>This is your own one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve">
<source>This setting applies to messages in your current chat profile **%@**.</source> <source>This setting applies to messages in your current chat profile **%@**.</source>
<target>Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**.</target> <target>Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**.</target>
@@ -5018,10 +4745,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку.</target> <target>Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve"> <trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source> <source>To make a new connection</source>
<target>Щоб створити нове з'єднання</target> <target>Щоб створити нове з'єднання</target>
@@ -5103,18 +4826,6 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>Не вдається записати голосове повідомлення</target> <target>Не вдається записати голосове повідомлення</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock" xml:space="preserve">
<source>Unblock</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
<source>Unblock member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
<source>Unblock member?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unexpected error: %@" xml:space="preserve"> <trans-unit id="Unexpected error: %@" xml:space="preserve">
<source>Unexpected error: %@</source> <source>Unexpected error: %@</source>
<target>Неочікувана помилка: %@</target> <target>Неочікувана помилка: %@</target>
@@ -5177,14 +4888,6 @@ To connect, please ask your contact to create another connection link and check
Щоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею.</target> Щоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve"> <trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source> <source>Unlock</source>
<target>Розблокувати</target> <target>Розблокувати</target>
@@ -5275,10 +4978,6 @@ To connect, please ask your contact to create another connection link and check
<target>Використовуйте для нових з'єднань</target> <target>Використовуйте для нових з'єднань</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve"> <trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source> <source>Use iOS call interface</source>
<target>Використовуйте інтерфейс виклику iOS</target> <target>Використовуйте інтерфейс виклику iOS</target>
@@ -5309,23 +5008,11 @@ To connect, please ask your contact to create another connection link and check
<target>Використання серверів SimpleX Chat.</target> <target>Використання серверів SimpleX Chat.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve"> <trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source> <source>Verify connection security</source>
<target>Перевірте безпеку з'єднання</target> <target>Перевірте безпеку з'єднання</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve"> <trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source> <source>Verify security code</source>
<target>Підтвердіть код безпеки</target> <target>Підтвердіть код безпеки</target>
@@ -5336,10 +5023,6 @@ To connect, please ask your contact to create another connection link and check
<target>Через браузер</target> <target>Через браузер</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve"> <trans-unit id="Video call" xml:space="preserve">
<source>Video call</source> <source>Video call</source>
<target>Відеодзвінок</target> <target>Відеодзвінок</target>
@@ -5390,10 +5073,6 @@ To connect, please ask your contact to create another connection link and check
<target>Голосове повідомлення…</target> <target>Голосове повідомлення…</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve"> <trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source> <source>Waiting for file</source>
<target>Очікування файлу</target> <target>Очікування файлу</target>
@@ -5494,35 +5173,6 @@ To connect, please ask your contact to create another connection link and check
<target>Ви вже підключені до %@.</target> <target>Ви вже підключені до %@.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You are already connecting to %@." xml:space="preserve">
<source>You are already connecting to %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already connecting via this one-time link!" xml:space="preserve">
<source>You are already connecting via this one-time link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already in group %@." xml:space="preserve">
<source>You are already in group %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group %@." xml:space="preserve">
<source>You are already joining the group %@.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group via this link!" xml:space="preserve">
<source>You are already joining the group via this link!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group via this link." xml:space="preserve">
<source>You are already joining the group via this link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already joining the group!&#10;Repeat join request?" xml:space="preserve">
<source>You are already joining the group!
Repeat join request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
<source>You are connected to the server used to receive messages from this contact.</source> <source>You are connected to the server used to receive messages from this contact.</source>
<target>Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту.</target> <target>Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту.</target>
@@ -5618,15 +5268,6 @@ Repeat join request?</source>
<target>Вас не вдалося верифікувати, спробуйте ще раз.</target> <target>Вас не вдалося верифікувати, спробуйте ще раз.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You have already requested connection via this address!" xml:space="preserve">
<source>You have already requested connection via this address!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You have already requested connection!&#10;Repeat connection request?" xml:space="preserve">
<source>You have already requested connection!
Repeat connection request?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You have no chats" xml:space="preserve"> <trans-unit id="You have no chats" xml:space="preserve">
<source>You have no chats</source> <source>You have no chats</source>
<target>У вас немає чатів</target> <target>У вас немає чатів</target>
@@ -5677,10 +5318,6 @@ Repeat connection request?</source>
<target>Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!</target> <target>Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve">
<source>You will be connected when group link host's device is online, please wait or check later!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
<source>You will be connected when your connection request is accepted, please wait or check later!</source> <source>You will be connected when your connection request is accepted, please wait or check later!</source>
<target>Ви будете підключені, коли ваш запит на підключення буде прийнято, будь ласка, зачекайте або перевірте пізніше!</target> <target>Ви будете підключені, коли ваш запит на підключення буде прийнято, будь ласка, зачекайте або перевірте пізніше!</target>
@@ -5696,8 +5333,9 @@ Repeat connection request?</source>
<target>Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі.</target> <target>Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will connect to all group members." xml:space="preserve"> <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
<source>You will connect to all group members.</source> <source>You will join a group this link refers to and connect to its group members.</source>
<target>Ви приєднаєтеся до групи, на яку посилається це посилання, і з'єднаєтеся з її учасниками.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve">
@@ -5765,6 +5403,11 @@ Repeat connection request?</source>
<target>Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її.</target> <target>Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Your chat profile will be sent to group members" xml:space="preserve">
<source>Your chat profile will be sent to group members</source>
<target>Ваш профіль у чаті буде надіслано учасникам групи</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve"> <trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source> <source>Your chat profiles</source>
<target>Ваші профілі чату</target> <target>Ваші профілі чату</target>
@@ -5819,10 +5462,6 @@ You can change it in Settings.</source>
<target>Ваша конфіденційність</target> <target>Ваша конфіденційність</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Your profile" xml:space="preserve">
<source>Your profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve">
<source>Your profile **%@** will be shared.</source> <source>Your profile **%@** will be shared.</source>
<target>Ваш профіль **%@** буде опублікований.</target> <target>Ваш профіль **%@** буде опублікований.</target>
@@ -5915,19 +5554,11 @@ SimpleX servers cannot see your profile.</source>
<target>завжди</target> <target>завжди</target>
<note>pref value</note> <note>pref value</note>
</trans-unit> </trans-unit>
<trans-unit id="and %lld other events" xml:space="preserve">
<source>and %lld other events</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve">
<source>audio call (not e2e encrypted)</source> <source>audio call (not e2e encrypted)</source>
<target>аудіовиклик (без шифрування e2e)</target> <target>аудіовиклик (без шифрування e2e)</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve"> <trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source> <source>bad message ID</source>
<target>невірний ідентифікатор повідомлення</target> <target>невірний ідентифікатор повідомлення</target>
@@ -5938,10 +5569,6 @@ SimpleX servers cannot see your profile.</source>
<target>невірний хеш повідомлення</target> <target>невірний хеш повідомлення</target>
<note>integrity error chat item</note> <note>integrity error chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked" xml:space="preserve">
<source>blocked</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
<source>bold</source> <source>bold</source>
<target>жирний</target> <target>жирний</target>
@@ -6111,10 +5738,6 @@ SimpleX servers cannot see your profile.</source>
<target>видалено</target> <target>видалено</target>
<note>deleted chat item</note> <note>deleted chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="deleted contact" xml:space="preserve">
<source>deleted contact</source>
<note>rcv direct event chat item</note>
</trans-unit>
<trans-unit id="deleted group" xml:space="preserve"> <trans-unit id="deleted group" xml:space="preserve">
<source>deleted group</source> <source>deleted group</source>
<target>видалено групу</target> <target>видалено групу</target>
@@ -6399,8 +6022,7 @@ SimpleX servers cannot see your profile.</source>
<source>off</source> <source>off</source>
<target>вимкнено</target> <target>вимкнено</target>
<note>enabled status <note>enabled status
group pref value group pref value</note>
time to disappear</note>
</trans-unit> </trans-unit>
<trans-unit id="offered %@" xml:space="preserve"> <trans-unit id="offered %@" xml:space="preserve">
<source>offered %@</source> <source>offered %@</source>
@@ -6417,6 +6039,11 @@ SimpleX servers cannot see your profile.</source>
<target>увімкнено</target> <target>увімкнено</target>
<note>group pref value</note> <note>group pref value</note>
</trans-unit> </trans-unit>
<trans-unit id="or chat with the developers" xml:space="preserve">
<source>or chat with the developers</source>
<target>або поспілкуйтеся з розробниками</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="owner" xml:space="preserve"> <trans-unit id="owner" xml:space="preserve">
<source>owner</source> <source>owner</source>
<target>власник</target> <target>власник</target>
@@ -6506,10 +6133,6 @@ SimpleX servers cannot see your profile.</source>
<target>оновлений профіль групи</target> <target>оновлений профіль групи</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve"> <trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source> <source>v%@ (%@)</source>
<target>v%@ (%@)</target> <target>v%@ (%@)</target>
@@ -6647,10 +6270,6 @@ SimpleX servers cannot see your profile.</source>
<target>SimpleX використовує Face ID для локальної автентифікації</target> <target>SimpleX використовує Face ID для локальної автентифікації</target>
<note>Privacy - Face ID Usage Description</note> <note>Privacy - Face ID Usage Description</note>
</trans-unit> </trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX потребує доступу до мікрофона для аудіо та відео дзвінків, а також для запису голосових повідомлень.</target> <target>SimpleX потребує доступу до мікрофона для аудіо та відео дзвінків, а також для запису голосових повідомлень.</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
@@ -216,7 +216,6 @@ func startChat() -> DBMigrationResult? {
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(xftpConfig) try setXFTPConfig(xftpConfig)
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
let justStarted = try apiStartChat() let justStarted = try apiStartChat()
chatStarted = true chatStarted = true
if justStarted { if justStarted {
@@ -352,12 +351,6 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
throw r throw r
} }
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return }
throw r
}
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
guard apiGetActiveUser() != nil else { guard apiGetActiveUser() != nil else {
logger.debug("no active user") logger.debug("no active user")

Some files were not shown because too many files have changed in this diff Show More