Compare commits
218 Commits
_archived-
...
ep/journal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b956f80132 | ||
|
|
34b07d6a3b | ||
|
|
5bdbba1117 | ||
|
|
fad5128a83 | ||
|
|
4fd38a270c | ||
|
|
4cc20a2d32 | ||
|
|
68873464d7 | ||
|
|
c1a0486c1d | ||
|
|
c8c17a2f68 | ||
|
|
9e8084874f | ||
|
|
07173f7b2f | ||
|
|
42458a2715 | ||
|
|
f34bbdbd9c | ||
|
|
9568279b0f | ||
|
|
a7b5dfb74c | ||
|
|
7102723c23 | ||
|
|
4a5fdd3e0e | ||
|
|
4a8da196ad | ||
|
|
743597e848 | ||
|
|
b0f55d6de5 | ||
|
|
1dcd2760b0 | ||
|
|
10f79aae66 | ||
|
|
b58d61c339 | ||
|
|
239765e482 | ||
|
|
f8332bac7f | ||
|
|
ed1eef7362 | ||
|
|
66d8bb94d6 | ||
|
|
6eb09625ab | ||
|
|
0bd59364fd | ||
|
|
795c54343a | ||
|
|
f026a38a75 | ||
|
|
530ec70171 | ||
|
|
1401f56288 | ||
|
|
d172b0cb6d | ||
|
|
3e0b6826bf | ||
|
|
79275424ea | ||
|
|
b25c2e3a09 | ||
|
|
8891314507 | ||
|
|
5c57987e9f | ||
|
|
87d84cfccc | ||
|
|
c090b68bdd | ||
|
|
2219cea026 | ||
|
|
852e77b1d9 | ||
|
|
706d6bf65b | ||
|
|
a02886ca5d | ||
|
|
29c8ab7c9b | ||
|
|
d8d47d706d | ||
|
|
99c458406f | ||
|
|
e4c8386f3f | ||
|
|
9ed31261e1 | ||
|
|
4b6df43e97 | ||
|
|
43b67ba157 | ||
|
|
e6b0983c3e | ||
|
|
c2a320640b | ||
|
|
838751fe78 | ||
|
|
a35dc263b7 | ||
|
|
c609303348 | ||
|
|
07047a3ef3 | ||
|
|
ab290fb068 | ||
|
|
675fc19745 | ||
|
|
8ffe1c23c1 | ||
|
|
5d078bec53 | ||
|
|
b956988a83 | ||
|
|
247f2c9e61 | ||
|
|
7b488c7f1b | ||
|
|
4df8ea2e78 | ||
|
|
8ff6b392c2 | ||
|
|
bca9473d77 | ||
|
|
b03fe183bb | ||
|
|
4ecf94dfad | ||
|
|
a67b79952b | ||
|
|
eb5081624a | ||
|
|
86c2f29920 | ||
|
|
dffbd32c76 | ||
|
|
3ddf7b2680 | ||
|
|
c0e22d74c4 | ||
|
|
d764b3485a | ||
|
|
09e5798d59 | ||
|
|
f31b4e4632 | ||
|
|
d72c04682f | ||
|
|
20995c6912 | ||
|
|
73b3ea3648 | ||
|
|
bc26c23d58 | ||
|
|
4a581cb292 | ||
|
|
ab46cbc5dd | ||
|
|
985b9837c3 | ||
|
|
d50c7ad7f6 | ||
|
|
82faaebb33 | ||
|
|
381346cdba | ||
|
|
7fe940e921 | ||
|
|
76fb5b6dca | ||
|
|
b05a45f559 | ||
|
|
c738c6c522 | ||
|
|
1be70169ba | ||
|
|
34e1e44338 | ||
|
|
303d0eedf5 | ||
|
|
0d8558a6d0 | ||
|
|
91fc238ddc | ||
|
|
cc95fa6b30 | ||
|
|
38be27271f | ||
|
|
da2a94578a | ||
|
|
77db70139b | ||
|
|
fdf3da73aa | ||
|
|
0d93dab692 | ||
|
|
d4cbef1ba1 | ||
|
|
8545a1e8f9 | ||
|
|
157ea59ebb | ||
|
|
7231201c3c | ||
|
|
695d47da2d | ||
|
|
968d8e9c34 | ||
|
|
d72c9a6de0 | ||
|
|
5878d4608c | ||
|
|
b26195e581 | ||
|
|
4d37eff26c | ||
|
|
4d2826f490 | ||
|
|
c4ac5a784f | ||
|
|
d32adf6f6c | ||
|
|
8d6fee89db | ||
|
|
5ce388522e | ||
|
|
70a65e8969 | ||
|
|
1d34500fba | ||
|
|
bc7baf560b | ||
|
|
eb22f32d18 | ||
|
|
497ef087c5 | ||
|
|
c1854b7d50 | ||
|
|
682dfe503c | ||
|
|
957f3b3eb0 | ||
|
|
dea96df27b | ||
|
|
942e5eb8c4 | ||
|
|
ea319313f1 | ||
|
|
bbe329072e | ||
|
|
c64d1e8361 | ||
|
|
7e17ed7b1b | ||
|
|
3c7fc6b0ee | ||
|
|
8709ad6ff3 | ||
|
|
50d624ef6b | ||
|
|
11e448267d | ||
|
|
aacf741ef5 | ||
|
|
420d80ad6c | ||
|
|
343131c64e | ||
|
|
9b107fbdeb | ||
|
|
60d13e258e | ||
|
|
4f42c2b0d8 | ||
|
|
48ae1111a6 | ||
|
|
76dbe32cfc | ||
|
|
120f42cbba | ||
|
|
5f46433f40 | ||
|
|
7b71078c76 | ||
|
|
d6b62d0c18 | ||
|
|
ba4c427bec | ||
|
|
a179154e87 | ||
|
|
5eea3da7f4 | ||
|
|
b3e880ee54 | ||
|
|
08ea5dc2e7 | ||
|
|
20f90ee865 | ||
|
|
5ca2ab6138 | ||
|
|
ba61b15225 | ||
|
|
9ac13a3433 | ||
|
|
b9407c0157 | ||
|
|
5e8cfec653 | ||
|
|
cb10f8b080 | ||
|
|
b7c562fb10 | ||
|
|
2d7655281f | ||
|
|
6de0ed4766 | ||
|
|
5b87206c22 | ||
|
|
be3eb740a7 | ||
|
|
bbd778a6be | ||
|
|
027ce2c559 | ||
|
|
02864a894e | ||
|
|
3419ce293b | ||
|
|
b08768ea71 | ||
|
|
6f481356f7 | ||
|
|
0a2513c9e7 | ||
|
|
ae6996b2ee | ||
|
|
648a9761f9 | ||
|
|
ba71a42aa0 | ||
|
|
f16388323b | ||
|
|
92ac3e2a8a | ||
|
|
52966e7e3d | ||
|
|
f19fae615d | ||
|
|
0dab4491e2 | ||
|
|
1928256b09 | ||
|
|
ed3fb0b222 | ||
|
|
2524609a97 | ||
|
|
0d0097326e | ||
|
|
f1101b09ce | ||
|
|
904b758e79 | ||
|
|
fb1e944744 | ||
|
|
3cd279bd20 | ||
|
|
2e231209d1 | ||
|
|
603e745aa1 | ||
|
|
071d6b3686 | ||
|
|
82e3310c54 | ||
|
|
04770fb30d | ||
|
|
0e5b16498a | ||
|
|
b5e4f127a4 | ||
|
|
8178e8183e | ||
|
|
70fb10189b | ||
|
|
c6dbbcc84e | ||
|
|
7c70577822 | ||
|
|
a1790d6ac0 | ||
|
|
01f99baaac | ||
|
|
75f18bc5f0 | ||
|
|
4b88a2abfd | ||
|
|
614b724602 | ||
|
|
28e1d5fb1b | ||
|
|
fd5b40fd8e | ||
|
|
fedc31c72c | ||
|
|
215020b1df | ||
|
|
5d580c3a1b | ||
|
|
7103524174 | ||
|
|
38ff7d173c | ||
|
|
3cefe19264 | ||
|
|
a2aac72dd1 | ||
|
|
7ef8ae9f42 | ||
|
|
9134624e2a | ||
|
|
fde39e74ee | ||
|
|
761ddac55d |
125
.github/workflows/build.yml
vendored
125
.github/workflows/build.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
- users
|
||||
tags:
|
||||
- "v*"
|
||||
- "!*-fdroid"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -78,10 +79,10 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Haskell
|
||||
uses: haskell/actions/setup@v2
|
||||
uses: haskell-actions/setup@v2
|
||||
with:
|
||||
ghc-version: "8.10.7"
|
||||
cabal-version: "latest"
|
||||
ghc-version: "9.6.2"
|
||||
cabal-version: "3.10.1.0"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
@@ -125,7 +126,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cabal build --enable-tests
|
||||
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
|
||||
path=$(cabal list-bin simplex-chat)
|
||||
echo "bin_path=$path" >> $GITHUB_OUTPUT
|
||||
echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Unix upload CLI binary to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
|
||||
@@ -136,6 +139,16 @@ jobs:
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Unix update CLI binary hash
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
append_body: true
|
||||
body: |
|
||||
${{ steps.unix_cli_build.outputs.bin_hash }}
|
||||
|
||||
- name: Setup Java
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/setup-java@v3
|
||||
@@ -152,7 +165,9 @@ jobs:
|
||||
scripts/desktop/build-lib-linux.sh
|
||||
cd apps/multiplatform
|
||||
./gradlew packageDeb
|
||||
echo "::set-output name=package_path::$(echo $PWD/release/main/deb/simplex_*_amd64.deb)"
|
||||
path=$(echo $PWD/release/main/deb/simplex_*_amd64.deb)
|
||||
echo "package_path=$path" >> $GITHUB_OUTPUT
|
||||
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Linux make AppImage
|
||||
id: linux_appimage_build
|
||||
@@ -160,7 +175,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
scripts/desktop/make-appimage-linux.sh
|
||||
echo "::set-output name=appimage_path::$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage)"
|
||||
path=$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage)
|
||||
echo "appimage_path=$path" >> $GITHUB_OUTPUT
|
||||
echo "appimage_hash=$(echo SHA2-512\(simplex-desktop-x86_64.AppImage\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Mac build desktop
|
||||
id: mac_desktop_build
|
||||
@@ -171,8 +188,10 @@ jobs:
|
||||
APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }}
|
||||
APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }}
|
||||
run: |
|
||||
scripts/desktop/build-desktop-mac-ci.sh
|
||||
echo "::set-output name=package_path::$(echo $PWD/release/main/dmg/SimpleX-*.dmg)"
|
||||
scripts/ci/build-desktop-mac.sh
|
||||
path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg)
|
||||
echo "package_path=$path" >> $GITHUB_OUTPUT
|
||||
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Linux upload desktop package to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
|
||||
@@ -183,6 +202,16 @@ jobs:
|
||||
asset_name: ${{ matrix.desktop_asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Linux update desktop package hash
|
||||
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
append_body: true
|
||||
body: |
|
||||
${{ steps.linux_desktop_build.outputs.package_hash }}
|
||||
|
||||
- name: Linux upload AppImage to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
@@ -192,6 +221,16 @@ jobs:
|
||||
asset_name: simplex-desktop-x86_64.AppImage
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Linux update AppImage hash
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
append_body: true
|
||||
body: |
|
||||
${{ steps.linux_appimage_build.outputs.appimage_hash }}
|
||||
|
||||
- name: Mac upload desktop package to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
@@ -201,6 +240,16 @@ jobs:
|
||||
asset_name: ${{ matrix.desktop_asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Mac update desktop package hash
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest'
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
append_body: true
|
||||
body: |
|
||||
${{ steps.mac_desktop_build.outputs.package_hash }}
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest'
|
||||
timeout-minutes: 30
|
||||
@@ -210,21 +259,22 @@ jobs:
|
||||
# Unix /
|
||||
|
||||
# / Windows
|
||||
|
||||
# * 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
|
||||
# rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing
|
||||
|
||||
- name: Windows build
|
||||
id: windows_build
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: cmd
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf dist-newstyle/src/direct-sq*
|
||||
sed -i "s/, unix /--, unix /" simplex-chat.cabal
|
||||
cabal build --enable-tests
|
||||
cabal list-bin simplex-chat > tmp_bin_path
|
||||
set /p bin_path= < tmp_bin_path
|
||||
echo ::set-output name=bin_path::%bin_path%
|
||||
rm -rf dist-newstyle/src/direct-sq*
|
||||
path=$(cabal list-bin simplex-chat | tail -n 1)
|
||||
echo "bin_path=$path" >> $GITHUB_OUTPUT
|
||||
echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Windows upload binary to release
|
||||
- name: Windows upload CLI binary to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
@@ -233,4 +283,47 @@ jobs:
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Windows update CLI binary hash
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
append_body: true
|
||||
body: |
|
||||
${{ steps.windows_build.outputs.bin_hash }}
|
||||
|
||||
- name: Windows build desktop
|
||||
id: windows_desktop_build
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
env:
|
||||
SIMPLEX_CI_REPO_URL: ${{ secrets.SIMPLEX_CI_REPO_URL }}
|
||||
shell: bash
|
||||
run: |
|
||||
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 /
|
||||
|
||||
1
.github/workflows/web.yml
vendored
1
.github/workflows/web.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
- website/**
|
||||
- images/**
|
||||
- blog/**
|
||||
- docs/**
|
||||
- .github/workflows/web.yml
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -8,12 +8,12 @@ RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/
|
||||
chmod +x /usr/bin/ghcup
|
||||
|
||||
# Install ghc
|
||||
RUN ghcup install ghc 8.10.7
|
||||
RUN ghcup install ghc 9.6.2
|
||||
# Install cabal
|
||||
RUN ghcup install cabal
|
||||
RUN ghcup install cabal 3.10.1.0
|
||||
# Set both as default
|
||||
RUN ghcup set ghc 8.10.7 && \
|
||||
ghcup set cabal
|
||||
RUN ghcup set ghc 9.6.2 && \
|
||||
ghcup set cabal 3.10.1.0
|
||||
|
||||
COPY . /project
|
||||
WORKDIR /project
|
||||
|
||||
45
README.md
45
README.md
@@ -119,19 +119,22 @@ 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|
|
||||
|:----:|:-------:|:---------:|:---------:|:---------:|:---------:|
|
||||
|🇬🇧 en|English | |✓|✓|✓|✓|
|
||||
|ar|العربية |[jermanuts](https://github.com/jermanuts)||[](https://hosted.weblate.org/projects/simplex-chat/website/ar/)||
|
||||
|🇧🇬 bg|Български |-|[](https://hosted.weblate.org/projects/simplex-chat/android/bg/)<br>-|||
|
||||
|ar|العربية |[jermanuts](https://github.com/jermanuts)|[](https://hosted.weblate.org/projects/simplex-chat/android/ar/)<br>-|[](https://hosted.weblate.org/projects/simplex-chat/website/ar/)||
|
||||
|🇧🇬 bg|Български | |[](https://hosted.weblate.org/projects/simplex-chat/android/bg/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/bg/)|||
|
||||
|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[](https://hosted.weblate.org/projects/simplex-chat/website/de/)||
|
||||
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|
||||
|🇫🇮 fi|Suomi | |[](https://hosted.weblate.org/projects/simplex-chat/android/fi/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fi/)|[](https://hosted.weblate.org/projects/simplex-chat/website/fi/)||
|
||||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|
||||
|🇮🇱 he|עִברִית | |[](https://hosted.weblate.org/projects/simplex-chat/android/he/)<br>-|||
|
||||
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|
||||
|🇯🇵 ja|Japanese ||[](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|||
|
||||
|🇯🇵 ja|日本語 | |[](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|[](https://hosted.weblate.org/projects/simplex-chat/website/ja/)||
|
||||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|
||||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[](https://hosted.weblate.org/projects/simplex-chat/android/pl/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|
||||
|🇧🇷 pt-BR|Português||[](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|
||||
|🇷🇺 ru|Русский ||[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|
||||
|🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[](https://hosted.weblate.org/projects/simplex-chat/android/th/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/th/)|||
|
||||
|🇺🇦 uk|Українська| |[](https://hosted.weblate.org/projects/simplex-chat/android/uk/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br> |<br><br>[](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!
|
||||
@@ -227,24 +230,18 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
## News and updates
|
||||
|
||||
Recent updates:
|
||||
Recent and important updates:
|
||||
|
||||
[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).
|
||||
|
||||
[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).
|
||||
|
||||
[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).
|
||||
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
@@ -290,18 +287,20 @@ 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.
|
||||
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
|
||||
5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
|
||||
6. 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.
|
||||
6. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
|
||||
7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
|
||||
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
|
||||
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
|
||||
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
|
||||
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
|
||||
12. Manual messaging queue rotations to move conversation to another SMP relay.
|
||||
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
|
||||
14. Local files encryption, except videos (to be added later).
|
||||
|
||||
We plan to add:
|
||||
|
||||
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. 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. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
|
||||
2. Post-quantum resistant key exchange in double ratchet protocol.
|
||||
3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
|
||||
5. Reproducible builds – this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
|
||||
@@ -365,22 +364,26 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
||||
- ✅ Message editing history
|
||||
- ✅ Reduced battery and traffic usage in large groups.
|
||||
- ✅ 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.
|
||||
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- Post-quantum resistant key exchange in double ratchet protocol.
|
||||
- Large groups, communities and public channels.
|
||||
- Privacy & security slider - a simple way to set all settings at once.
|
||||
- Improve sending videos (including encryption of locally stored videos).
|
||||
- Improve experience for the new users.
|
||||
- SMP queue redundancy and rotation (manual is supported).
|
||||
- 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.).
|
||||
- Large groups, communities and public channels.
|
||||
- Feeds/broadcasts.
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Privately share your location.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- 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:
|
||||
- keep all your contacts and groups even if you lose the domain.
|
||||
- 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.
|
||||
|
||||
## Disclaimers
|
||||
|
||||
@@ -55,7 +55,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
|
||||
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
logger.debug("AppDelegate: didReceiveRemoteNotification")
|
||||
print("*** userInfo", userInfo)
|
||||
let m = ChatModel.shared
|
||||
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
|
||||
m.notificationMode != .off {
|
||||
|
||||
@@ -31,11 +31,11 @@ struct ContentView: View {
|
||||
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
||||
|
||||
private enum ChatListActionSheet: Identifiable {
|
||||
case connectViaUrl(action: ConnReqType, link: String)
|
||||
case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .connectViaUrl: return "connectViaUrl \(link)"
|
||||
case let .planAndConnectSheet(sheet): return sheet.id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ struct ContentView: View {
|
||||
mainView()
|
||||
.actionSheet(item: $chatListActionSheet) { sheet in
|
||||
switch sheet {
|
||||
case let .connectViaUrl(action, link): return connectViaUrlSheet(action, link)
|
||||
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -285,36 +285,30 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func connectViaUrl() {
|
||||
let m = ChatModel.shared
|
||||
if let url = m.appOpenUrl {
|
||||
m.appOpenUrl = nil
|
||||
var path = url.path
|
||||
logger.debug("ContentView.connectViaUrl path: \(path)")
|
||||
if (path == "/contact" || path == "/invitation") {
|
||||
path.removeFirst()
|
||||
let action: ConnReqType = path == "contact" ? .contact : .invitation
|
||||
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
||||
chatListActionSheet = .connectViaUrl(action: action, link: link)
|
||||
} else {
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid")))
|
||||
dismissAllSheets() {
|
||||
let m = ChatModel.shared
|
||||
if let url = m.appOpenUrl {
|
||||
m.appOpenUrl = nil
|
||||
var path = url.path
|
||||
if (path == "/contact" || path == "/invitation") {
|
||||
path.removeFirst()
|
||||
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
||||
planAndConnect(
|
||||
link,
|
||||
showAlert: showPlanAndConnectAlert,
|
||||
showActionSheet: { chatListActionSheet = .planAndConnectSheet(sheet: $0) },
|
||||
dismiss: false,
|
||||
incognito: nil
|
||||
)
|
||||
} else {
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func connectViaUrlSheet(_ action: ConnReqType, _ link: String) -> ActionSheet {
|
||||
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()
|
||||
]
|
||||
)
|
||||
private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) {
|
||||
AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,9 @@ final class ChatModel: ObservableObject {
|
||||
// current chat
|
||||
@Published var chatId: String?
|
||||
@Published var reversedChatItems: [ChatItem] = []
|
||||
var chatItemStatuses: Dictionary<Int64, CIStatus> = [:]
|
||||
@Published var chatToTop: String?
|
||||
@Published var groupMembers: [GroupMember] = []
|
||||
@Published var groupMembers: [GMember] = []
|
||||
// items in the terminal view
|
||||
@Published var showingTerminal = false
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@@ -152,6 +153,20 @@ 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? {
|
||||
chats.firstIndex(where: { $0.id == id })
|
||||
}
|
||||
@@ -165,6 +180,7 @@ final class ChatModel: ObservableObject {
|
||||
func updateChatInfo(_ cInfo: ChatInfo) {
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
chats[i].chatInfo = cInfo
|
||||
chats[i].created = Date.now
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +312,11 @@ final class ChatModel: ObservableObject {
|
||||
return false
|
||||
} else {
|
||||
withAnimation(itemAnimation()) {
|
||||
reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
|
||||
var ci = cItem
|
||||
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
|
||||
ci.meta.itemStatus = status
|
||||
}
|
||||
reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -309,26 +329,22 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
withAnimation {
|
||||
_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) {
|
||||
let ci = reversedChatItems[i]
|
||||
reversedChatItems[i] = cItem
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private func getChatItemIndex(_ cItem: ChatItem) -> Int? {
|
||||
func getChatItemIndex(_ cItem: ChatItem) -> Int? {
|
||||
reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
||||
}
|
||||
|
||||
@@ -464,6 +480,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
// clear current chat
|
||||
if chatId == cInfo.id {
|
||||
chatItemStatuses = [:]
|
||||
reversedChatItems = []
|
||||
}
|
||||
}
|
||||
@@ -516,27 +533,62 @@ final class ChatModel: ObservableObject {
|
||||
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
|
||||
}
|
||||
|
||||
func getConnectedMemberNames(_ ci: ChatItem) -> [String] {
|
||||
guard var i = getChatItemIndex(ci) else { return [] }
|
||||
// this function analyses "connected" events and assumes that each member will be there only once
|
||||
func getConnectedMemberNames(_ chatItem: ChatItem) -> (Int, [String]) {
|
||||
var count = 0
|
||||
var ns: [String] = []
|
||||
while i < reversedChatItems.count, let m = reversedChatItems[i].memberConnected {
|
||||
ns.append(m.displayName)
|
||||
i += 1
|
||||
if let ciCategory = chatItem.mergeCategory,
|
||||
var i = getChatItemIndex(chatItem) {
|
||||
while i < reversedChatItems.count {
|
||||
let ci = reversedChatItems[i]
|
||||
if ci.mergeCategory != ciCategory { break }
|
||||
if let m = ci.memberConnected {
|
||||
ns.append(m.displayName)
|
||||
}
|
||||
count += 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
return ns
|
||||
return (count, ns)
|
||||
}
|
||||
|
||||
func getChatItemNeighbors(_ ci: ChatItem) -> (ChatItem?, ChatItem?) {
|
||||
if let i = getChatItemIndex(ci) {
|
||||
return (
|
||||
i + 1 < reversedChatItems.count ? reversedChatItems[i + 1] : nil,
|
||||
i - 1 >= 0 ? reversedChatItems[i - 1] : nil
|
||||
)
|
||||
// returns the index of the passed item and the next item (it has smaller index)
|
||||
func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) {
|
||||
if let i = getChatItemIndex(ci) {
|
||||
(i, i > 0 ? reversedChatItems[i - 1] : nil)
|
||||
} else {
|
||||
return (nil, nil)
|
||||
(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) {
|
||||
if let i = getChatIndex(id) {
|
||||
popChat_(i)
|
||||
@@ -563,10 +615,6 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func getGroupMember(_ groupMemberId: Int64) -> GroupMember? {
|
||||
groupMembers.first(where: { $0.groupMemberId == groupMemberId })
|
||||
}
|
||||
|
||||
func upsertGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) -> Bool {
|
||||
// user member was updated
|
||||
if groupInfo.membership.groupMemberId == member.groupMemberId {
|
||||
@@ -575,13 +623,14 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
// update current chat
|
||||
if chatId == groupInfo.id {
|
||||
if let i = groupMembers.firstIndex(where: { $0.id == member.id }) {
|
||||
if let i = groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) {
|
||||
withAnimation(.default) {
|
||||
self.groupMembers[i] = member
|
||||
self.groupMembers[i].wrapped = member
|
||||
self.groupMembers[i].created = Date.now
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
withAnimation { groupMembers.append(member) }
|
||||
withAnimation { groupMembers.append(GMember(member)) }
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
@@ -590,11 +639,10 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateGroupMemberConnectionStats(_ groupInfo: GroupInfo, _ member: GroupMember, _ connectionStats: ConnectionStats) {
|
||||
if let conn = member.activeConn {
|
||||
var updatedConn = conn
|
||||
updatedConn.connectionStats = connectionStats
|
||||
if var conn = member.activeConn {
|
||||
conn.connectionStats = connectionStats
|
||||
var updatedMember = member
|
||||
updatedMember.activeConn = updatedConn
|
||||
updatedMember.activeConn = conn
|
||||
_ = upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
@@ -693,40 +741,18 @@ final class Chat: ObservableObject, Identifiable {
|
||||
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
}
|
||||
|
||||
enum NetworkStatus: Decodable, Equatable {
|
||||
case unknown
|
||||
case connected
|
||||
case disconnected
|
||||
case error(String)
|
||||
final class GMember: ObservableObject, Identifiable {
|
||||
@Published var wrapped: GroupMember
|
||||
var created = Date.now
|
||||
|
||||
var statusString: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "connected"
|
||||
case .error: return "error"
|
||||
default: return "connecting"
|
||||
}
|
||||
}
|
||||
init(_ member: GroupMember) {
|
||||
self.wrapped = member
|
||||
}
|
||||
|
||||
var statusExplanation: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "You are connected to the server used to receive messages from this contact."
|
||||
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
|
||||
default: return "Trying to connect to the server used to receive messages from this contact."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String {
|
||||
get {
|
||||
switch self {
|
||||
case .unknown: return "circle.dotted"
|
||||
case .connected: return "circle.fill"
|
||||
case .disconnected: return "ellipsis.circle.fill"
|
||||
case .error: return "exclamationmark.circle.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
var id: String { wrapped.id }
|
||||
var groupId: Int64 { wrapped.groupId }
|
||||
var groupMemberId: Int64 { wrapped.groupMemberId }
|
||||
var displayName: String { wrapped.displayName }
|
||||
var viewId: String { get { "\(wrapped.id) \(created.timeIntervalSince1970)" } }
|
||||
static let sampleData = GMember(GroupMember.sampleData)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ import SimpleXChat
|
||||
|
||||
private var chatController: chat_ctrl?
|
||||
|
||||
// currentChatVersion in core
|
||||
public let CURRENT_CHAT_VERSION: Int = 2
|
||||
|
||||
// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core)
|
||||
public let CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion: 2, maxVersion: CURRENT_CHAT_VERSION)
|
||||
|
||||
enum TerminalItem: Identifiable {
|
||||
case cmd(Date, ChatCommand)
|
||||
case resp(Date, ChatResponse)
|
||||
@@ -251,6 +257,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
|
||||
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 {
|
||||
try await sendCommandOkResp(.apiExportArchive(config: config))
|
||||
}
|
||||
@@ -300,6 +312,7 @@ func loadChat(chat: Chat, search: String = "") {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
let m = ChatModel.shared
|
||||
m.chatItemStatuses = [:]
|
||||
m.reversedChatItems = []
|
||||
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search)
|
||||
m.updateChatInfo(chat.chatInfo)
|
||||
@@ -315,18 +328,34 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSendMessage(sendRef: SendRef, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
|
||||
func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
|
||||
let chatModel = ChatModel.shared
|
||||
let cmd: ChatCommand = .apiSendMessage(sendRef: sendRef, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
|
||||
switch sendRef {
|
||||
case .direct:
|
||||
let cItem = await sendMessageDirect(cmd)
|
||||
return cItem
|
||||
case .group(_, .some(_)):
|
||||
let cItem = await sendMessageDirect(cmd)
|
||||
return cItem
|
||||
case .group(_, .none):
|
||||
let r = await chatSendCmd(cmd, bgDelay: msgDelay)
|
||||
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
|
||||
let r: ChatResponse
|
||||
if type == .direct {
|
||||
var cItem: ChatItem? = nil
|
||||
let endTask = beginBGTask({
|
||||
if let cItem = cItem {
|
||||
DispatchQueue.main.async {
|
||||
chatModel.messageDelivery.removeValue(forKey: cItem.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
r = await chatSendCmd(cmd, bgTask: false)
|
||||
if case let .newChatItem(_, aChatItem) = r {
|
||||
cItem = aChatItem.chatItem
|
||||
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
|
||||
return cItem
|
||||
}
|
||||
if let networkErrorAlert = networkErrorAlert(r) {
|
||||
AlertManager.shared.showAlert(networkErrorAlert)
|
||||
} else {
|
||||
sendMessageErrorAlert(r)
|
||||
}
|
||||
endTask()
|
||||
return nil
|
||||
} else {
|
||||
r = await chatSendCmd(cmd, bgDelay: msgDelay)
|
||||
if case let .newChatItem(_, aChatItem) = r {
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
@@ -335,31 +364,6 @@ func apiSendMessage(sendRef: SendRef, file: CryptoFile?, quotedItemId: Int64?, m
|
||||
}
|
||||
}
|
||||
|
||||
private func sendMessageDirect(_ cmd: ChatCommand) async -> ChatItem? {
|
||||
let chatModel = ChatModel.shared
|
||||
var cItem: ChatItem? = nil
|
||||
let endTask = beginBGTask({
|
||||
if let cItem = cItem {
|
||||
DispatchQueue.main.async {
|
||||
chatModel.messageDelivery.removeValue(forKey: cItem.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
let r = await chatSendCmd(cmd, bgTask: false)
|
||||
if case let .newChatItem(_, aChatItem) = r {
|
||||
cItem = aChatItem.chatItem
|
||||
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
|
||||
return cItem
|
||||
}
|
||||
if let networkErrorAlert = networkErrorAlert(r) {
|
||||
AlertManager.shared.showAlert(networkErrorAlert)
|
||||
} else {
|
||||
sendMessageErrorAlert(r)
|
||||
}
|
||||
endTask()
|
||||
return nil
|
||||
}
|
||||
|
||||
private func sendMessageErrorAlert(_ r: ChatResponse) {
|
||||
logger.error("apiSendMessage error: \(String(describing: r))")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -498,6 +502,10 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
|
||||
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?) {
|
||||
let r = await chatSendCmd(.apiContactInfo(contactId: contactId))
|
||||
if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
|
||||
@@ -589,6 +597,14 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
|
||||
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? {
|
||||
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
|
||||
if let alert = alert {
|
||||
@@ -613,10 +629,7 @@ func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert
|
||||
if let c = m.getContactChat(contact.contactId) {
|
||||
await MainActor.run { m.chatId = c.id }
|
||||
}
|
||||
let alert = mkAlert(
|
||||
title: "Contact already exists",
|
||||
message: "You are already connected to \(contact.displayName)."
|
||||
)
|
||||
let alert = contactAlreadyExistsAlert(contact)
|
||||
return (nil, alert)
|
||||
case .chatCmdError(_, .error(.invalidConnReq)):
|
||||
let alert = mkAlert(
|
||||
@@ -644,6 +657,13 @@ func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, 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 {
|
||||
if let networkErrorAlert = networkErrorAlert(r) {
|
||||
return networkErrorAlert
|
||||
@@ -655,18 +675,18 @@ private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
|
||||
}
|
||||
}
|
||||
|
||||
func apiDeleteChat(type: ChatType, id: Int64) async throws {
|
||||
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false)
|
||||
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 .contactConnection = type, case .contactConnectionDeleted = r { return }
|
||||
if case .group = type, case .groupDeletedUser = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func deleteChat(_ chat: Chat) async {
|
||||
func deleteChat(_ chat: Chat, notify: Bool? = nil) async {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId)
|
||||
try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, notify: notify)
|
||||
DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) }
|
||||
} catch let error {
|
||||
logger.error("deleteChat apiDeleteChat error: \(responseError(error))")
|
||||
@@ -704,8 +724,9 @@ func apiUpdateProfile(profile: Profile) async throws -> (Profile, [Contact])? {
|
||||
let userId = try currentUserId("apiUpdateProfile")
|
||||
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
|
||||
switch r {
|
||||
case .userProfileNoChange: return nil
|
||||
case .userProfileNoChange: return (profile, [])
|
||||
case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts)
|
||||
case .chatCmdError(_, .errorStore(.duplicateName)): return nil;
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
@@ -947,6 +968,12 @@ 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 {
|
||||
do {
|
||||
if chat.chatStats.unreadCount > 0 {
|
||||
@@ -994,9 +1021,9 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
|
||||
func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInfo {
|
||||
let userId = try currentUserId("apiNewGroup")
|
||||
let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
|
||||
let r = chatSendCmdSync(.apiNewGroup(userId: userId, incognito: incognito, groupProfile: groupProfile))
|
||||
if case let .groupCreated(_, groupInfo) = r { return groupInfo }
|
||||
throw r
|
||||
}
|
||||
@@ -1056,8 +1083,8 @@ func apiListMembers(_ groupId: Int64) async -> [GroupMember] {
|
||||
return []
|
||||
}
|
||||
|
||||
func filterMembersToAdd(_ ms: [GroupMember]) -> [Contact] {
|
||||
let memberContactIds = ms.compactMap{ m in m.memberCurrent ? m.memberContactId : nil }
|
||||
func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
|
||||
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
|
||||
return ChatModel.shared.chats
|
||||
.compactMap{ $0.chatInfo.contact }
|
||||
.filter{ !memberContactIds.contains($0.apiId) }
|
||||
@@ -1099,6 +1126,18 @@ func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiCreateMemberContact(_ groupId: Int64, _ groupMemberId: Int64) async throws -> Contact {
|
||||
let r = await chatSendCmd(.apiCreateMemberContact(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .newMemberContact(_, contact, _, _) = r { return contact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async throws -> Contact {
|
||||
let r = await chatSendCmd(.apiSendMemberContactInvitation(contactId: contactId, msg: msg), bgDelay: msgDelay)
|
||||
if case let .newMemberContactSentInv(_, contact, _, _) = r { return contact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetVersion() throws -> CoreVersionInfo {
|
||||
let r = chatSendCmdSync(.showVersion)
|
||||
if case let .versionInfo(info, _, _) = r { return info }
|
||||
@@ -1124,6 +1163,7 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
|
||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try setXFTPConfig(getXFTPCfg())
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
m.chatInitialized = true
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
if m.currentUser == nil {
|
||||
@@ -1276,6 +1316,12 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
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, _):
|
||||
if active(user) && contact.directOrUsed {
|
||||
await MainActor.run {
|
||||
@@ -1320,6 +1366,12 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
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):
|
||||
if active(user) && m.hasChat(mergedContact.id) {
|
||||
await MainActor.run {
|
||||
@@ -1333,13 +1385,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
await updateContactsStatus(contactRefs, status: .connected)
|
||||
case let .contactsDisconnected(_, contactRefs):
|
||||
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):
|
||||
await MainActor.run {
|
||||
for sub in contactSubscriptions {
|
||||
@@ -1354,6 +1399,18 @@ 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):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
@@ -1375,11 +1432,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
case let .chatItemStatusUpdated(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if !cItem.isDeletedContent {
|
||||
let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true
|
||||
if added && cItem.showNotification {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
if !cItem.isDeletedContent && active(user) {
|
||||
await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
|
||||
}
|
||||
if let endTask = m.messageDelivery[cItem.id] {
|
||||
switch cItem.meta.itemStatus {
|
||||
@@ -1431,6 +1485,16 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
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):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
@@ -1490,10 +1554,17 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
m.updateGroup(toGroup)
|
||||
}
|
||||
}
|
||||
case let .memberRole(user, groupInfo, _, _, _, _):
|
||||
case let .memberRole(user, groupInfo, byMember: _, member: member, fromRole: _, toRole: _):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroup(groupInfo)
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
case let .newMemberContactReceivedInv(user, contact, _, _):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
}
|
||||
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
|
||||
@@ -1628,7 +1699,7 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
|
||||
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
|
||||
default: err = String(describing: chatError)
|
||||
}
|
||||
m.setContactNetworkStatus(contact, .error(err))
|
||||
m.setContactNetworkStatus(contact, .error(connectionError: err))
|
||||
}
|
||||
|
||||
func refreshCallInvitations() throws {
|
||||
|
||||
@@ -19,7 +19,11 @@ let bgSuspendTimeout: Int = 5 // seconds
|
||||
let terminationTimeout: Int = 3 // seconds
|
||||
|
||||
private func _suspendChat(timeout: Int) {
|
||||
if ChatModel.ok {
|
||||
// this is a redundant check to prevent logical errors, like the one fixed in this PR
|
||||
let state = appStateGroupDefault.get()
|
||||
if !state.canSuspend {
|
||||
logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)")
|
||||
} else if ChatModel.ok {
|
||||
appStateGroupDefault.set(.suspending)
|
||||
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
|
||||
let endTask = beginBGTask(chatSuspended)
|
||||
@@ -31,9 +35,7 @@ private func _suspendChat(timeout: Int) {
|
||||
|
||||
func suspendChat() {
|
||||
suspendLockQueue.sync {
|
||||
if appStateGroupDefault.get() != .stopped {
|
||||
_suspendChat(timeout: appSuspendTimeout)
|
||||
}
|
||||
_suspendChat(timeout: appSuspendTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +47,25 @@ func suspendBgRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
private var terminating = false
|
||||
|
||||
func terminateChat() {
|
||||
logger.debug("terminateChat")
|
||||
suspendLockQueue.sync {
|
||||
switch appStateGroupDefault.get() {
|
||||
case .suspending:
|
||||
// suspend instantly if already suspending
|
||||
_chatSuspended()
|
||||
// when apiSuspendChat is called with timeout 0, it won't send any events on suspension
|
||||
if ChatModel.ok { apiSuspendChat(timeoutMicroseconds: 0) }
|
||||
case .stopped: ()
|
||||
chatCloseStore()
|
||||
case .suspended:
|
||||
chatCloseStore()
|
||||
case .stopped:
|
||||
chatCloseStore()
|
||||
default:
|
||||
terminating = true
|
||||
// the store will be closed in _chatSuspended when event is received
|
||||
_suspendChat(timeout: terminationTimeout)
|
||||
}
|
||||
}
|
||||
@@ -73,10 +85,14 @@ private func _chatSuspended() {
|
||||
if ChatModel.shared.chatRunning == true {
|
||||
ChatReceiver.shared.stop()
|
||||
}
|
||||
if terminating {
|
||||
chatCloseStore()
|
||||
}
|
||||
}
|
||||
|
||||
func activateChat(appState: AppState = .active) {
|
||||
logger.debug("DEBUGGING: activateChat")
|
||||
terminating = false
|
||||
suspendLockQueue.sync {
|
||||
appStateGroupDefault.set(appState)
|
||||
if ChatModel.ok { apiActivateChat() }
|
||||
@@ -85,6 +101,7 @@ func activateChat(appState: AppState = .active) {
|
||||
}
|
||||
|
||||
func initChatAndMigrate(refreshInvitations: Bool = true) {
|
||||
terminating = false
|
||||
let m = ChatModel.shared
|
||||
if (!m.chatInitialized) {
|
||||
do {
|
||||
@@ -97,6 +114,7 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
|
||||
}
|
||||
|
||||
func startChatAndActivate() {
|
||||
terminating = false
|
||||
logger.debug("DEBUGGING: startChatAndActivate")
|
||||
if ChatModel.shared.chatRunning == true {
|
||||
ChatReceiver.shared.start()
|
||||
|
||||
@@ -108,7 +108,6 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
try audioSession.setActive(true)
|
||||
logger.debug("audioSession activated")
|
||||
} catch {
|
||||
print(error)
|
||||
logger.error("failed activating audio session")
|
||||
}
|
||||
}
|
||||
@@ -121,7 +120,6 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
try audioSession.setActive(false)
|
||||
logger.debug("audioSession deactivated")
|
||||
} catch {
|
||||
print(error)
|
||||
logger.error("failed deactivating audio session")
|
||||
}
|
||||
suspendOnEndCall()
|
||||
|
||||
@@ -99,12 +99,12 @@ struct ChatInfoView: View {
|
||||
@Binding var connectionCode: String?
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
@State private var alert: ChatInfoViewAlert? = nil
|
||||
@State private var showDeleteContactActionSheet = false
|
||||
@State private var sendReceipts = SendReceipts.userDefault(true)
|
||||
@State private var sendReceiptsUserDefault = true
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
enum ChatInfoViewAlert: Identifiable {
|
||||
case deleteContactAlert
|
||||
case clearChatAlert
|
||||
case networkStatusAlert
|
||||
case switchAddressAlert
|
||||
@@ -114,7 +114,6 @@ struct ChatInfoView: View {
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .deleteContactAlert: return "deleteContactAlert"
|
||||
case .clearChatAlert: return "clearChatAlert"
|
||||
case .networkStatusAlert: return "networkStatusAlert"
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
@@ -164,12 +163,13 @@ struct ChatInfoView: View {
|
||||
// synchronizeConnectionButtonForce()
|
||||
// }
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
|
||||
if let contactLink = contact.contactLink {
|
||||
Section {
|
||||
QRCode(uri: contactLink)
|
||||
SimpleXLinkQRCode(uri: contactLink)
|
||||
Button {
|
||||
showShareSheet(items: [contactLink])
|
||||
showShareSheet(items: [simplexChatLink(contactLink)])
|
||||
} label: {
|
||||
Label("Share address", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
@@ -180,30 +180,32 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("Servers") {
|
||||
networkStatusRow()
|
||||
.onTapGesture {
|
||||
alert = .networkStatusAlert
|
||||
}
|
||||
if let connStats = connectionStats {
|
||||
Button("Change receiving address") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
|
||||
Button("Abort changing address") {
|
||||
alert = .abortSwitchAddressAlert
|
||||
if contact.ready && contact.active {
|
||||
Section("Servers") {
|
||||
networkStatusRow()
|
||||
.onTapGesture {
|
||||
alert = .networkStatusAlert
|
||||
}
|
||||
if let connStats = connectionStats {
|
||||
Button("Change receiving address") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
|
||||
Button("Abort changing address") {
|
||||
alert = .abortSwitchAddressAlert
|
||||
}
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +232,6 @@ struct ChatInfoView: View {
|
||||
}
|
||||
.alert(item: $alert) { alertItem in
|
||||
switch(alertItem) {
|
||||
case .deleteContactAlert: return deleteContactAlert()
|
||||
case .clearChatAlert: return clearChatAlert()
|
||||
case .networkStatusAlert: return networkStatusAlert()
|
||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||
@@ -239,6 +240,26 @@ struct ChatInfoView: View {
|
||||
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 {
|
||||
@@ -411,7 +432,7 @@ struct ChatInfoView: View {
|
||||
|
||||
private func deleteContactButton() -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .deleteContactAlert
|
||||
showDeleteContactActionSheet = true
|
||||
} label: {
|
||||
Label("Delete contact", systemImage: "trash")
|
||||
.foregroundColor(Color.red)
|
||||
@@ -427,30 +448,23 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteContactAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Delete contact?"),
|
||||
message: Text("Contact and all messages will be deleted - this cannot be undone!"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId)
|
||||
await MainActor.run {
|
||||
chatModel.removeChat(chat.chatInfo.id)
|
||||
chatModel.chatId = nil
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error deleting contact")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
private func deleteContact(notify: Bool? = nil) {
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, notify: notify)
|
||||
await MainActor.run {
|
||||
dismiss()
|
||||
chatModel.chatId = nil
|
||||
chatModel.removeChat(chat.chatInfo.id)
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
} catch let error {
|
||||
logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error deleting contact")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func clearChatAlert() -> Alert {
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CICallItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var chatInfo: ChatInfo
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var status: CICallStatus
|
||||
var duration: Int
|
||||
@@ -60,7 +60,7 @@ struct CICallItemView: View {
|
||||
|
||||
|
||||
@ViewBuilder private func acceptCallButton() -> some View {
|
||||
if case let .direct(contact) = chatInfo {
|
||||
if case let .direct(contact) = chat.chatInfo {
|
||||
Button {
|
||||
if let invitation = m.callInvitations[contact.id] {
|
||||
CallController.shared.answerCall(invitation: invitation)
|
||||
|
||||
@@ -10,20 +10,92 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIChatFeatureView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
var feature: Feature
|
||||
var icon: String? = nil
|
||||
var iconColor: Color
|
||||
|
||||
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) {
|
||||
Image(systemName: icon ?? feature.iconFilled)
|
||||
.foregroundColor(iconColor)
|
||||
.scaleEffect(feature.iconScale)
|
||||
chatEventText(chatItem)
|
||||
}
|
||||
.padding(.leading, 6)
|
||||
.padding(.bottom, 6)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 4)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
}
|
||||
@@ -31,6 +103,6 @@ struct CIChatFeatureView: View {
|
||||
struct CIChatFeatureView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let enabled = FeatureEnabled(forUser: false, forContact: false)
|
||||
CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
|
||||
CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,9 @@ struct CIEventView: View {
|
||||
var eventText: Text
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
eventText
|
||||
}
|
||||
.padding(.leading, 6)
|
||||
.padding(.bottom, 6)
|
||||
eventText
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 4)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIFeaturePreferenceView: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var feature: ChatFeature
|
||||
var allowed: FeatureAllowed
|
||||
@@ -80,7 +80,6 @@ struct CIFeaturePreferenceView_Previews: PreviewProvider {
|
||||
quotedItem: nil,
|
||||
file: nil
|
||||
)
|
||||
CIFeaturePreferenceView(chatItem: chatItem, feature: ChatFeature.timedMessages, allowed: .yes, param: 30)
|
||||
.environmentObject(Chat.sampleData)
|
||||
CIFeaturePreferenceView(chat: Chat.sampleData, chatItem: chatItem, feature: ChatFeature.timedMessages, allowed: .yes, param: 30)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIFileView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let file: CIFile?
|
||||
let edited: Bool
|
||||
@@ -83,8 +84,8 @@ struct CIFileView: View {
|
||||
if fileSizeValid() {
|
||||
Task {
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
let encrypted = file.fileProtocol == .xftp && privacyEncryptLocalFilesGroupDefault.get()
|
||||
if let user = m.currentUser {
|
||||
let encrypted = privacyEncryptLocalFilesGroupDefault.get()
|
||||
await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted)
|
||||
}
|
||||
}
|
||||
@@ -234,18 +235,17 @@ struct CIFileView_Previews: PreviewProvider {
|
||||
file: nil
|
||||
)
|
||||
Group {
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentFile, revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(), 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(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", 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(chatInfo: ChatInfo.sampleData.direct, chatItem: fileChatItemWtFile, revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: sentFile, revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, revealed: Binding.constant(false))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 360))
|
||||
.environmentObject(Chat.sampleData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,34 +17,45 @@ struct CIGroupInvitationView: View {
|
||||
var memberRole: GroupMemberRole
|
||||
var chatIncognito: Bool = false
|
||||
@State private var frameWidth: CGFloat = 0
|
||||
@State private var inProgress = false
|
||||
@State private var progressByTimeout = false
|
||||
|
||||
var body: some View {
|
||||
let action = !chatItem.chatDir.sent && groupInvitation.status == .pending
|
||||
let v = ZStack(alignment: .bottomTrailing) {
|
||||
VStack(alignment: .leading) {
|
||||
groupInfoView(action)
|
||||
.padding(.horizontal, 2)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
ZStack {
|
||||
VStack(alignment: .leading) {
|
||||
groupInfoView(action)
|
||||
.padding(.horizontal, 2)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
|
||||
Divider().frame(width: frameWidth)
|
||||
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())
|
||||
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
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -55,11 +66,24 @@ struct CIGroupInvitationView: View {
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.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 {
|
||||
v.onTapGesture {
|
||||
joinGroup(groupInvitation.groupId)
|
||||
inProgress = true
|
||||
joinGroup(groupInvitation.groupId) {
|
||||
await MainActor.run { inProgress = false }
|
||||
}
|
||||
}
|
||||
.disabled(inProgress)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
@@ -67,7 +91,7 @@ struct CIGroupInvitationView: View {
|
||||
|
||||
private func groupInfoView(_ action: Bool) -> some View {
|
||||
var color: Color
|
||||
if action {
|
||||
if action && !inProgress {
|
||||
color = chatIncognito ? .indigo : .accentColor
|
||||
} else {
|
||||
color = Color(uiColor: .tertiaryLabel)
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIImageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let chatItem: ChatItem
|
||||
let image: String
|
||||
@@ -36,7 +37,7 @@ struct CIImageView: View {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation:
|
||||
Task {
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// CIMemberCreatedContactView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 19.09.2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIMemberCreatedContactView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
switch chatItem.chatDir {
|
||||
case let .groupRcv(groupMember):
|
||||
if let contactId = groupMember.memberContactId {
|
||||
memberCreatedContactView(openText: "Open")
|
||||
.onTapGesture {
|
||||
dismissAllSheets(animated: true)
|
||||
DispatchQueue.main.async {
|
||||
m.chatId = "@\(contactId)"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memberCreatedContactView()
|
||||
}
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.padding(.leading, 6)
|
||||
.padding(.bottom, 6)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
private func memberCreatedContactView(openText: LocalizedStringKey? = nil) -> some View {
|
||||
var r = eventText()
|
||||
if let openText {
|
||||
r = r
|
||||
+ Text(openText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.accentColor)
|
||||
+ Text(" ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
return r.font(.caption)
|
||||
}
|
||||
|
||||
private func eventText() -> Text {
|
||||
if let member = chatItem.memberDisplayName {
|
||||
return Text(member + " " + chatItem.content.text + " ")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
return Text(chatItem.content.text + " ")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CIMemberCreatedContactView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let content = CIContent.rcvGroupEvent(rcvGroupEvent: .memberCreatedContact)
|
||||
let chatItem = ChatItem(
|
||||
chatDir: .groupRcv(groupMember: GroupMember.sampleData),
|
||||
meta: CIMeta.getSample(1, .now, content.text, .rcvRead),
|
||||
content: content,
|
||||
quotedItem: nil,
|
||||
file: nil
|
||||
)
|
||||
CIMemberCreatedContactView(chatItem: chatItem)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIMetaView: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var metaColor = Color.secondary
|
||||
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
||||
@@ -95,15 +95,14 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text {
|
||||
struct CIMetaView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
|
||||
CIMetaView(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: .partial)))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
|
||||
CIMetaView(chatItem: ChatItem.getDeletedContentSample())
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
|
||||
CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 100))
|
||||
.environmentObject(Chat.sampleData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import SimpleXChat
|
||||
let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup."
|
||||
|
||||
struct CIRcvDecryptionError: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
var msgDecryptError: MsgDecryptError
|
||||
var msgCount: UInt32
|
||||
var chatItem: ChatItem
|
||||
@@ -41,11 +42,11 @@ struct CIRcvDecryptionError: View {
|
||||
.onAppear {
|
||||
// for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear
|
||||
if case let .group(groupInfo) = chat.chatInfo,
|
||||
case let .groupRcv(groupMember, _) = chatItem.chatDir {
|
||||
case let .groupRcv(groupMember) = chatItem.chatDir {
|
||||
do {
|
||||
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
|
||||
if let s = stats {
|
||||
ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, member, s)
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, s)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo error: \(responseError(error))")
|
||||
@@ -78,9 +79,9 @@ struct CIRcvDecryptionError: View {
|
||||
basicDecryptionErrorItem()
|
||||
}
|
||||
} else if case let .group(groupInfo) = chat.chatInfo,
|
||||
case let .groupRcv(groupMember, _) = chatItem.chatDir,
|
||||
let modelMember = ChatModel.shared.groupMembers.first(where: { $0.id == groupMember.id }),
|
||||
let memberStats = modelMember.activeConn?.connectionStats {
|
||||
case let .groupRcv(groupMember) = chatItem.chatDir,
|
||||
let mem = m.getGroupMember(groupMember.groupMemberId),
|
||||
let memberStats = mem.wrapped.activeConn?.connectionStats {
|
||||
if memberStats.ratchetSyncAllowed {
|
||||
decryptionErrorItemFixButton(syncSupported: true) {
|
||||
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
|
||||
@@ -122,7 +123,7 @@ struct CIRcvDecryptionError: View {
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
@@ -142,7 +143,7 @@ struct CIRcvDecryptionError: View {
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.onTapGesture(perform: { onClick() })
|
||||
@@ -173,7 +174,7 @@ struct CIRcvDecryptionError: View {
|
||||
do {
|
||||
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, false)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateGroupMemberConnectionStats(groupInfo, mem, stats)
|
||||
m.updateGroupMemberConnectionStats(groupInfo, mem, stats)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
|
||||
@@ -190,7 +191,7 @@ struct CIRcvDecryptionError: View {
|
||||
do {
|
||||
let stats = try apiSyncContactRatchet(contact.apiId, false)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateContactConnectionStats(contact, stats)
|
||||
m.updateContactConnectionStats(contact, stats)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
|
||||
|
||||
@@ -11,6 +11,7 @@ import AVKit
|
||||
import SimpleXChat
|
||||
|
||||
struct CIVideoView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
private let chatItem: ChatItem
|
||||
private let image: String
|
||||
@@ -101,7 +102,7 @@ struct CIVideoView: View {
|
||||
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete
|
||||
VideoPlayerView(player: player, url: url, showControls: false)
|
||||
.frame(width: w, height: w * preview.size.height / preview.size.width)
|
||||
.onChange(of: ChatModel.shared.stopPreviousRecPlay) { playingUrl in
|
||||
.onChange(of: m.stopPreviousRecPlay) { playingUrl in
|
||||
if playingUrl != url {
|
||||
player.pause()
|
||||
videoPlaying = false
|
||||
@@ -124,7 +125,7 @@ struct CIVideoView: View {
|
||||
}
|
||||
if !videoPlaying {
|
||||
Button {
|
||||
ChatModel.shared.stopPreviousRecPlay = url
|
||||
m.stopPreviousRecPlay = url
|
||||
player.play()
|
||||
} label: {
|
||||
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
|
||||
@@ -256,7 +257,7 @@ struct CIVideoView: View {
|
||||
// TODO encrypt: where file size is checked?
|
||||
private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
|
||||
Task {
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user, file.fileId, encrypted, false)
|
||||
}
|
||||
}
|
||||
@@ -290,7 +291,7 @@ struct CIVideoView: View {
|
||||
)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||
ChatModel.shared.stopPreviousRecPlay = url
|
||||
m.stopPreviousRecPlay = url
|
||||
if let player = fullPlayer {
|
||||
player.play()
|
||||
fullScreenTimeObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIVoiceView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
let recordingFile: CIFile?
|
||||
let duration: Int
|
||||
@@ -91,7 +92,7 @@ struct CIVoiceView: View {
|
||||
}
|
||||
|
||||
private func metaView() -> some View {
|
||||
CIMetaView(chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +220,7 @@ struct VoiceMessagePlayer: View {
|
||||
private func downloadButton(_ recordingFile: CIFile) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
if let user = chatModel.currentUser {
|
||||
await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get())
|
||||
}
|
||||
}
|
||||
@@ -284,6 +285,7 @@ struct CIVoiceView_Previews: PreviewProvider {
|
||||
)
|
||||
Group {
|
||||
CIVoiceView(
|
||||
chat: Chat.sampleData,
|
||||
chatItem: ChatItem.getVoiceMsgContentSample(),
|
||||
recordingFile: CIFile.getSample(fileName: "voice.m4a", fileSize: 65536, fileStatus: .rcvComplete),
|
||||
duration: 30,
|
||||
@@ -292,12 +294,11 @@ struct CIVoiceView_Previews: PreviewProvider {
|
||||
playbackTime: .constant(TimeInterval(20)),
|
||||
allowMenu: Binding.constant(true)
|
||||
)
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, 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(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(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 360))
|
||||
.environmentObject(Chat.sampleData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct DeletedItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -18,7 +19,7 @@ struct DeletedItemView: View {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
CIMetaView(chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
@@ -32,8 +33,8 @@ struct DeletedItemView: View {
|
||||
struct DeletedItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
DeletedItemView(chatItem: ChatItem.getDeletedContentSample())
|
||||
DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData, messageScope: .msGroup)))
|
||||
DeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
|
||||
DeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 200))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct EmojiItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -17,7 +18,7 @@ struct EmojiItemView: View {
|
||||
emojiText(chatItem.content.text)
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, 6)
|
||||
CIMetaView(chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
@@ -32,8 +33,8 @@ func emojiText(_ text: String) -> Text {
|
||||
struct EmojiItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete)))
|
||||
EmojiItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "👍"))
|
||||
EmojiItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete)))
|
||||
EmojiItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "👍"))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 70))
|
||||
}
|
||||
|
||||
@@ -88,13 +88,12 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
|
||||
file: CIFile.getSample(fileStatus: .sndComplete)
|
||||
)
|
||||
Group {
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), 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(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(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false))
|
||||
}
|
||||
.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() {
|
||||
if let player = player {
|
||||
ChatModel.shared.stopPreviousRecPlay = url
|
||||
m.stopPreviousRecPlay = url
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct IntegrityErrorItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
var msgError: MsgErrorType
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
CIMsgError(chatItem: chatItem) {
|
||||
CIMsgError(chat: chat, chatItem: chatItem) {
|
||||
switch msgError {
|
||||
case .msgSkipped:
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -52,6 +53,7 @@ struct IntegrityErrorItemView: View {
|
||||
}
|
||||
|
||||
struct CIMsgError: View {
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var onTap: () -> Void
|
||||
|
||||
@@ -60,7 +62,7 @@ struct CIMsgError: View {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
CIMetaView(chatItem: chatItem)
|
||||
CIMetaView(chat: chat, chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
@@ -74,6 +76,6 @@ struct CIMsgError: View {
|
||||
|
||||
struct IntegrityErrorItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
IntegrityErrorItemView(msgError: .msgBadHash, chatItem: ChatItem.getIntegrityErrorSample())
|
||||
IntegrityErrorItemView(chat: Chat.sampleData, msgError: .msgBadHash, chatItem: ChatItem.getIntegrityErrorSample())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,39 +10,70 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct MarkedDeletedItemView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
if case let .moderated(_, byGroupMember) = chatItem.meta.itemDeleted {
|
||||
markedDeletedText("moderated by \(byGroupMember.chatViewName)")
|
||||
} else {
|
||||
markedDeletedText("marked deleted")
|
||||
}
|
||||
CIMetaView(chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
func markedDeletedText(_ s: LocalizedStringKey) -> some View {
|
||||
Text(s)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
.lineLimit(1)
|
||||
var mergedMarkedDeletedText: LocalizedStringKey {
|
||||
if !revealed,
|
||||
let ciCategory = chatItem.mergeCategory,
|
||||
var i = m.getChatItemIndex(chatItem) {
|
||||
var moderated = 0
|
||||
var blocked = 0
|
||||
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 {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)))
|
||||
MarkedDeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 200))
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ private func typing(_ w: Font.Weight = .light) -> Text {
|
||||
}
|
||||
|
||||
struct MsgContentView: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
@ObservedObject var chat: Chat
|
||||
var text: String
|
||||
var formattedText: [FormattedText]? = nil
|
||||
var sender: String? = nil
|
||||
@@ -121,13 +121,11 @@ private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
|
||||
case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary)
|
||||
case let .colored(color): return Text(t).foregroundColor(color.uiColor)
|
||||
case .uri: return linkText(t, t, preview, prefix: "")
|
||||
case let .simplexLink(linkType, simplexUri, trustedUri, smpHosts):
|
||||
case let .simplexLink(linkType, simplexUri, smpHosts):
|
||||
switch privacySimplexLinkModeDefault.get() {
|
||||
case .description: return linkText(simplexLinkText(linkType, smpHosts), simplexUri, preview, prefix: "")
|
||||
case .full: return linkText(t, simplexUri, preview, prefix: "")
|
||||
case .browser: return trustedUri
|
||||
? linkText(t, t, preview, prefix: "")
|
||||
: linkText(t, t, preview, prefix: "", color: .red, uiColor: .red)
|
||||
case .browser: return linkText(t, simplexUri, preview, prefix: "")
|
||||
}
|
||||
case .email: return linkText(t, t, preview, prefix: "mailto:")
|
||||
case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:")
|
||||
@@ -154,6 +152,7 @@ struct MsgContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
|
||||
return MsgContentView(
|
||||
chat: Chat.sampleData,
|
||||
text: chatItem.text,
|
||||
formattedText: chatItem.formattedText,
|
||||
sender: chatItem.memberDisplayName,
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatItemInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var ci: ChatItem
|
||||
@Binding var chatItemInfo: ChatItemInfo?
|
||||
@@ -290,8 +291,8 @@ struct ChatItemInfoView: View {
|
||||
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
|
||||
memberDeliveryStatuses.compactMap({ mds in
|
||||
if let mem = ChatModel.shared.getGroupMember(mds.groupMemberId) {
|
||||
return (mem, mds.memberDeliveryStatus)
|
||||
if let mem = chatModel.getGroupMember(mds.groupMemberId) {
|
||||
return (mem.wrapped, mds.memberDeliveryStatus)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatItemView: View {
|
||||
var chatInfo: ChatInfo
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@State var scrollProxy: ScrollViewProxy? = nil
|
||||
@@ -19,8 +19,19 @@ struct ChatItemView: View {
|
||||
@Binding var audioPlayer: AudioPlayer?
|
||||
@Binding var playbackState: VoiceMessagePlaybackState
|
||||
@Binding var playbackTime: TimeInterval?
|
||||
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)) {
|
||||
self.chatInfo = chatInfo
|
||||
init(
|
||||
chat: Chat,
|
||||
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.maxWidth = maxWidth
|
||||
_scrollProxy = .init(initialValue: scrollProxy)
|
||||
@@ -33,15 +44,15 @@ struct ChatItemView: View {
|
||||
|
||||
var body: some View {
|
||||
let ci = chatItem
|
||||
if chatItem.meta.itemDeleted != nil && !revealed {
|
||||
MarkedDeletedItemView(chatItem: chatItem)
|
||||
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
|
||||
MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed)
|
||||
} else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
|
||||
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
|
||||
EmojiItemView(chatItem: ci)
|
||||
EmojiItemView(chat: chat, chatItem: ci)
|
||||
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
|
||||
CIVoiceView(chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu)
|
||||
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu)
|
||||
} else if ci.content.msgContent == nil {
|
||||
ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||
ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
@@ -51,15 +62,17 @@ struct ChatItemView: View {
|
||||
}
|
||||
|
||||
private func framedItemView() -> some View {
|
||||
FramedItemView(chatInfo: chatInfo, chatItem: chatItem, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatItemContentView<Content: View>: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
var chatInfo: ChatInfo
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
var msgContentView: () -> Content
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
var body: some View {
|
||||
switch chatItem.content {
|
||||
@@ -69,11 +82,17 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case .rcvDeleted: deletedItemView()
|
||||
case let .sndCall(status, duration): callItemView(status, duration)
|
||||
case let .rcvCall(status, duration): callItemView(status, duration)
|
||||
case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem)
|
||||
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
|
||||
case let .rcvIntegrityError(msgError):
|
||||
if developerTools {
|
||||
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 .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText)
|
||||
case .rcvDirectEvent: eventItemView()
|
||||
case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem)
|
||||
case .rcvGroupEvent: eventItemView()
|
||||
case .sndGroupEvent: eventItemView()
|
||||
case .rcvConnEvent: eventItemView()
|
||||
@@ -81,9 +100,9 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor)
|
||||
case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor)
|
||||
case let .rcvChatPreference(feature, allowed, param):
|
||||
CIFeaturePreferenceView(chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
||||
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
||||
case let .sndChatPreference(feature, _, _):
|
||||
CIChatFeatureView(chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: .secondary)
|
||||
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary)
|
||||
case let .rcvGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
|
||||
case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
|
||||
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
@@ -95,15 +114,15 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func deletedItemView() -> some View {
|
||||
DeletedItemView(chatItem: chatItem)
|
||||
DeletedItemView(chat: chat, chatItem: chatItem)
|
||||
}
|
||||
|
||||
private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View {
|
||||
CICallItemView(chatInfo: chatInfo, chatItem: chatItem, status: status, duration: duration)
|
||||
CICallItemView(chat: chat, chatItem: chatItem, status: status, duration: duration)
|
||||
}
|
||||
|
||||
private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View {
|
||||
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chatInfo.incognito)
|
||||
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chat.chatInfo.incognito)
|
||||
}
|
||||
|
||||
private func eventItemView() -> some View {
|
||||
@@ -111,7 +130,9 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func eventItemViewText() -> Text {
|
||||
if let member = chatItem.memberDisplayName {
|
||||
if !revealed, let t = mergedGroupEventText {
|
||||
return chatEventText(t + Text(" ") + chatItem.timestampText)
|
||||
} else if let member = chatItem.memberDisplayName {
|
||||
return Text(member + " ")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -123,36 +144,44 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
||||
CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor)
|
||||
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
|
||||
}
|
||||
|
||||
private var membersConnectedItemText: Text {
|
||||
if let t = membersConnectedText {
|
||||
return chatEventText(t, chatItem.timestampText)
|
||||
private var mergedGroupEventText: Text? {
|
||||
let (count, ns) = chatModel.getConnectedMemberNames(chatItem)
|
||||
let members: LocalizedStringKey =
|
||||
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 {
|
||||
return eventItemViewText()
|
||||
Text(members)
|
||||
}
|
||||
}
|
||||
|
||||
private var membersConnectedText: LocalizedStringKey? {
|
||||
let ns = chatModel.getConnectedMemberNames(chatItem)
|
||||
return ns.count > 3
|
||||
? "\(ns[0]), \(ns[1]) and \(ns.count - 2) other members connected"
|
||||
: 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 {
|
||||
(Text(eventText) + Text(" ") + ts)
|
||||
func chatEventText(_ text: Text) -> Text {
|
||||
text
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
|
||||
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text {
|
||||
chatEventText(Text(eventText) + Text(" ") + ts)
|
||||
}
|
||||
|
||||
func chatEventText(_ ci: ChatItem) -> Text {
|
||||
chatEventText("\(ci.content.text)", ci.timestampText)
|
||||
}
|
||||
@@ -160,15 +189,15 @@ func chatEventText(_ ci: ChatItem) -> Text {
|
||||
struct ChatItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, 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(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample(), 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(chatInfo: ChatInfo.sampleData.direct, 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, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 70))
|
||||
.environmentObject(Chat.sampleData)
|
||||
@@ -180,7 +209,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil)
|
||||
Group{
|
||||
ChatItemView(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chat: Chat.sampleData,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
@@ -191,7 +220,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
revealed: Binding.constant(true)
|
||||
)
|
||||
ChatItemView(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chat: Chat.sampleData,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead),
|
||||
@@ -202,7 +231,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
revealed: Binding.constant(true)
|
||||
)
|
||||
ChatItemView(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chat: Chat.sampleData,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
@@ -213,7 +242,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
revealed: Binding.constant(true)
|
||||
)
|
||||
ChatItemView(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chat: Chat.sampleData,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
@@ -224,7 +253,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
revealed: Binding.constant(true)
|
||||
)
|
||||
ChatItemView(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chat: Chat.sampleData,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
|
||||
@@ -21,9 +21,7 @@ struct ChatView: View {
|
||||
@State private var showChatInfoSheet: Bool = false
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var composeState = ComposeState()
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@State private var keyboardVisible = false
|
||||
@State private var showDeleteMessage = false
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var customUserProfile: Profile?
|
||||
@State private var connectionCode: String?
|
||||
@@ -36,7 +34,12 @@ struct ChatView: View {
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocussed
|
||||
// opening GroupMemberInfoView on member icon
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
@State private var membersLoaded = false
|
||||
@State private var selectedMember: GMember? = nil
|
||||
// opening GroupLinkView on link button (incognito)
|
||||
@State private var showGroupLinkSheet: Bool = false
|
||||
@State private var groupLink: String?
|
||||
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -64,6 +67,7 @@ struct ChatView: View {
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
connectingText()
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
composeState: $composeState,
|
||||
@@ -90,7 +94,10 @@ struct ChatView: View {
|
||||
chatModel.chatId = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
if chatModel.chatId == nil {
|
||||
chatModel.chatItemStatuses = [:]
|
||||
chatModel.reversedChatItems = []
|
||||
chatModel.groupMembers = []
|
||||
membersLoaded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,21 +135,21 @@ struct ChatView: View {
|
||||
}
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
Button {
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
showChatInfoSheet = true
|
||||
}
|
||||
}
|
||||
Task { await loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.appSheet(isPresented: $showChatInfoSheet) {
|
||||
GroupChatInfoView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
|
||||
GroupChatInfoView(
|
||||
chat: chat,
|
||||
groupInfo: Binding(
|
||||
get: { groupInfo },
|
||||
set: { gInfo in
|
||||
chat.chatInfo = .group(groupInfo: gInfo)
|
||||
chat.created = Date.now
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,6 +159,7 @@ struct ChatView: View {
|
||||
HStack {
|
||||
if contact.allowsFeature(.calls) {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
Menu {
|
||||
if contact.allowsFeature(.calls) {
|
||||
@@ -160,9 +168,11 @@ struct ChatView: View {
|
||||
} label: {
|
||||
Label("Video call", systemImage: "video")
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
searchButton()
|
||||
toggleNtfsButton(chat)
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
@@ -171,9 +181,16 @@ struct ChatView: View {
|
||||
HStack {
|
||||
if groupInfo.canAddMembers {
|
||||
if (chat.chatInfo.incognito) {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.onTapGesture { AlertManager.shared.showAlert(cantInviteIncognitoAlert()) }
|
||||
groupLinkButton()
|
||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
groupLinkMemberRole: $groupLinkMemberRole,
|
||||
showTitle: true,
|
||||
creatingGroup: false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
addMembersButton()
|
||||
.appSheet(isPresented: $showAddMembersSheet) {
|
||||
@@ -195,6 +212,17 @@ 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() {
|
||||
let cInfo = chat.chatInfo
|
||||
if case let .direct(contact) = cInfo {
|
||||
@@ -210,13 +238,6 @@ struct ChatView: View {
|
||||
logger.error("apiContactInfo error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
}
|
||||
}
|
||||
}
|
||||
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
|
||||
composeState = draft
|
||||
@@ -323,6 +344,20 @@ struct ChatView: View {
|
||||
}
|
||||
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||
}
|
||||
|
||||
@ViewBuilder private func connectingText() -> some View {
|
||||
if case let .direct(contact) = chat.chatInfo,
|
||||
!contact.ready,
|
||||
contact.active,
|
||||
!contact.nextSendGrpInv {
|
||||
Text("connecting…")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private func floatingButtons(_ proxy: ScrollViewProxy) -> some View {
|
||||
let counts = chatModel.unreadChatItemCounts(itemsInView: itemsInView)
|
||||
@@ -396,19 +431,32 @@ struct ChatView: View {
|
||||
private func addMembersButton() -> some View {
|
||||
Button {
|
||||
if case let .group(gInfo) = chat.chatInfo {
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
showAddMembersSheet = true
|
||||
}
|
||||
}
|
||||
Task { await loadGroupMembers(gInfo) { showAddMembersSheet = true } }
|
||||
}
|
||||
} label: {
|
||||
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) {
|
||||
if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id {
|
||||
if loadingItems || firstPage { return }
|
||||
@@ -438,171 +486,30 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
|
||||
if case let .groupRcv(member, msgScope) = 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, msgScope, prevItem) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if ci.content.showMemberName {
|
||||
Group {
|
||||
if msgScope == .msDirect {
|
||||
Text("\(member.displayName) **only to you**")
|
||||
} else {
|
||||
Text(member.displayName)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, memberImageSize + 14)
|
||||
.padding(.top, 7)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
RcvMemberImageWithMenu(
|
||||
member: member,
|
||||
groupInfo: groupInfo,
|
||||
composeState: $composeState,
|
||||
selectedMember: $selectedMember
|
||||
)
|
||||
.environmentObject(chat)
|
||||
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 if case let .groupSnd(directMember) = ci.chatDir {
|
||||
let (prevItem, _) = chatModel.getChatItemNeighbors(ci)
|
||||
if prevItem == nil || showSndScope(directMember, prevItem) {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
if ci.content.showMemberName {
|
||||
HStack {
|
||||
if let directMember = directMember {
|
||||
Text("**only to** \(directMember.displayName)")
|
||||
ProfileImage(imageStr: directMember.image)
|
||||
.frame(width: 20, height: 20)
|
||||
.onTapGesture { selectedMember = directMember }
|
||||
} else {
|
||||
Text("to group")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 7)
|
||||
}
|
||||
chatItemWithMenu(ci, maxWidth)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 5)
|
||||
} else {
|
||||
chatItemWithMenu(ci, maxWidth)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
} else {
|
||||
chatItemWithMenu(ci, maxWidth)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
}
|
||||
|
||||
private struct RcvMemberImageWithMenu: View {
|
||||
var member: GroupMember
|
||||
var groupInfo: GroupInfo
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var selectedMember: GroupMember?
|
||||
@State private var allowMenu: Bool = true
|
||||
|
||||
var body: some View {
|
||||
let menuItems = memberMenu(member, groupInfo)
|
||||
if menuItems.isEmpty {
|
||||
profileImage()
|
||||
} else {
|
||||
let uiMenu: Binding<UIMenu> = Binding(
|
||||
get: { UIMenu(title: "", children: menuItems) },
|
||||
set: { _ in }
|
||||
)
|
||||
profileImage()
|
||||
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
|
||||
}
|
||||
}
|
||||
|
||||
private func profileImage() -> some View {
|
||||
ProfileImage(imageStr: member.memberProfile.image)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
.onTapGesture { selectedMember = member }
|
||||
}
|
||||
|
||||
private func memberMenu(_ member: GroupMember, _ groupInfo: GroupInfo) -> [UIMenuElement] {
|
||||
var menu: [UIMenuElement] = []
|
||||
if let memberWithConn = ChatModel.shared.getGroupMember(member.groupMemberId),
|
||||
memberWithConn.allowedToSendDirectlyTo(groupInfo: groupInfo) {
|
||||
menu.append(memberMenuSendDirectlyUIAction(memberWithConn, groupInfo))
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
private func memberMenuSendDirectlyUIAction(_ toDirectMember: GroupMember, _ groupInfo: GroupInfo) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Send directly", comment: "chat item action"),
|
||||
image: UIImage(systemName: "arrow.left.arrow.right")
|
||||
) { _ in
|
||||
let canSend = toDirectMember.canSendDirectlyTo(groupInfo: groupInfo)
|
||||
switch canSend {
|
||||
case .notConnected:
|
||||
AlertManager.shared.showAlert(cantSendDirectlyNotConnectedAlert())
|
||||
case .notSupported:
|
||||
AlertManager.shared.showAlert(cantSendDirectlyNotSupportedAlert())
|
||||
case .canSend:
|
||||
withAnimation {
|
||||
if composeState.editing {
|
||||
composeState = ComposeState(directMember: .directMember(groupMember: toDirectMember))
|
||||
} else {
|
||||
composeState = composeState.copy(
|
||||
directMember: .directMember(groupMember: toDirectMember),
|
||||
contextItem: .noContextItem
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
|
||||
ChatItemWithMenu(
|
||||
ci: ci,
|
||||
chat: chat,
|
||||
chatItem: ci,
|
||||
maxWidth: maxWidth,
|
||||
scrollProxy: scrollProxy,
|
||||
deleteMessage: deleteMessage,
|
||||
deletingItem: $deletingItem,
|
||||
composeState: $composeState,
|
||||
showDeleteMessage: $showDeleteMessage
|
||||
selectedMember: $selectedMember,
|
||||
chatView: self
|
||||
)
|
||||
.environmentObject(chat)
|
||||
}
|
||||
|
||||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var ci: ChatItem
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat
|
||||
var scrollProxy: ScrollViewProxy?
|
||||
var deleteMessage: (CIDeleteMode) -> Void
|
||||
@Binding var deletingItem: ChatItem?
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var showDeleteMessage: Bool
|
||||
@Binding var selectedMember: GMember?
|
||||
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 showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@@ -614,18 +521,114 @@ struct ChatView: View {
|
||||
@State private var playbackTime: TimeInterval?
|
||||
|
||||
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 uiMenu: Binding<UIMenu> = Binding(
|
||||
get: { UIMenu(title: "", children: menu(live: composeState.liveMessage != nil)) },
|
||||
get: { UIMenu(title: "", children: menu(ci, range, live: composeState.liveMessage != nil)) },
|
||||
set: { _ in }
|
||||
)
|
||||
|
||||
VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
|
||||
.accessibilityLabel("")
|
||||
ChatItemView(
|
||||
chat: chat,
|
||||
chatItem: ci,
|
||||
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 {
|
||||
chatItemReactions()
|
||||
chatItemReactions(ci)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
}
|
||||
@@ -639,6 +642,11 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog(deleteMessagesTitle, isPresented: $showDeleteMessages, titleVisibility: .visible) {
|
||||
Button("Delete for me", role: .destructive) {
|
||||
deleteMessages()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
|
||||
.onDisappear {
|
||||
@@ -656,7 +664,15 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func chatItemReactions() -> some View {
|
||||
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 chatItemReactions(_ ci: ChatItem) -> some View {
|
||||
HStack(spacing: 4) {
|
||||
ForEach(ci.reactions, id: \.reaction) { r in
|
||||
let v = HStack(spacing: 4) {
|
||||
@@ -676,7 +692,7 @@ struct ChatView: View {
|
||||
|
||||
if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) {
|
||||
v.onTapGesture {
|
||||
setReaction(add: !r.userReacted, reaction: r.reaction)
|
||||
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
|
||||
}
|
||||
} else {
|
||||
v
|
||||
@@ -685,10 +701,10 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func menu(live: Bool) -> [UIMenuElement] {
|
||||
private func menu(_ ci: ChatItem, _ range: ClosedRange<Int>?, live: Bool) -> [UIMenuElement] {
|
||||
var menu: [UIMenuElement] = []
|
||||
if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed {
|
||||
let rs = allReactions()
|
||||
let rs = allReactions(ci)
|
||||
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction,
|
||||
rs.count > 0 {
|
||||
var rm: UIMenu
|
||||
@@ -705,18 +721,10 @@ struct ChatView: View {
|
||||
menu.append(rm)
|
||||
}
|
||||
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
|
||||
if ci.directMember == nil {
|
||||
menu.append(replyUIAction())
|
||||
}
|
||||
if case let .group(groupInfo) = chat.chatInfo,
|
||||
let toDirectMember = ci.memberToReplyDirectlyTo,
|
||||
let toDirectMemberWithConn = ChatModel.shared.getGroupMember(toDirectMember.groupMemberId),
|
||||
toDirectMemberWithConn.allowedToSendDirectlyTo(groupInfo: groupInfo) {
|
||||
menu.append(replyDirectlyUIAction(toDirectMemberWithConn, groupInfo))
|
||||
}
|
||||
menu.append(replyUIAction(ci))
|
||||
}
|
||||
menu.append(shareUIAction())
|
||||
menu.append(copyUIAction())
|
||||
menu.append(shareUIAction(ci))
|
||||
menu.append(copyUIAction(ci))
|
||||
if let fileSource = getLoadedFileSource(ci.file) {
|
||||
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
|
||||
if image.imageData != nil {
|
||||
@@ -729,9 +737,9 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
if ci.meta.editable && !mc.isVoice && !live {
|
||||
menu.append(editAction())
|
||||
menu.append(editAction(ci))
|
||||
}
|
||||
menu.append(viewInfoUIAction())
|
||||
menu.append(viewInfoUIAction(ci))
|
||||
if revealed {
|
||||
menu.append(hideUIAction())
|
||||
}
|
||||
@@ -741,66 +749,40 @@ struct ChatView: View {
|
||||
menu.append(cancelFileUIAction(file.fileId, cancelAction))
|
||||
}
|
||||
if !live || !ci.meta.isLive {
|
||||
menu.append(deleteUIAction())
|
||||
menu.append(deleteUIAction(ci))
|
||||
}
|
||||
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) {
|
||||
menu.append(moderateUIAction(groupInfo))
|
||||
menu.append(moderateUIAction(ci, groupInfo))
|
||||
}
|
||||
} else if ci.meta.itemDeleted != nil {
|
||||
if !ci.isDeletedContent {
|
||||
if revealed {
|
||||
menu.append(hideUIAction())
|
||||
} else if !ci.isDeletedContent {
|
||||
menu.append(revealUIAction())
|
||||
} else if range != nil {
|
||||
menu.append(expandUIAction())
|
||||
}
|
||||
menu.append(viewInfoUIAction())
|
||||
menu.append(deleteUIAction())
|
||||
menu.append(viewInfoUIAction(ci))
|
||||
menu.append(deleteUIAction(ci))
|
||||
} else if ci.isDeletedContent {
|
||||
menu.append(viewInfoUIAction())
|
||||
menu.append(deleteUIAction())
|
||||
menu.append(viewInfoUIAction(ci))
|
||||
menu.append(deleteUIAction(ci))
|
||||
} else if ci.mergeCategory != nil {
|
||||
menu.append(revealed ? shrinkUIAction() : expandUIAction())
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
private func replyUIAction() -> UIAction {
|
||||
private func replyUIAction(_ ci: ChatItem) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Reply", comment: "chat item action"),
|
||||
image: UIImage(systemName: "arrowshape.turn.up.left.2")
|
||||
image: UIImage(systemName: "arrowshape.turn.up.left")
|
||||
) { _ in
|
||||
withAnimation {
|
||||
if composeState.editing {
|
||||
composeState = ComposeState(contextItem: .quotedItem(chatItem: ci))
|
||||
} else {
|
||||
composeState = composeState.copy(
|
||||
directMember: .noDirectMember,
|
||||
contextItem: .quotedItem(chatItem: ci)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func replyDirectlyUIAction(_ toDirectMember: GroupMember, _ groupInfo: GroupInfo) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Reply directly", comment: "chat item action"),
|
||||
image: UIImage(systemName: "arrowshape.turn.up.left")
|
||||
) { _ in
|
||||
let canSend = toDirectMember.canSendDirectlyTo(groupInfo: groupInfo)
|
||||
switch canSend {
|
||||
case .notConnected:
|
||||
AlertManager.shared.showAlert(cantSendDirectlyNotConnectedAlert())
|
||||
case .notSupported:
|
||||
AlertManager.shared.showAlert(cantSendDirectlyNotSupportedAlert())
|
||||
case .canSend:
|
||||
withAnimation {
|
||||
if composeState.editing {
|
||||
composeState = ComposeState(
|
||||
directMember: .directMember(groupMember: toDirectMember),
|
||||
contextItem: .quotedItem(chatItem: ci)
|
||||
)
|
||||
} else {
|
||||
composeState = composeState.copy(
|
||||
directMember: .directMember(groupMember: toDirectMember),
|
||||
contextItem: .quotedItem(chatItem: ci)
|
||||
)
|
||||
}
|
||||
composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -826,11 +808,11 @@ struct ChatView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func allReactions() -> [UIAction] {
|
||||
private func allReactions(_ ci: ChatItem) -> [UIAction] {
|
||||
MsgReaction.values.compactMap { r in
|
||||
ci.reactions.contains(where: { $0.userReacted && $0.reaction == r })
|
||||
? nil
|
||||
: UIAction(title: r.text) { _ in setReaction(add: true, reaction: r) }
|
||||
: UIAction(title: r.text) { _ in setReaction(ci, add: true, reaction: r) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,7 +820,7 @@ struct ChatView: View {
|
||||
rs.count > 4 ? 3 : 4
|
||||
}
|
||||
|
||||
private func setReaction(add: Bool, reaction: MsgReaction) {
|
||||
private func setReaction(_ ci: ChatItem, add: Bool, reaction: MsgReaction) {
|
||||
Task {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
@@ -850,7 +832,7 @@ struct ChatView: View {
|
||||
reaction: reaction
|
||||
)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateChatItem(chat.chatInfo, chatItem)
|
||||
m.updateChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiChatItemReaction error: \(responseError(error))")
|
||||
@@ -858,7 +840,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func shareUIAction() -> UIAction {
|
||||
private func shareUIAction(_ ci: ChatItem) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Share", comment: "chat item action"),
|
||||
image: UIImage(systemName: "square.and.arrow.up")
|
||||
@@ -871,7 +853,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func copyUIAction() -> UIAction {
|
||||
private func copyUIAction(_ ci: ChatItem) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Copy", comment: "chat item action"),
|
||||
image: UIImage(systemName: "doc.on.doc")
|
||||
@@ -904,7 +886,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func editAction() -> UIAction {
|
||||
private func editAction(_ ci: ChatItem) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Edit", comment: "chat item action"),
|
||||
image: UIImage(systemName: "square.and.pencil")
|
||||
@@ -915,7 +897,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func viewInfoUIAction() -> UIAction {
|
||||
private func viewInfoUIAction(_ ci: ChatItem) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Info", comment: "chat item action"),
|
||||
image: UIImage(systemName: "info.circle")
|
||||
@@ -928,10 +910,7 @@ struct ChatView: View {
|
||||
chatItemInfo = ciInfo
|
||||
}
|
||||
if case let .group(gInfo) = chat.chatInfo {
|
||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
}
|
||||
await chatView.loadGroupMembers(gInfo)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiGetChatItemInfo error: \(responseError(error))")
|
||||
@@ -952,7 +931,7 @@ struct ChatView: View {
|
||||
message: Text(cancelAction.alert.message),
|
||||
primaryButton: .destructive(Text(cancelAction.alert.confirm)) {
|
||||
Task {
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
if let user = m.currentUser {
|
||||
await cancelFile(user: user, fileId: fileId)
|
||||
}
|
||||
}
|
||||
@@ -973,18 +952,45 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteUIAction() -> UIAction {
|
||||
private func deleteUIAction(_ ci: ChatItem) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Delete", comment: "chat item action"),
|
||||
image: UIImage(systemName: "trash"),
|
||||
attributes: [.destructive]
|
||||
) { _ in
|
||||
showDeleteMessage = true
|
||||
deletingItem = ci
|
||||
if !revealed && ci.meta.itemDeleted != nil,
|
||||
let currIndex = m.getChatItemIndex(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 moderateUIAction(_ groupInfo: GroupInfo) -> UIAction {
|
||||
private func itemsRange(_ currIndex: Int?, _ prevHidden: Int?) -> ClosedRange<Int>? {
|
||||
if let currIndex = currIndex,
|
||||
let prevHidden = prevHidden,
|
||||
prevHidden > currIndex {
|
||||
currIndex...prevHidden
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private func moderateUIAction(_ ci: ChatItem, _ groupInfo: GroupInfo) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Moderate", comment: "chat item action"),
|
||||
image: UIImage(systemName: "flag"),
|
||||
@@ -1016,43 +1022,105 @@ 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 {
|
||||
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
|
||||
}
|
||||
}
|
||||
|
||||
private static func cantSendDirectlyNotConnectedAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Can't send directly!"),
|
||||
message: Text("Member is not connected.")
|
||||
)
|
||||
}
|
||||
var deleteMessagesTitle: LocalizedStringKey {
|
||||
let n = deletingItems.count
|
||||
return n == 1 ? "Delete message?" : "Delete \(n) messages?"
|
||||
}
|
||||
|
||||
private static func cantSendDirectlyNotSupportedAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Can't send directly!"),
|
||||
message: Text("Member doesn't support this feature, ask them to update.")
|
||||
)
|
||||
}
|
||||
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 showMemberImage(_ member: GroupMember, _ msgScope: MessageScope, _ prevItem: ChatItem?) -> Bool {
|
||||
switch (prevItem?.chatDir) {
|
||||
case .groupSnd: return true
|
||||
case let .groupRcv(prevMember, prevMsgScope): return prevMember.groupMemberId != member.groupMemberId || prevMsgScope != msgScope
|
||||
default: return false
|
||||
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 showSndScope(_ directMember: GroupMember?, _ prevItem: ChatItem?) -> Bool {
|
||||
switch (prevItem?.chatDir) {
|
||||
case .groupRcv: return directMember != nil
|
||||
case let .groupSnd(prevDirectMember):
|
||||
return prevDirectMember?.groupMemberId != directMember?.groupMemberId
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
private func scrollToBottom(_ proxy: ScrollViewProxy) {
|
||||
if let ci = chatModel.reversedChatItems.first {
|
||||
withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) }
|
||||
@@ -1064,44 +1132,6 @@ struct ChatView: View {
|
||||
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 {
|
||||
@@ -1118,7 +1148,7 @@ struct ChatView: View {
|
||||
|
||||
func toggleNotifications(_ chat: Chat, enableNtfs: Bool) {
|
||||
var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults
|
||||
chatSettings.enableNtfs = enableNtfs
|
||||
chatSettings.enableNtfs = enableNtfs ? .all : .none
|
||||
updateChatSettings(chat, chatSettings: chatSettings)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,6 @@ import SimpleXChat
|
||||
import SwiftyGif
|
||||
import PhotosUI
|
||||
|
||||
enum ComposeDirectMember {
|
||||
case noDirectMember
|
||||
case directMember(groupMember: GroupMember)
|
||||
case directMemberCancelled
|
||||
}
|
||||
|
||||
enum ComposePreview {
|
||||
case noPreview
|
||||
case linkPreview(linkPreview: LinkPreview?)
|
||||
@@ -46,7 +40,6 @@ struct LiveMessage {
|
||||
struct ComposeState {
|
||||
var message: String
|
||||
var liveMessage: LiveMessage? = nil
|
||||
var directMember: ComposeDirectMember
|
||||
var preview: ComposePreview
|
||||
var contextItem: ComposeContextItem
|
||||
var voiceMessageRecordingState: VoiceMessageRecordingState
|
||||
@@ -56,14 +49,12 @@ struct ComposeState {
|
||||
init(
|
||||
message: String = "",
|
||||
liveMessage: LiveMessage? = nil,
|
||||
directMember: ComposeDirectMember = .noDirectMember,
|
||||
preview: ComposePreview = .noPreview,
|
||||
contextItem: ComposeContextItem = .noContextItem,
|
||||
voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording
|
||||
) {
|
||||
self.message = message
|
||||
self.liveMessage = liveMessage
|
||||
self.directMember = directMember
|
||||
self.preview = preview
|
||||
self.contextItem = contextItem
|
||||
self.voiceMessageRecordingState = voiceMessageRecordingState
|
||||
@@ -71,11 +62,6 @@ struct ComposeState {
|
||||
|
||||
init(editingItem: ChatItem) {
|
||||
self.message = editingItem.content.text
|
||||
if let directMember = editingItem.directMember {
|
||||
self.directMember = .directMember(groupMember: directMember)
|
||||
} else {
|
||||
self.directMember = .noDirectMember
|
||||
}
|
||||
self.preview = chatItemPreview(chatItem: editingItem)
|
||||
self.contextItem = .editingItem(chatItem: editingItem)
|
||||
if let emc = editingItem.content.msgContent,
|
||||
@@ -89,7 +75,6 @@ struct ComposeState {
|
||||
func copy(
|
||||
message: String? = nil,
|
||||
liveMessage: LiveMessage? = nil,
|
||||
directMember: ComposeDirectMember? = nil,
|
||||
preview: ComposePreview? = nil,
|
||||
contextItem: ComposeContextItem? = nil,
|
||||
voiceMessageRecordingState: VoiceMessageRecordingState? = nil
|
||||
@@ -97,7 +82,6 @@ struct ComposeState {
|
||||
ComposeState(
|
||||
message: message ?? self.message,
|
||||
liveMessage: liveMessage ?? self.liveMessage,
|
||||
directMember: directMember ?? self.directMember,
|
||||
preview: preview ?? self.preview,
|
||||
contextItem: contextItem ?? self.contextItem,
|
||||
voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState
|
||||
@@ -273,7 +257,9 @@ struct ComposeView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
contextDirectMemberView()
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
ContextInvitingContactMemberView()
|
||||
}
|
||||
contextItemView()
|
||||
switch (composeState.editing, composeState.preview) {
|
||||
case (true, .filePreview): EmptyView()
|
||||
@@ -287,7 +273,7 @@ struct ComposeView: View {
|
||||
Image(systemName: "paperclip")
|
||||
.resizable()
|
||||
}
|
||||
.disabled(composeState.attachmentDisabled || !chat.userCanSend)
|
||||
.disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false))
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.bottom, 12)
|
||||
.padding(.leading, 12)
|
||||
@@ -315,6 +301,7 @@ struct ComposeView: View {
|
||||
composeState.liveMessage = nil
|
||||
chatModel.removeLiveDummy()
|
||||
},
|
||||
nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false,
|
||||
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
||||
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
||||
startVoiceMessageRecording: {
|
||||
@@ -599,38 +586,20 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func contextDirectMemberView() -> some View {
|
||||
switch composeState.directMember {
|
||||
case .noDirectMember:
|
||||
EmptyView()
|
||||
case let .directMember(groupMember):
|
||||
ContextDirectMemberView(
|
||||
directMember: groupMember,
|
||||
cancelDirectMemberContext: { composeState = composeState.copy(
|
||||
directMember: .directMemberCancelled,
|
||||
contextItem: .noContextItem
|
||||
)}
|
||||
)
|
||||
case .directMemberCancelled:
|
||||
ContextDirectMemberView(
|
||||
directMember: nil,
|
||||
cancelDirectMemberContext: { composeState = composeState.copy(directMember: .noDirectMember) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func contextItemView() -> some View {
|
||||
switch composeState.contextItem {
|
||||
case .noContextItem:
|
||||
EmptyView()
|
||||
case let .quotedItem(chatItem: quotedItem):
|
||||
ContextItemView(
|
||||
chat: chat,
|
||||
contextItem: quotedItem,
|
||||
contextIcon: "arrowshape.turn.up.left",
|
||||
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
|
||||
)
|
||||
case let .editingItem(chatItem: editingItem):
|
||||
ContextItemView(
|
||||
chat: chat,
|
||||
contextItem: editingItem,
|
||||
contextIcon: "pencil",
|
||||
cancelContextItem: { clearState() }
|
||||
@@ -654,7 +623,9 @@ struct ComposeView: View {
|
||||
if liveMessage != nil { composeState = composeState.copy(liveMessage: nil) }
|
||||
await sending()
|
||||
}
|
||||
if case let .editingItem(ci) = composeState.contextItem {
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
await sendMemberContactInvitation()
|
||||
} else if case let .editingItem(ci) = composeState.contextItem {
|
||||
sent = await updateMessage(ci, live: live)
|
||||
} else if let liveMessage = liveMessage, liveMessage.sentMsg != nil {
|
||||
sent = await updateMessage(liveMessage.chatItem, live: live)
|
||||
@@ -706,6 +677,19 @@ struct ComposeView: View {
|
||||
await MainActor.run { composeState.inProgress = true }
|
||||
}
|
||||
|
||||
func sendMemberContactInvitation() async {
|
||||
do {
|
||||
let mc = checkLinkPreview()
|
||||
let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
|
||||
await MainActor.run {
|
||||
self.chatModel.updateContact(contact)
|
||||
}
|
||||
} catch {
|
||||
logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)")
|
||||
AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func updateMessage(_ ei: ChatItem, live: Bool) async -> ChatItem? {
|
||||
if let oldMsgContent = ei.content.msgContent {
|
||||
do {
|
||||
@@ -784,43 +768,25 @@ struct ComposeView: View {
|
||||
}
|
||||
|
||||
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
|
||||
var sendRef: SendRef?
|
||||
let chatId = chat.chatInfo.apiId
|
||||
switch chat.chatInfo.chatType {
|
||||
case .direct:
|
||||
sendRef = .direct(contactId: chatId)
|
||||
case .group:
|
||||
switch composeState.directMember {
|
||||
case let .directMember(groupMember):
|
||||
sendRef = .group(groupId: chatId, directMemberId: groupMember.groupMemberId)
|
||||
default:
|
||||
sendRef = .group(groupId: chatId, directMemberId: nil)
|
||||
if let chatItem = await apiSendMessage(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
file: file,
|
||||
quotedItemId: quoted,
|
||||
msg: mc,
|
||||
live: live,
|
||||
ttl: ttl
|
||||
) {
|
||||
await MainActor.run {
|
||||
chatModel.removeLiveDummy(animated: false)
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
default: sendRef = nil
|
||||
return chatItem
|
||||
}
|
||||
if let sendRef = sendRef {
|
||||
if let chatItem = await apiSendMessage(
|
||||
sendRef: sendRef,
|
||||
file: file,
|
||||
quotedItemId: quoted,
|
||||
msg: mc,
|
||||
live: live,
|
||||
ttl: ttl
|
||||
) {
|
||||
await MainActor.run {
|
||||
chatModel.removeLiveDummy(animated: false)
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
return chatItem
|
||||
}
|
||||
if let file = file {
|
||||
removeFile(file.filePath)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
logger.error("ComposeView send: sendRef is nil")
|
||||
return nil
|
||||
if let file = file {
|
||||
removeFile(file.filePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkLinkPreview() -> MsgContent {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
//
|
||||
// ContextDirectMemberView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 11.09.2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ContextDirectMemberView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let directMember: GroupMember?
|
||||
let cancelDirectMemberContext: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if let directMember = directMember {
|
||||
Text("**only to** \(directMember.chatViewName)")
|
||||
.lineLimit(1)
|
||||
if let image = directMember.image {
|
||||
ProfileImage(imageStr: image)
|
||||
.frame(width: 24, height: 24)
|
||||
.padding(.trailing, 2)
|
||||
} else {
|
||||
Image(systemName: "arrow.left.arrow.right")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.trailing, 2)
|
||||
}
|
||||
} else {
|
||||
// maybe make it disappear after some time?
|
||||
Text("send to all group members")
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
withAnimation {
|
||||
cancelDirectMemberContext()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextDirectMemberView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContextDirectMemberView(directMember: GroupMember.sampleData, cancelDirectMemberContext: {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// ContextInvitingContactMemberView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 18.09.2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContextInvitingContactMemberView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "message")
|
||||
.foregroundColor(.secondary)
|
||||
Text("Send direct message to connect")
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextInvitingContactMemberView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContextInvitingContactMemberView()
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct ContextItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
let contextItem: ChatItem
|
||||
let contextIcon: String
|
||||
let cancelContextItem: () -> Void
|
||||
@@ -48,6 +49,7 @@ struct ContextItemView: View {
|
||||
|
||||
private func msgContentView(lines: Int) -> some View {
|
||||
MsgContentView(
|
||||
chat: chat,
|
||||
text: contextItem.text,
|
||||
formattedText: contextItem.formattedText
|
||||
)
|
||||
@@ -59,6 +61,6 @@ struct ContextItemView: View {
|
||||
struct ContextItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
|
||||
return ContextItemView(contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {})
|
||||
return ContextItemView(chat: Chat.sampleData, contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,20 +14,28 @@ import PhotosUI
|
||||
struct NativeTextEditor: UIViewRepresentable {
|
||||
@Binding var text: String
|
||||
@Binding var disableEditing: Bool
|
||||
let height: CGFloat
|
||||
let font: UIFont
|
||||
@Binding var height: CGFloat
|
||||
@Binding var focused: Bool
|
||||
let alignment: TextAlignment
|
||||
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 {
|
||||
let field = CustomUITextField()
|
||||
let field = CustomUITextField(height: _height)
|
||||
field.text = text
|
||||
field.font = font
|
||||
field.textAlignment = alignment == .leading ? .left : .right
|
||||
field.autocapitalizationType = .sentences
|
||||
field.setOnTextChangedListener { newText, images in
|
||||
if !disableEditing {
|
||||
// Speed up the process of updating layout, reduce jumping content on screen
|
||||
if !isShortEmoji(newText) { updateHeight(field) }
|
||||
text = newText
|
||||
} else {
|
||||
field.text = text
|
||||
@@ -39,24 +47,72 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
field.setOnFocusChangedListener { focused = $0 }
|
||||
field.delegate = field
|
||||
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
|
||||
updateFont(field)
|
||||
updateHeight(field)
|
||||
return field
|
||||
}
|
||||
|
||||
func updateUIView(_ field: UITextView, context: Context) {
|
||||
field.text = text
|
||||
field.font = font
|
||||
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 {
|
||||
var height: Binding<CGFloat>
|
||||
var newHeight: CGFloat = 0
|
||||
var onTextChanged: (String, [UploadContent]) -> Void = { newText, image 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) {
|
||||
self.onTextChanged = onTextChanged
|
||||
}
|
||||
|
||||
|
||||
func setOnFocusChangedListener(onFocusChanged: @escaping (Bool) -> Void) {
|
||||
self.onFocusChanged = onFocusChanged
|
||||
}
|
||||
@@ -144,14 +200,14 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
|
||||
|
||||
struct NativeTextEditor_Previews: PreviewProvider{
|
||||
static var previews: some View {
|
||||
return NativeTextEditor(
|
||||
NativeTextEditor(
|
||||
text: Binding.constant("Hello, world!"),
|
||||
disableEditing: Binding.constant(false),
|
||||
height: 100,
|
||||
font: UIFont.preferredFont(forTextStyle: .body),
|
||||
height: Binding.constant(100),
|
||||
focused: Binding.constant(false),
|
||||
alignment: TextAlignment.leading,
|
||||
onImagesAdded: { _ in }
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ struct SendMessageView: View {
|
||||
var sendLiveMessage: (() async -> Void)? = nil
|
||||
var updateLiveMessage: (() async -> Void)? = nil
|
||||
var cancelLiveMessage: (() -> Void)? = nil
|
||||
var nextSendGrpInv: Bool = false
|
||||
var showVoiceMessageButton: Bool = true
|
||||
var voiceMessageAllowed: Bool = true
|
||||
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
|
||||
@@ -31,15 +32,12 @@ struct SendMessageView: View {
|
||||
var sendButtonColor = Color.accentColor
|
||||
@State private var teHeight: CGFloat = 42
|
||||
@State private var teFont: Font = .body
|
||||
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
|
||||
@State private var sendButtonSize: CGFloat = 29
|
||||
@State private var sendButtonOpacity: CGFloat = 1
|
||||
@State private var showCustomDisappearingMessageDialogue = false
|
||||
@State private var showCustomTimePicker = false
|
||||
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
|
||||
@State private var progressByTimeout = false
|
||||
var maxHeight: CGFloat = 360
|
||||
var minHeight: CGFloat = 37
|
||||
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
||||
|
||||
var body: some View {
|
||||
@@ -56,30 +54,16 @@ struct SendMessageView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
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(
|
||||
text: $composeState.message,
|
||||
disableEditing: $composeState.inProgress,
|
||||
height: teHeight,
|
||||
font: teUiFont,
|
||||
height: $teHeight,
|
||||
focused: $keyboardVisible,
|
||||
alignment: alignment,
|
||||
onImagesAdded: onMediaAdded
|
||||
)
|
||||
.allowsTightening(false)
|
||||
.frame(height: teHeight)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +83,13 @@ struct SendMessageView: View {
|
||||
.frame(height: teHeight, alignment: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
.padding(.vertical, 1)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
|
||||
.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
|
||||
if inProgress {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
@@ -118,7 +104,9 @@ struct SendMessageView: View {
|
||||
|
||||
@ViewBuilder private func composeActionButtons() -> some View {
|
||||
let vmrs = composeState.voiceMessageRecordingState
|
||||
if showVoiceMessageButton
|
||||
if nextSendGrpInv {
|
||||
inviteMemberContactButton()
|
||||
} else if showVoiceMessageButton
|
||||
&& composeState.message.isEmpty
|
||||
&& !composeState.editing
|
||||
&& composeState.liveMessage == nil
|
||||
@@ -162,6 +150,24 @@ struct SendMessageView: View {
|
||||
.padding([.top, .trailing], 4)
|
||||
}
|
||||
|
||||
private func inviteMemberContactButton() -> some View {
|
||||
Button {
|
||||
sendMessage(nil)
|
||||
} label: {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.resizable()
|
||||
.foregroundColor(sendButtonColor)
|
||||
.frame(width: sendButtonSize, height: sendButtonSize)
|
||||
.opacity(sendButtonOpacity)
|
||||
}
|
||||
.disabled(
|
||||
!composeState.sendEnabled ||
|
||||
composeState.inProgress
|
||||
)
|
||||
.frame(width: 29, height: 29)
|
||||
.padding([.bottom, .trailing], 4)
|
||||
}
|
||||
|
||||
private func sendMessageButton() -> some View {
|
||||
Button {
|
||||
sendMessage(nil)
|
||||
@@ -394,16 +400,12 @@ struct SendMessageView: View {
|
||||
.padding([.bottom, .trailing], 4)
|
||||
}
|
||||
|
||||
private func updateHeight(_ g: GeometryProxy) -> Color {
|
||||
private func updateFont(_ text: String) {
|
||||
DispatchQueue.main.async {
|
||||
teHeight = min(max(g.frame(in: .local).size.height, minHeight), maxHeight)
|
||||
(teFont, teUiFont) = isShortEmoji(composeState.message)
|
||||
? composeState.message.count < 4
|
||||
? (largeEmojiFont, largeEmojiUIFont)
|
||||
: (mediumEmojiFont, mediumEmojiUIFont)
|
||||
: (.body, UIFont.preferredFont(forTextStyle: .body))
|
||||
teFont = isShortEmoji(text)
|
||||
? (text.count < 4 ? largeEmojiFont : mediumEmojiFont)
|
||||
: .body
|
||||
}
|
||||
return Color.clear
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ struct AddGroupMembersViewCommon: View {
|
||||
do {
|
||||
for contactId in selectedContacts {
|
||||
let member = try await apiAddMember(groupInfo.groupId, contactId, selectedRole)
|
||||
await MainActor.run { _ = ChatModel.shared.upsertGroupMember(groupInfo, member) }
|
||||
await MainActor.run { _ = chatModel.upsertGroupMember(groupInfo, member) }
|
||||
}
|
||||
addedMembersCb(selectedContacts)
|
||||
} catch {
|
||||
|
||||
@@ -15,7 +15,7 @@ struct GroupChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
@State var groupInfo: GroupInfo
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@ObservedObject private var alertManager = AlertManager.shared
|
||||
@State private var alert: GroupChatInfoViewAlert? = nil
|
||||
@State private var groupLink: String?
|
||||
@@ -35,14 +35,30 @@ struct GroupChatInfoView: View {
|
||||
case leaveGroupAlert
|
||||
case cantInviteIncognitoAlert
|
||||
case largeGroupReceiptsDisabled
|
||||
case blockMemberAlert(mem: GroupMember)
|
||||
case unblockMemberAlert(mem: GroupMember)
|
||||
case removeMemberAlert(mem: GroupMember)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
|
||||
var id: GroupChatInfoViewAlert { get { self } }
|
||||
var id: String {
|
||||
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 {
|
||||
NavigationView {
|
||||
let members = chatModel.groupMembers
|
||||
.filter { $0.memberStatus != .memLeft && $0.memberStatus != .memRemoved }
|
||||
.filter { m in let status = m.wrapped.memberStatus; return status != .memLeft && status != .memRemoved }
|
||||
.sorted { $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||
|
||||
List {
|
||||
@@ -57,7 +73,7 @@ struct GroupChatInfoView: View {
|
||||
addOrEditWelcomeMessage()
|
||||
}
|
||||
groupPreferencesButton($groupInfo)
|
||||
if members.filter({ $0.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
|
||||
if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
|
||||
sendReceiptsOption()
|
||||
} else {
|
||||
sendReceiptsOptionDisabled()
|
||||
@@ -84,17 +100,17 @@ struct GroupChatInfoView: View {
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
let filteredMembers = s == "" ? members : members.filter { $0.chatViewName.localizedLowercase.contains(s) }
|
||||
memberView(groupInfo.membership, user: true)
|
||||
let filteredMembers = s == "" ? members : members.filter { $0.wrapped.chatViewName.localizedLowercase.contains(s) }
|
||||
MemberRowView(groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert)
|
||||
ForEach(filteredMembers) { member in
|
||||
ZStack {
|
||||
NavigationLink {
|
||||
memberInfoView(member.groupMemberId)
|
||||
memberInfoView(member)
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.opacity(0)
|
||||
memberView(member)
|
||||
MemberRowView(groupInfo: groupInfo, groupMember: member, alert: $alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +142,10 @@ struct GroupChatInfoView: View {
|
||||
case .leaveGroupAlert: return leaveGroupAlert()
|
||||
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
|
||||
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 {
|
||||
@@ -174,7 +194,7 @@ struct GroupChatInfoView: View {
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
chatModel.groupMembers = groupMembers.map { GMember.init($0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,51 +203,92 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func memberView(_ member: GroupMember, user: Bool = false) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image)
|
||||
.frame(width: 38, height: 38)
|
||||
.padding(.trailing, 2)
|
||||
// TODO server connection status
|
||||
VStack(alignment: .leading) {
|
||||
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : .primary)
|
||||
(member.verified ? memberVerifiedShield + t : t)
|
||||
.lineLimit(1)
|
||||
let s = Text(member.memberStatus.shortText)
|
||||
(user ? Text ("you: ") + s : s)
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
private struct MemberRowView: View {
|
||||
var groupInfo: GroupInfo
|
||||
@ObservedObject var groupMember: GMember
|
||||
var user: Bool = false
|
||||
@Binding var alert: GroupChatInfoViewAlert?
|
||||
|
||||
var body: some View {
|
||||
let member = groupMember.wrapped
|
||||
let v = HStack{
|
||||
ProfileImage(imageStr: member.image)
|
||||
.frame(width: 38, height: 38)
|
||||
.padding(.trailing, 2)
|
||||
// TODO server connection status
|
||||
VStack(alignment: .leading) {
|
||||
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : .primary)
|
||||
(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()
|
||||
let role = member.memberRole
|
||||
if role == .owner || role == .admin {
|
||||
Text(member.memberRole.text)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if user {
|
||||
v
|
||||
} else if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
removeSwipe(member, blockSwipe(member, v))
|
||||
} 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 var memberVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.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 memberInfoView(_ groupMember: GMember) -> some View {
|
||||
GroupMemberInfoView(groupInfo: groupInfo, groupMember: groupMember)
|
||||
.navigationBarHidden(false)
|
||||
}
|
||||
|
||||
private func groupLinkButton() -> some View {
|
||||
NavigationLink {
|
||||
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink, groupLinkMemberRole: $groupLinkMemberRole)
|
||||
.navigationBarTitle("Group link")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
groupLinkMemberRole: $groupLinkMemberRole,
|
||||
showTitle: false,
|
||||
creatingGroup: false
|
||||
)
|
||||
.navigationBarTitle("Group link")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if groupLink == nil {
|
||||
Label("Create group link", systemImage: "link.badge.plus")
|
||||
@@ -299,9 +360,9 @@ struct GroupChatInfoView: View {
|
||||
do {
|
||||
try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId)
|
||||
await MainActor.run {
|
||||
chatModel.removeChat(chat.chatInfo.id)
|
||||
chatModel.chatId = nil
|
||||
dismiss()
|
||||
chatModel.chatId = nil
|
||||
chatModel.removeChat(chat.chatInfo.id)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("deleteGroupAlert apiDeleteChat error: \(error.localizedDescription)")
|
||||
@@ -375,6 +436,28 @@ struct GroupChatInfoView: View {
|
||||
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 {
|
||||
@@ -396,6 +479,14 @@ 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 {
|
||||
Alert(
|
||||
title: Text("Can't invite contacts!"),
|
||||
@@ -412,6 +503,9 @@ func largeGroupReceiptsDisabledAlert() -> Alert {
|
||||
|
||||
struct GroupChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData)
|
||||
GroupChatInfoView(
|
||||
chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []),
|
||||
groupInfo: Binding.constant(GroupInfo.sampleData)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ struct GroupLinkView: View {
|
||||
var groupId: Int64
|
||||
@Binding var groupLink: String?
|
||||
@Binding var groupLinkMemberRole: GroupMemberRole
|
||||
var showTitle: Bool = false
|
||||
var creatingGroup: Bool = false
|
||||
var linkCreatedCb: (() -> Void)? = nil
|
||||
@State private var creatingLink = false
|
||||
@State private var alert: GroupLinkAlert?
|
||||
|
||||
@@ -29,10 +32,35 @@ struct GroupLinkView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if creatingGroup {
|
||||
NavigationView {
|
||||
groupLinkView()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button ("Continue") { linkCreatedCb?() }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
groupLinkView()
|
||||
}
|
||||
}
|
||||
|
||||
private func groupLinkView() -> some View {
|
||||
List {
|
||||
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)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
Group {
|
||||
if showTitle {
|
||||
Text("Group link")
|
||||
.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 {
|
||||
if let groupLink = groupLink {
|
||||
Picker("Initial role", selection: $groupLinkMemberRole) {
|
||||
@@ -41,15 +69,17 @@ struct GroupLinkView: View {
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
QRCode(uri: groupLink)
|
||||
SimpleXLinkQRCode(uri: groupLink)
|
||||
Button {
|
||||
showShareSheet(items: [groupLink])
|
||||
showShareSheet(items: [simplexChatLink(groupLink)])
|
||||
} label: {
|
||||
Label("Share link", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
|
||||
Button(role: .destructive) { alert = .deleteLink } label: {
|
||||
Label("Delete link", systemImage: "trash")
|
||||
if !creatingGroup {
|
||||
Button(role: .destructive) { alert = .deleteLink } label: {
|
||||
Label("Delete link", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button(action: createGroupLink) {
|
||||
|
||||
@@ -12,37 +12,40 @@ import SimpleXChat
|
||||
struct GroupMemberInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var groupInfo: GroupInfo
|
||||
@State var member: GroupMember
|
||||
@State var groupInfo: GroupInfo
|
||||
@ObservedObject var groupMember: GMember
|
||||
var navigation: Bool = false
|
||||
@State private var connectionStats: ConnectionStats? = nil
|
||||
@State private var connectionCode: String? = nil
|
||||
@State private var newRole: GroupMemberRole = .member
|
||||
@State private var alert: GroupMemberInfoViewAlert?
|
||||
@State private var connectToMemberDialog: Bool = false
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var justOpened = true
|
||||
@State private var progressIndicator = false
|
||||
|
||||
enum GroupMemberInfoViewAlert: Identifiable {
|
||||
case blockMemberAlert(mem: GroupMember)
|
||||
case unblockMemberAlert(mem: GroupMember)
|
||||
case removeMemberAlert(mem: GroupMember)
|
||||
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
|
||||
case switchAddressAlert
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case connRequestSentAlert(type: ConnReqType)
|
||||
case planAndConnectAlert(alert: PlanAndConnectAlert)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
case other(alert: Alert)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .removeMemberAlert: return "removeMemberAlert"
|
||||
case let .changeMemberRoleAlert(_, role): return "changeMemberRoleAlert \(role.rawValue)"
|
||||
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 .changeMemberRoleAlert(mem, role): return "changeMemberRoleAlert \(mem.groupMemberId) \(role.rawValue)"
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||
case .connRequestSentAlert: return "connRequestSentAlert"
|
||||
case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
case let .other(alert): return "other \(alert)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,171 +68,181 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
|
||||
private func groupMemberInfoView() -> some View {
|
||||
VStack {
|
||||
List {
|
||||
groupMemberInfoHeader(member)
|
||||
.listRowBackground(Color.clear)
|
||||
ZStack {
|
||||
VStack {
|
||||
let member = groupMember.wrapped
|
||||
List {
|
||||
groupMemberInfoHeader(member)
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
if member.memberActive {
|
||||
Section {
|
||||
if let contactId = member.memberContactId {
|
||||
if let chat = knownDirectChat(contactId) {
|
||||
if member.memberActive {
|
||||
Section {
|
||||
if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) {
|
||||
knownDirectChatButton(chat)
|
||||
} else if groupInfo.fullGroupPreferences.directMessages.on {
|
||||
newDirectChatButton(contactId)
|
||||
if let contactId = member.memberContactId {
|
||||
newDirectChatButton(contactId)
|
||||
} else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false {
|
||||
createMemberContactButton()
|
||||
}
|
||||
}
|
||||
if let code = connectionCode { verifyCodeButton(code) }
|
||||
if let connStats = connectionStats,
|
||||
connStats.ratchetSyncAllowed {
|
||||
synchronizeConnectionButton()
|
||||
}
|
||||
// } else if developerTools {
|
||||
// synchronizeConnectionButtonForce()
|
||||
// }
|
||||
}
|
||||
if let code = connectionCode { verifyCodeButton(code) }
|
||||
if let connStats = connectionStats,
|
||||
connStats.ratchetSyncAllowed {
|
||||
synchronizeConnectionButton()
|
||||
}
|
||||
// } else if developerTools {
|
||||
// synchronizeConnectionButtonForce()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
if let contactLink = member.contactLink {
|
||||
Section {
|
||||
QRCode(uri: contactLink)
|
||||
Button {
|
||||
showShareSheet(items: [contactLink])
|
||||
} label: {
|
||||
Label("Share address", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
if let contactId = member.memberContactId {
|
||||
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on {
|
||||
if let contactLink = member.contactLink {
|
||||
Section {
|
||||
SimpleXLinkQRCode(uri: contactLink)
|
||||
Button {
|
||||
showShareSheet(items: [simplexChatLink(contactLink)])
|
||||
} label: {
|
||||
Label("Share address", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
if let contactId = member.memberContactId {
|
||||
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on {
|
||||
connectViaAddressButton(contactLink)
|
||||
}
|
||||
} else {
|
||||
connectViaAddressButton(contactLink)
|
||||
}
|
||||
} else {
|
||||
connectViaAddressButton(contactLink)
|
||||
} header: {
|
||||
Text("Address")
|
||||
} footer: {
|
||||
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
|
||||
}
|
||||
} header: {
|
||||
Text("Address")
|
||||
} footer: {
|
||||
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
|
||||
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||
Picker("Change role", selection: $newRole) {
|
||||
ForEach(roles) { role in
|
||||
Text(role.text)
|
||||
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||
Picker("Change role", selection: $newRole) {
|
||||
ForEach(roles) { role in
|
||||
Text(role.text)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
} else {
|
||||
infoRow("Role", member.memberRole.text)
|
||||
}
|
||||
|
||||
// TODO invited by - need to get contact by contact id
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
|
||||
infoRow("Connection", connLevelDesc)
|
||||
}
|
||||
.frame(height: 36)
|
||||
} else {
|
||||
infoRow("Role", member.memberRole.text)
|
||||
}
|
||||
|
||||
// TODO invited by - need to get contact by contact id
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
|
||||
infoRow("Connection", connLevelDesc)
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
// TODO network connection status
|
||||
Button("Change receiving address") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
|
||||
Button("Abort changing address") {
|
||||
alert = .abortSwitchAddressAlert
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
// TODO network connection status
|
||||
Button("Change receiving address") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
|
||||
Button("Abort changing address") {
|
||||
alert = .abortSwitchAddressAlert
|
||||
}
|
||||
.disabled(
|
||||
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|
||||
|| connStats.ratchetSyncSendProhibited
|
||||
)
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer })
|
||||
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer })
|
||||
}
|
||||
}
|
||||
|
||||
if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
Section {
|
||||
removeMemberButton(member)
|
||||
if member.memberSettings.showMessages {
|
||||
blockMemberButton(member)
|
||||
} else {
|
||||
unblockMemberButton(member)
|
||||
}
|
||||
if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
removeMemberButton(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
if developerTools {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.onAppear {
|
||||
if #unavailable(iOS 16) {
|
||||
// this condition prevents re-setting picker
|
||||
if !justOpened { return }
|
||||
.navigationBarHidden(true)
|
||||
.onAppear {
|
||||
if #unavailable(iOS 16) {
|
||||
// this condition prevents re-setting picker
|
||||
if !justOpened { return }
|
||||
}
|
||||
newRole = member.memberRole
|
||||
do {
|
||||
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
|
||||
_ = chatModel.upsertGroupMember(groupInfo, mem)
|
||||
connectionStats = stats
|
||||
connectionCode = code
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
|
||||
}
|
||||
justOpened = false
|
||||
}
|
||||
newRole = member.memberRole
|
||||
do {
|
||||
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
|
||||
member = mem
|
||||
connectionStats = stats
|
||||
connectionCode = code
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
|
||||
.onChange(of: newRole) { newRole in
|
||||
if newRole != member.memberRole {
|
||||
alert = .changeMemberRoleAlert(mem: member, role: newRole)
|
||||
}
|
||||
}
|
||||
justOpened = false
|
||||
}
|
||||
.onChange(of: newRole) { _ in
|
||||
if newRole != member.memberRole {
|
||||
alert = .changeMemberRoleAlert(mem: member, role: newRole)
|
||||
.onChange(of: member.memberRole) { role in
|
||||
newRole = role
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.alert(item: $alert) { alertItem in
|
||||
switch(alertItem) {
|
||||
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
|
||||
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
|
||||
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
|
||||
case let .connRequestSentAlert(type): return connReqSentAlert(type)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
case let .other(alert): return alert
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.alert(item: $alert) { alertItem in
|
||||
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 .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
|
||||
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
|
||||
case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
|
||||
|
||||
if progressIndicator {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func connectViaAddressButton(_ contactLink: String) -> some View {
|
||||
Button {
|
||||
connectToMemberDialog = true
|
||||
planAndConnect(
|
||||
contactLink,
|
||||
showAlert: { alert = .planAndConnectAlert(alert: $0) },
|
||||
showActionSheet: { sheet = $0 },
|
||||
dismiss: true,
|
||||
incognito: nil
|
||||
)
|
||||
} label: {
|
||||
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 {
|
||||
@@ -239,7 +252,7 @@ struct GroupMemberInfoView: View {
|
||||
chatModel.chatId = chat.id
|
||||
}
|
||||
} label: {
|
||||
Label("Open direct chat", systemImage: "message")
|
||||
Label("Send direct message", systemImage: "message")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +269,34 @@ struct GroupMemberInfoView: View {
|
||||
logger.error("openDirectChatButton apiGetChat error: \(responseError(error))")
|
||||
}
|
||||
} label: {
|
||||
Label("Open direct chat", systemImage: "message")
|
||||
Label("Send direct message", systemImage: "message")
|
||||
}
|
||||
}
|
||||
|
||||
func createMemberContactButton() -> some View {
|
||||
Button {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
|
||||
await MainActor.run {
|
||||
progressIndicator = false
|
||||
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
|
||||
dismissAllSheets(animated: true)
|
||||
chatModel.chatId = memberContact.id
|
||||
chatModel.setContactNetworkStatus(memberContact, .connected)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error creating member contact")
|
||||
await MainActor.run {
|
||||
progressIndicator = false
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Send direct message", systemImage: "message")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,20 +336,20 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
|
||||
private func verifyCodeButton(_ code: String) -> some View {
|
||||
NavigationLink {
|
||||
let member = groupMember.wrapped
|
||||
return NavigationLink {
|
||||
VerifyCodeView(
|
||||
displayName: member.displayName,
|
||||
connectionCode: code,
|
||||
connectionVerified: member.verified,
|
||||
verify: { code in
|
||||
var member = groupMember.wrapped
|
||||
if let r = apiVerifyGroupMember(member.groupId, member.groupMemberId, connectionCode: code) {
|
||||
let (verified, existingCode) = r
|
||||
let connCode = verified ? SecurityCode(securityCode: existingCode, verifiedAt: .now) : nil
|
||||
connectionCode = existingCode
|
||||
member.activeConn?.connectionCode = connCode
|
||||
if let i = chatModel.groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) {
|
||||
chatModel.groupMembers[i].activeConn?.connectionCode = connCode
|
||||
}
|
||||
_ = chatModel.upsertGroupMember(groupInfo, member)
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
@@ -343,12 +383,29 @@ 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 {
|
||||
Button(role: .destructive) {
|
||||
alert = .removeMemberAlert(mem: mem)
|
||||
} label: {
|
||||
Label("Remove member", systemImage: "trash")
|
||||
.foregroundColor(Color.red)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +441,6 @@ struct GroupMemberInfoView: View {
|
||||
do {
|
||||
let updatedMember = try await apiMemberRole(groupInfo.groupId, mem.groupMemberId, newRole)
|
||||
await MainActor.run {
|
||||
member = updatedMember
|
||||
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
|
||||
@@ -405,10 +461,10 @@ struct GroupMemberInfoView: View {
|
||||
private func switchMemberAddress() {
|
||||
Task {
|
||||
do {
|
||||
let stats = try apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
let stats = try apiSwitchGroupMember(groupInfo.apiId, groupMember.groupMemberId)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, groupMember.wrapped, stats)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
@@ -424,10 +480,10 @@ struct GroupMemberInfoView: View {
|
||||
private func abortSwitchMemberAddress() {
|
||||
Task {
|
||||
do {
|
||||
let stats = try apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
let stats = try apiAbortSwitchGroupMember(groupInfo.apiId, groupMember.groupMemberId)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, member, stats)
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, groupMember.wrapped, stats)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))")
|
||||
@@ -442,7 +498,7 @@ struct GroupMemberInfoView: View {
|
||||
private func syncMemberConnection(force: Bool) {
|
||||
Task {
|
||||
do {
|
||||
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force)
|
||||
let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, groupMember.groupMemberId, force)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, mem, stats)
|
||||
@@ -459,11 +515,54 @@ 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 {
|
||||
static var previews: some View {
|
||||
GroupMemberInfoView(
|
||||
groupInfo: GroupInfo.sampleData,
|
||||
member: GroupMember.sampleData
|
||||
groupMember: GMember.sampleData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@ struct GroupPreferencesView: View {
|
||||
featureSection(.directMessages, $preferences.directMessages.enable)
|
||||
featureSection(.reactions, $preferences.reactions.enable)
|
||||
featureSection(.voice, $preferences.voice.enable)
|
||||
// TODO uncomment in 5.3
|
||||
// featureSection(.files, $preferences.files.enable)
|
||||
featureSection(.files, $preferences.files.enable)
|
||||
|
||||
if groupInfo.canEdit {
|
||||
Section {
|
||||
|
||||
@@ -9,6 +9,18 @@
|
||||
import SwiftUI
|
||||
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 {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@@ -18,8 +30,7 @@ struct GroupProfileView: View {
|
||||
@State private var showImagePicker = false
|
||||
@State private var showTakePhoto = false
|
||||
@State private var chosenImage: UIImage? = nil
|
||||
@State private var showSaveErrorAlert = false
|
||||
@State private var saveGroupError: String? = nil
|
||||
@State private var alert: GroupProfileAlert?
|
||||
@FocusState private var focusDisplayName
|
||||
|
||||
var body: some View {
|
||||
@@ -47,20 +58,29 @@ struct GroupProfileView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
ZStack(alignment: .leading) {
|
||||
if !validDisplayName(groupProfile.displayName) {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.foregroundColor(.red)
|
||||
.padding(.bottom, 10)
|
||||
ZStack(alignment: .topLeading) {
|
||||
if !validNewProfileName() {
|
||||
Button {
|
||||
alert = .invalidName(validName: mkValidName(groupProfile.displayName))
|
||||
} label: {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
||||
}
|
||||
profileNameTextEdit("Group display name", $groupProfile.displayName)
|
||||
.focused($focusDisplayName)
|
||||
}
|
||||
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
|
||||
.padding(.bottom)
|
||||
let fullName = groupInfo.groupProfile.fullName
|
||||
if fullName != "" && fullName != groupProfile.displayName {
|
||||
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
|
||||
.padding(.bottom)
|
||||
}
|
||||
HStack(spacing: 20) {
|
||||
Button("Cancel") { dismiss() }
|
||||
Button("Save group profile") { saveProfile() }
|
||||
.disabled(groupProfile.displayName == "" || !validDisplayName(groupProfile.displayName))
|
||||
.disabled(!canUpdateProfile())
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
||||
@@ -99,27 +119,39 @@ struct GroupProfileView: View {
|
||||
focusDisplayName = true
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showSaveErrorAlert) {
|
||||
Alert(
|
||||
title: Text("Error saving group profile"),
|
||||
message: Text("\(saveGroupError ?? "Unexpected error")")
|
||||
)
|
||||
.alert(item: $alert) { a in
|
||||
switch a {
|
||||
case let .saveError(err):
|
||||
return Alert(
|
||||
title: Text("Error saving group profile"),
|
||||
message: Text(err)
|
||||
)
|
||||
case let .invalidName(name):
|
||||
return createInvalidNameAlert(name, $groupProfile.displayName)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.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 {
|
||||
TextField(label, text: name)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.padding(.bottom)
|
||||
.padding(.leading, 28)
|
||||
.padding(.leading, 32)
|
||||
}
|
||||
|
||||
func saveProfile() {
|
||||
Task {
|
||||
do {
|
||||
groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces)
|
||||
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
|
||||
await MainActor.run {
|
||||
groupInfo = gInfo
|
||||
@@ -128,8 +160,7 @@ struct GroupProfileView: View {
|
||||
}
|
||||
} catch let error {
|
||||
let err = responseError(error)
|
||||
saveGroupError = err
|
||||
showSaveErrorAlert = true
|
||||
alert = .saveError(err: err)
|
||||
logger.error("GroupProfile apiUpdateGroup error: \(err)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,28 +32,41 @@ struct ChatListNavLink: View {
|
||||
@State private var showJoinGroupDialog = false
|
||||
@State private var showContactConnectionInfo = false
|
||||
@State private var showInvalidJSON = false
|
||||
@State private var showDeleteContactActionSheet = false
|
||||
@State private var inProgress = false
|
||||
@State private var progressByTimeout = false
|
||||
|
||||
var body: some View {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
contactNavLink(contact)
|
||||
case let .group(groupInfo):
|
||||
groupNavLink(groupInfo)
|
||||
case let .contactRequest(cReq):
|
||||
contactRequestNavLink(cReq)
|
||||
case let .contactConnection(cConn):
|
||||
contactConnectionNavLink(cConn)
|
||||
case let .invalidJSON(json):
|
||||
invalidJSONPreview(json)
|
||||
Group {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
contactNavLink(contact)
|
||||
case let .group(groupInfo):
|
||||
groupNavLink(groupInfo)
|
||||
case let .contactRequest(cReq):
|
||||
contactRequestNavLink(cReq)
|
||||
case let .contactConnection(cConn):
|
||||
contactConnectionNavLink(cConn)
|
||||
case let .invalidJSON(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 {
|
||||
let v = NavLinkPlain(
|
||||
NavLinkPlain(
|
||||
tag: chat.chatInfo.id,
|
||||
selection: $chatModel.chatId,
|
||||
label: { ChatPreviewView(chat: chat) },
|
||||
disabled: !contact.ready
|
||||
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }
|
||||
)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
@@ -65,23 +78,35 @@ struct ChatListNavLink: View {
|
||||
clearChatButton()
|
||||
}
|
||||
Button {
|
||||
AlertManager.shared.showAlert(
|
||||
contact.ready
|
||||
? deleteContactAlert(chat.chatInfo)
|
||||
: deletePendingContactAlert(chat, contact)
|
||||
)
|
||||
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])
|
||||
|
||||
if contact.ready {
|
||||
v
|
||||
} else {
|
||||
v.onTapGesture {
|
||||
AlertManager.shared.showAlert(pendingContactAlert(chat, contact))
|
||||
.actionSheet(isPresented: $showDeleteContactActionSheet) {
|
||||
if contact.ready && contact.active {
|
||||
return ActionSheet(
|
||||
title: Text("Delete contact?\nThis cannot be undone!"),
|
||||
buttons: [
|
||||
.destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } },
|
||||
.destructive(Text("Delete")) { Task { await deleteChat(chat, notify: false) } },
|
||||
.cancel()
|
||||
]
|
||||
)
|
||||
} else {
|
||||
return ActionSheet(
|
||||
title: Text("Delete contact?\nThis cannot be undone!"),
|
||||
buttons: [
|
||||
.destructive(Text("Delete")) { Task { await deleteChat(chat) } },
|
||||
.cancel()
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +114,7 @@ struct ChatListNavLink: View {
|
||||
@ViewBuilder private func groupNavLink(_ groupInfo: GroupInfo) -> some View {
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited:
|
||||
ChatPreviewView(chat: chat)
|
||||
ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
joinGroupButton()
|
||||
@@ -100,12 +125,16 @@ struct ChatListNavLink: View {
|
||||
.onTapGesture { showJoinGroupDialog = true }
|
||||
.confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) {
|
||||
Button(chat.chatInfo.incognito ? "Join incognito" : "Join group") {
|
||||
joinGroup(groupInfo.groupId)
|
||||
inProgress = true
|
||||
joinGroup(groupInfo.groupId) {
|
||||
await MainActor.run { inProgress = false }
|
||||
}
|
||||
}
|
||||
Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } }
|
||||
}
|
||||
.disabled(inProgress)
|
||||
case .memAccepted:
|
||||
ChatPreviewView(chat: chat)
|
||||
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(groupInvitationAcceptedAlert())
|
||||
@@ -122,7 +151,7 @@ struct ChatListNavLink: View {
|
||||
NavLinkPlain(
|
||||
tag: chat.chatInfo.id,
|
||||
selection: $chatModel.chatId,
|
||||
label: { ChatPreviewView(chat: chat) },
|
||||
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
|
||||
disabled: !groupInfo.ready
|
||||
)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
@@ -147,7 +176,10 @@ struct ChatListNavLink: View {
|
||||
|
||||
private func joinGroupButton() -> some View {
|
||||
Button {
|
||||
joinGroup(chat.chatInfo.apiId)
|
||||
inProgress = true
|
||||
joinGroup(chat.chatInfo.apiId) {
|
||||
await MainActor.run { inProgress = false }
|
||||
}
|
||||
} label: {
|
||||
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
|
||||
}
|
||||
@@ -278,17 +310,6 @@ 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 {
|
||||
Alert(
|
||||
title: Text("Delete group?"),
|
||||
@@ -418,7 +439,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection,
|
||||
)
|
||||
}
|
||||
|
||||
func joinGroup(_ groupId: Int64) {
|
||||
func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) {
|
||||
Task {
|
||||
logger.debug("joinGroup")
|
||||
do {
|
||||
@@ -433,7 +454,9 @@ func joinGroup(_ groupId: Int64) {
|
||||
AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.")
|
||||
await deleteGroup()
|
||||
}
|
||||
await onComplete()
|
||||
} catch let error {
|
||||
await onComplete()
|
||||
let a = getErrorAlert(error, "Error joining group")
|
||||
AlertManager.shared.showAlertMsg(title: a.title, message: a.message)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct ChatListView: View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
NavStackCompat(
|
||||
isActive: Binding(
|
||||
get: { ChatModel.shared.chatId != nil },
|
||||
get: { chatModel.chatId != nil },
|
||||
set: { _ in }
|
||||
),
|
||||
destination: chatView
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct ChatPreviewView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var progressByTimeout: Bool
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var darkGreen = Color(red: 0, green: 0.5, blue: 0)
|
||||
|
||||
@@ -57,19 +58,26 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatPreviewImageOverlayIcon() -> some View {
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
if !contact.active {
|
||||
inactiveIcon()
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memLeft: groupInactiveIcon()
|
||||
case .memRemoved: groupInactiveIcon()
|
||||
case .memGroupDeleted: groupInactiveIcon()
|
||||
case .memLeft: inactiveIcon()
|
||||
case .memRemoved: inactiveIcon()
|
||||
case .memGroupDeleted: inactiveIcon()
|
||||
default: EmptyView()
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func groupInactiveIcon() -> some View {
|
||||
@ViewBuilder private func inactiveIcon() -> some View {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.secondary.opacity(0.65))
|
||||
.background(Circle().foregroundColor(Color(uiColor: .systemBackground)))
|
||||
@@ -80,7 +88,6 @@ struct ChatPreviewView: View {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
previewTitle(contact.verified == true ? verifiedIcon + t : t)
|
||||
.foregroundColor(chat.chatInfo.ready ? .primary : .secondary)
|
||||
case let .group(groupInfo):
|
||||
let v = previewTitle(t)
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
@@ -105,14 +112,17 @@ struct ChatPreviewView: View {
|
||||
|
||||
private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
text
|
||||
let t = text
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 36)
|
||||
.privacySensitive(!showChatPreviews && !draft)
|
||||
.redacted(reason: .privacy)
|
||||
if !showChatPreviews && !draft {
|
||||
t.privacySensitive(true).redacted(reason: .privacy)
|
||||
} else {
|
||||
t
|
||||
}
|
||||
let s = chat.chatStats
|
||||
if s.unreadCount > 0 || s.unreadChat {
|
||||
unreadCountText(s.unreadCount)
|
||||
@@ -181,7 +191,11 @@ struct ChatPreviewView: View {
|
||||
switch (chat.chatInfo) {
|
||||
case let .direct(contact):
|
||||
if !contact.ready {
|
||||
chatPreviewInfoText("connecting…")
|
||||
if contact.nextSendGrpInv {
|
||||
chatPreviewInfoText("send direct message")
|
||||
} else if contact.active {
|
||||
chatPreviewInfoText("connecting…")
|
||||
}
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
@@ -224,16 +238,26 @@ struct ChatPreviewView: View {
|
||||
@ViewBuilder private func chatStatusImage() -> some View {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
switch (chatModel.contactNetworkStatus(contact)) {
|
||||
case .connected: incognitoIcon(chat.chatInfo.incognito)
|
||||
case .error:
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 17, height: 17)
|
||||
.foregroundColor(.secondary)
|
||||
default:
|
||||
if contact.active {
|
||||
switch (chatModel.contactNetworkStatus(contact)) {
|
||||
case .connected: incognitoIcon(chat.chatInfo.incognito)
|
||||
case .error:
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 17, height: 17)
|
||||
.foregroundColor(.secondary)
|
||||
default:
|
||||
ProgressView()
|
||||
}
|
||||
} else {
|
||||
incognitoIcon(chat.chatInfo.incognito)
|
||||
}
|
||||
case .group:
|
||||
if progressByTimeout {
|
||||
ProgressView()
|
||||
} else {
|
||||
incognitoIcon(chat.chatInfo.incognito)
|
||||
}
|
||||
default:
|
||||
incognitoIcon(chat.chatInfo.incognito)
|
||||
@@ -263,30 +287,30 @@ struct ChatPreviewView_Previews: PreviewProvider {
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: []
|
||||
))
|
||||
), progressByTimeout: Binding.constant(false))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))]
|
||||
))
|
||||
), progressByTimeout: Binding.constant(false))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))],
|
||||
chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0)
|
||||
))
|
||||
), progressByTimeout: Binding.constant(false))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))]
|
||||
))
|
||||
), progressByTimeout: Binding.constant(false))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))],
|
||||
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0)
|
||||
))
|
||||
), progressByTimeout: Binding.constant(false))
|
||||
ChatPreviewView(chat: Chat(
|
||||
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.")],
|
||||
chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0)
|
||||
))
|
||||
), progressByTimeout: Binding.constant(false))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 78))
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ struct ContactConnectionInfo: View {
|
||||
|
||||
if contactConnection.initiated,
|
||||
let connReqInv = contactConnection.connReqInv {
|
||||
QRCode(uri: connReqInv)
|
||||
SimpleXLinkQRCode(uri: simplexChatLink(connReqInv))
|
||||
incognitoEnabled()
|
||||
shareLinkButton(connReqInv)
|
||||
oneTimeLinkLearnMoreButton()
|
||||
@@ -119,7 +119,7 @@ struct ContactConnectionInfo: View {
|
||||
if let conn = try await apiSetConnectionAlias(connId: contactConnection.pccConnId, localAlias: localAlias) {
|
||||
await MainActor.run {
|
||||
contactConnection = conn
|
||||
ChatModel.shared.updateContactConnection(conn)
|
||||
m.updateContactConnection(conn)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ struct VideoPlayerView: UIViewRepresentable {
|
||||
var timeObserver: Any? = nil
|
||||
|
||||
deinit {
|
||||
print("deinit coordinator of VideoPlayer")
|
||||
if let timeObserver = timeObserver {
|
||||
NotificationCenter.default.removeObserver(timeObserver)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct AddContactView: View {
|
||||
List {
|
||||
Section {
|
||||
if connReqInvitation != "" {
|
||||
QRCode(uri: connReqInvitation)
|
||||
SimpleXLinkQRCode(uri: connReqInvitation)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
@@ -48,7 +48,7 @@ struct AddContactView: View {
|
||||
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
|
||||
await MainActor.run {
|
||||
contactConnection = conn
|
||||
ChatModel.shared.updateContactConnection(conn)
|
||||
chatModel.updateContactConnection(conn)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -99,7 +99,7 @@ func sharedProfileInfo(_ incognito: Bool) -> Text {
|
||||
|
||||
func shareLinkButton(_ connReqInvitation: String) -> some View {
|
||||
Button {
|
||||
showShareSheet(items: [connReqInvitation])
|
||||
showShareSheet(items: [simplexChatLink(connReqInvitation)])
|
||||
} label: {
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Text("Share 1-time link")
|
||||
|
||||
@@ -12,27 +12,45 @@ import SimpleXChat
|
||||
struct AddGroupView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
@State private var chat: Chat?
|
||||
@State private var groupInfo: GroupInfo?
|
||||
@State private var profile = GroupProfile(displayName: "", fullName: "")
|
||||
@FocusState private var focusDisplayName
|
||||
@FocusState private var focusFullName
|
||||
@State private var showChooseSource = false
|
||||
@State private var showImagePicker = false
|
||||
@State private var showTakePhoto = false
|
||||
@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 {
|
||||
if let chat = chat, let groupInfo = groupInfo {
|
||||
AddGroupMembersViewCommon(
|
||||
chat: chat,
|
||||
groupInfo: groupInfo,
|
||||
creatingGroup: true,
|
||||
showFooterCounter: false
|
||||
) { _ in
|
||||
dismiss()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
m.chatId = groupInfo.id
|
||||
if !groupInfo.membership.memberIncognito {
|
||||
AddGroupMembersViewCommon(
|
||||
chat: chat,
|
||||
groupInfo: groupInfo,
|
||||
creatingGroup: true,
|
||||
showFooterCounter: false
|
||||
) { _ in
|
||||
dismiss()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
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 {
|
||||
@@ -41,79 +59,62 @@ struct AddGroupView: View {
|
||||
}
|
||||
|
||||
func createGroupView() -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Create secret group")
|
||||
.font(.largeTitle)
|
||||
.padding(.vertical, 4)
|
||||
Text("The group is fully decentralized – it is visible only to the members.")
|
||||
.padding(.bottom, 4)
|
||||
List {
|
||||
Group {
|
||||
Text("Create secret group")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom, 24)
|
||||
.onTapGesture(perform: hideKeyboard)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("Your chat profile will be sent to group members").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
ZStack(alignment: .center) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
profileImageView(profile.image)
|
||||
if profile.image != nil {
|
||||
Button {
|
||||
profile.image = nil
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 12)
|
||||
ZStack(alignment: .center) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: profile.image, color: Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(maxWidth: 128, maxHeight: 128)
|
||||
if profile.image != nil {
|
||||
Button {
|
||||
profile.image = nil
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editImageButton { showChooseSource = true }
|
||||
.buttonStyle(BorderlessButtonStyle()) // otherwise whole "list row" is clickable
|
||||
}
|
||||
|
||||
editImageButton { showChooseSource = true }
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.bottom, 4)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
if !validDisplayName(profile.displayName) {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 4)
|
||||
Section {
|
||||
groupNameTextField()
|
||||
Button(action: createGroup) {
|
||||
settingsRow("checkmark", color: .accentColor) { Text("Create group") }
|
||||
}
|
||||
textField("Group display name", text: $profile.displayName)
|
||||
.focused($focusDisplayName)
|
||||
.submitLabel(.next)
|
||||
.onSubmit {
|
||||
if canCreateProfile() { focusFullName = true }
|
||||
else { focusDisplayName = true }
|
||||
}
|
||||
}
|
||||
textField("Group full name (optional)", text: $profile.fullName)
|
||||
.focused($focusFullName)
|
||||
.submitLabel(.go)
|
||||
.onSubmit {
|
||||
if canCreateProfile() { createGroup() }
|
||||
else { focusFullName = true }
|
||||
.disabled(!canCreateProfile())
|
||||
IncognitoToggle(incognitoEnabled: $incognitoDefault)
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
sharedGroupProfileInfo(incognitoDefault)
|
||||
Text("Fully decentralized – visible only to members.")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
createGroup()
|
||||
} label: {
|
||||
Text("Create")
|
||||
Image(systemName: "greaterthan")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.onTapGesture(perform: hideKeyboard)
|
||||
}
|
||||
.disabled(!canCreateProfile())
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.onAppear() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
focusDisplayName = true
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) {
|
||||
Button("Take picture") {
|
||||
showTakePhoto = true
|
||||
@@ -133,6 +134,9 @@ struct AddGroupView: View {
|
||||
didSelectItem in showImagePicker = false
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showInvalidNameAlert) {
|
||||
createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName)
|
||||
}
|
||||
.onChange(of: chosenImage) { image in
|
||||
if let image = image {
|
||||
profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
|
||||
@@ -140,26 +144,52 @@ struct AddGroupView: View {
|
||||
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 {
|
||||
TextField(placeholder, text: text)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.padding(.leading, 28)
|
||||
.padding(.bottom)
|
||||
.padding(.leading, 36)
|
||||
}
|
||||
|
||||
func sharedGroupProfileInfo(_ incognito: Bool) -> Text {
|
||||
let name = ChatModel.shared.currentUser?.displayName ?? ""
|
||||
return Text(
|
||||
incognito
|
||||
? "A new random profile will be shared."
|
||||
: "Your profile **\(name)** will be shared."
|
||||
)
|
||||
}
|
||||
|
||||
func createGroup() {
|
||||
hideKeyboard()
|
||||
do {
|
||||
let gInfo = try apiNewGroup(profile)
|
||||
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
|
||||
let gInfo = try apiNewGroup(incognito: incognitoDefault, groupProfile: profile)
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
m.groupMembers = groupMembers.map { GMember.init($0) }
|
||||
}
|
||||
}
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: [])
|
||||
@@ -180,7 +210,8 @@ struct AddGroupView: View {
|
||||
}
|
||||
|
||||
func canCreateProfile() -> Bool {
|
||||
profile.displayName != "" && validDisplayName(profile.displayName)
|
||||
let name = profile.displayName.trimmingCharacters(in: .whitespaces)
|
||||
return name != "" && validDisplayName(name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,65 +58,369 @@ struct NewChatButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
enum ConnReqType: Equatable {
|
||||
case contact
|
||||
case invitation
|
||||
enum PlanAndConnectAlert: Identifiable {
|
||||
case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case invitationLinkConnecting(connectionLink: String)
|
||||
case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||
case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
|
||||
case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
|
||||
case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
|
||||
case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)"
|
||||
case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
|
||||
case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)"
|
||||
case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func connectViaLink(_ connectionLink: String, dismiss: DismissAction? = nil, incognito: Bool) {
|
||||
Task {
|
||||
if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
|
||||
DispatchQueue.main.async {
|
||||
dismiss?()
|
||||
AlertManager.shared.showAlert(connReqSentAlert(connReqType))
|
||||
}
|
||||
func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert {
|
||||
switch alert {
|
||||
case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito):
|
||||
return Alert(
|
||||
title: Text("Connect to yourself?"),
|
||||
message: Text("This is your own one-time link!"),
|
||||
primaryButton: .destructive(
|
||||
Text(incognito ? "Connect incognito" : "Connect"),
|
||||
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
|
||||
),
|
||||
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 {
|
||||
DispatchQueue.main.async {
|
||||
dismiss?()
|
||||
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 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 .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 .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 .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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CReqClientData: Decodable {
|
||||
var type: String
|
||||
var groupLinkId: String?
|
||||
}
|
||||
|
||||
func parseLinkQueryData(_ connectionLink: String) -> CReqClientData? {
|
||||
if let hashIndex = connectionLink.firstIndex(of: "#"),
|
||||
let urlQuery = URL(string: String(connectionLink[connectionLink.index(after: hashIndex)...])),
|
||||
let components = URLComponents(url: urlQuery, resolvingAgainstBaseURL: false),
|
||||
let data = components.queryItems?.first(where: { $0.name == "data" })?.value,
|
||||
let d = data.data(using: .utf8),
|
||||
let crData = try? getJSONDecoder().decode(CReqClientData.self, from: d) {
|
||||
return crData
|
||||
} else {
|
||||
return nil
|
||||
private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
|
||||
Task {
|
||||
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 {
|
||||
if dismiss {
|
||||
dismissAllSheets(animated: true) {
|
||||
AlertManager.shared.showAlert(connReqSentAlert(crt))
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlert(connReqSentAlert(crt))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if dismiss {
|
||||
DispatchQueue.main.async {
|
||||
dismissAllSheets(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkCRDataGroup(_ crData: CReqClientData) -> Bool {
|
||||
return crData.type == "group" && crData.groupLinkId != nil
|
||||
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||
Task {
|
||||
let m = ChatModel.shared
|
||||
if let c = m.getContactChat(contact.contactId) {
|
||||
DispatchQueue.main.async {
|
||||
if dismiss {
|
||||
dismissAllSheets(animated: true) {
|
||||
m.chatId = c.id
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
} else {
|
||||
m.chatId = c.id
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||
Task {
|
||||
let m = ChatModel.shared
|
||||
if let g = m.getGroupChat(groupInfo.groupId) {
|
||||
DispatchQueue.main.async {
|
||||
if dismiss {
|
||||
dismissAllSheets(animated: true) {
|
||||
m.chatId = g.id
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
} else {
|
||||
m.chatId = g.id
|
||||
showAlreadyExistsAlert?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
|
||||
mkAlert(
|
||||
title: "Contact already exists",
|
||||
message: "You are already connecting to \(contact.displayName)."
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
return mkAlert(
|
||||
title: "Connection request sent!",
|
||||
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!"
|
||||
message: type.connReqSentText
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ struct PasteToConnectView: View {
|
||||
@State private var connectionLink: String = ""
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
@FocusState private var linkEditorFocused: Bool
|
||||
@State private var alert: PlanAndConnectAlert?
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
@@ -52,11 +54,15 @@ struct PasteToConnectView: View {
|
||||
|
||||
IncognitoToggle(incognitoEnabled: $incognitoDefault)
|
||||
} footer: {
|
||||
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.")
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
sharedProfileInfo(incognitoDefault)
|
||||
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 {
|
||||
@@ -83,13 +89,13 @@ struct PasteToConnectView: View {
|
||||
|
||||
private func connect() {
|
||||
let link = connectionLink.trimmingCharacters(in: .whitespaces)
|
||||
if let crData = parseLinkQueryData(link),
|
||||
checkCRDataGroup(crData) {
|
||||
dismiss()
|
||||
AlertManager.shared.showAlert(groupLinkAlert(link, incognito: incognitoDefault))
|
||||
} else {
|
||||
connectViaLink(link, dismiss: dismiss, incognito: incognitoDefault)
|
||||
}
|
||||
planAndConnect(
|
||||
link,
|
||||
showAlert: { alert = $0 },
|
||||
showActionSheet: { sheet = $0 },
|
||||
dismiss: true,
|
||||
incognito: incognitoDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,22 @@ 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 {
|
||||
let uri: String
|
||||
var withLogo: Bool = true
|
||||
|
||||
@@ -13,6 +13,8 @@ import CodeScanner
|
||||
struct ScanToConnectView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
@State private var alert: PlanAndConnectAlert?
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -36,11 +38,11 @@ struct ScanToConnectView: View {
|
||||
)
|
||||
.padding(.top)
|
||||
|
||||
Group {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
sharedProfileInfo(incognitoDefault)
|
||||
+ 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.")
|
||||
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)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal)
|
||||
@@ -49,18 +51,20 @@ struct ScanToConnectView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
.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>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
if let crData = parseLinkQueryData(r.string),
|
||||
checkCRDataGroup(crData) {
|
||||
dismiss()
|
||||
AlertManager.shared.showAlert(groupLinkAlert(r.string, incognito: incognitoDefault))
|
||||
} else {
|
||||
Task { connectViaLink(r.string, dismiss: dismiss, incognito: incognitoDefault) }
|
||||
}
|
||||
planAndConnect(
|
||||
r.string,
|
||||
showAlert: { alert = $0 },
|
||||
showActionSheet: { sheet = $0 },
|
||||
dismiss: true,
|
||||
incognito: incognitoDefault
|
||||
)
|
||||
case let .failure(e):
|
||||
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
|
||||
dismiss()
|
||||
|
||||
@@ -9,175 +9,244 @@
|
||||
import SwiftUI
|
||||
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 {
|
||||
@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
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var displayName: String = ""
|
||||
@State private var fullName: String = ""
|
||||
@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 {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Create your profile")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(.bottom, 4)
|
||||
.frame(maxWidth: .infinity)
|
||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||
.padding(.bottom, 4)
|
||||
Text("The profile is only shared with your contacts.")
|
||||
.padding(.bottom)
|
||||
Group {
|
||||
Text("Create your profile")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||
.foregroundColor(.secondary)
|
||||
Text("The profile is only shared with your contacts.")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
if !validDisplayName(displayName) {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 4)
|
||||
let name = displayName.trimmingCharacters(in: .whitespaces)
|
||||
let validName = mkValidName(name)
|
||||
if name != validName {
|
||||
Button {
|
||||
showAlert(.invalidNameError(validName: validName))
|
||||
} label: {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
||||
}
|
||||
textField("Display name", text: $displayName)
|
||||
TextField("Enter your name…", text: $displayName)
|
||||
.focused($focusDisplayName)
|
||||
.submitLabel(.next)
|
||||
.onSubmit {
|
||||
if canCreateProfile() { focusFullName = true }
|
||||
else { focusDisplayName = true }
|
||||
}
|
||||
.padding(.leading, 32)
|
||||
}
|
||||
textField("Full name (optional)", text: $fullName)
|
||||
.focused($focusFullName)
|
||||
.submitLabel(.go)
|
||||
.onSubmit {
|
||||
if canCreateProfile() { createProfile() }
|
||||
else { focusFullName = true }
|
||||
}
|
||||
|
||||
.padding(.bottom)
|
||||
Spacer()
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
onboardingButtons()
|
||||
}
|
||||
.onAppear() {
|
||||
focusDisplayName = true
|
||||
setLastVersionDefault()
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
switch a {
|
||||
case .duplicateUserError: return duplicateUserAlert
|
||||
case let .createUserError(err): return creatUserErrorAlert(err)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.keyboardPadding()
|
||||
}
|
||||
|
||||
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
|
||||
TextField(placeholder, text: text)
|
||||
.textInputAutocapitalization(.never)
|
||||
.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()
|
||||
func onboardingButtons() -> some View {
|
||||
HStack {
|
||||
Button {
|
||||
hideKeyboard()
|
||||
withAnimation {
|
||||
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
|
||||
m.onboardingStage = .step3_CreateSimpleXAddress
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
}
|
||||
} 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 {
|
||||
alert = .duplicateUserError
|
||||
}
|
||||
default:
|
||||
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
||||
if m.currentUser == nil {
|
||||
AlertManager.shared.showAlert(creatUserErrorAlert(err))
|
||||
} else {
|
||||
alert = .createUserError(error: err)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "lessthan")
|
||||
Text("About SimpleX")
|
||||
}
|
||||
}
|
||||
logger.error("Failed to create user or start chat: \(responseError(error))")
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
createProfile(displayName, showAlert: showAlert, dismiss: dismiss)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Create")
|
||||
Image(systemName: "greaterthan")
|
||||
}
|
||||
}
|
||||
.disabled(!canCreateProfile(displayName))
|
||||
}
|
||||
}
|
||||
|
||||
func canCreateProfile() -> Bool {
|
||||
displayName != "" && validDisplayName(displayName)
|
||||
}
|
||||
|
||||
private var duplicateUserAlert: Alert {
|
||||
Alert(
|
||||
title: Text("Duplicate display name!"),
|
||||
message: Text("You already have a chat profile with the same display name. Please choose another name.")
|
||||
)
|
||||
}
|
||||
|
||||
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
|
||||
Alert(
|
||||
title: Text("Error creating profile!"),
|
||||
message: Text(err)
|
||||
)
|
||||
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 {
|
||||
showAlert(.duplicateUserError)
|
||||
}
|
||||
default:
|
||||
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
||||
if m.currentUser == nil {
|
||||
AlertManager.shared.showAlert(creatUserErrorAlert(err))
|
||||
} else {
|
||||
showAlert(.createUserError(error: err))
|
||||
}
|
||||
}
|
||||
logger.error("Failed to create user or start chat: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
private func canCreateProfile(_ displayName: String) -> Bool {
|
||||
let name = displayName.trimmingCharacters(in: .whitespaces)
|
||||
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 {
|
||||
Alert(
|
||||
title: Text("Duplicate display name!"),
|
||||
message: Text("You already have a chat profile with the same display name. Please choose another name.")
|
||||
)
|
||||
}
|
||||
|
||||
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
|
||||
Alert(
|
||||
title: Text("Error creating profile!"),
|
||||
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 {
|
||||
name.firstIndex(of: " ") == nil && name.first != "@" && name.first != "#"
|
||||
mkValidName(name.trimmingCharacters(in: .whitespaces)) == name
|
||||
}
|
||||
|
||||
func mkValidName(_ s: String) -> String {
|
||||
var c = s.cString(using: .utf8)!
|
||||
return fromCString(chat_valid_name(&c)!)
|
||||
}
|
||||
|
||||
struct CreateProfile_Previews: PreviewProvider {
|
||||
|
||||
@@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View {
|
||||
Spacer()
|
||||
|
||||
if let userAddress = m.userAddress {
|
||||
QRCode(uri: userAddress.connReqContact)
|
||||
SimpleXLinkQRCode(uri: userAddress.connReqContact)
|
||||
.frame(maxHeight: g.size.width)
|
||||
shareQRCodeButton(userAddress)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -126,7 +126,7 @@ struct CreateSimpleXAddress: View {
|
||||
|
||||
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
|
||||
Button {
|
||||
showShareSheet(items: [userAddress.connReqContact])
|
||||
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
|
||||
} label: {
|
||||
Label("Share", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
@@ -194,7 +194,7 @@ struct SendAddressMailView: View {
|
||||
let messageBody = String(format: NSLocalizedString("""
|
||||
<p>Hi!</p>
|
||||
<p><a href="%@">Connect to me via SimpleX Chat</a></p>
|
||||
""", comment: "email text"), userAddress.connReqContact)
|
||||
""", comment: "email text"), simplexChatLink(userAddress.connReqContact))
|
||||
MailView(
|
||||
isShowing: self.$showMailView,
|
||||
result: $mailViewResult,
|
||||
|
||||
@@ -14,7 +14,7 @@ struct OnboardingView: View {
|
||||
var body: some View {
|
||||
switch onboarding {
|
||||
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
|
||||
case .step2_CreateProfile: CreateProfile()
|
||||
case .step2_CreateProfile: CreateFirstProfile()
|
||||
case .step3_CreateSimpleXAddress: CreateSimpleXAddress()
|
||||
case .step4_SetNotificationsMode: SetNotificationsMode()
|
||||
case .onboardingComplete: EmptyView()
|
||||
|
||||
@@ -251,7 +251,38 @@ private let versionDescriptions: [VersionDescription] = [
|
||||
description: "- more stable message delivery.\n- a bit better groups.\n- and more!"
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version: "v5.3",
|
||||
post: URL(string: "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"),
|
||||
features: [
|
||||
FeatureDescription(
|
||||
icon: "desktopcomputer",
|
||||
title: "New desktop app!",
|
||||
description: "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻"
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "lock",
|
||||
title: "Encrypt stored files & media",
|
||||
description: "App encrypts new local files (except videos)."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "magnifyingglass",
|
||||
title: "Discover and join groups",
|
||||
description: "- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "theatermasks",
|
||||
title: "Simplified incognito mode",
|
||||
description: "Toggle incognito when connecting."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "character",
|
||||
title: "\(4) new interface languages",
|
||||
description: "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
private let lastVersion = versionDescriptions.last!.version
|
||||
@@ -321,12 +352,15 @@ struct WhatsNewView: View {
|
||||
private func featureDescription(_ icon: String, _ title: LocalizedStringKey, _ description: LocalizedStringKey) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
Image(systemName: icon).foregroundColor(.secondary)
|
||||
Image(systemName: icon)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(minWidth: 30, alignment: .center)
|
||||
Text(title).font(.title3).bold()
|
||||
}
|
||||
Text(description)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(10)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ struct AdvancedNetworkSettings: View {
|
||||
|
||||
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel)
|
||||
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [3_000000, 5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
|
||||
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [10_000, 20_000, 40_000, 75_000, 100_000], label: secondsLabel)
|
||||
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 60_000, 90_000, 120_000], label: secondsLabel)
|
||||
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
|
||||
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
|
||||
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)
|
||||
|
||||
@@ -66,6 +66,9 @@ struct PrivacySettings: View {
|
||||
Section {
|
||||
settingsRow("lock.doc") {
|
||||
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
|
||||
.onChange(of: encryptLocalFiles) {
|
||||
setEncryptLocalFiles($0)
|
||||
}
|
||||
}
|
||||
settingsRow("photo") {
|
||||
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
||||
@@ -90,7 +93,9 @@ struct PrivacySettings: View {
|
||||
}
|
||||
settingsRow("link") {
|
||||
Picker("SimpleX links", selection: $simplexLinkMode) {
|
||||
ForEach(SimpleXLinkMode.values) { mode in
|
||||
ForEach(
|
||||
SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode])
|
||||
) { mode in
|
||||
Text(mode.text)
|
||||
}
|
||||
}
|
||||
@@ -101,10 +106,6 @@ struct PrivacySettings: View {
|
||||
}
|
||||
} header: {
|
||||
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 {
|
||||
@@ -118,7 +119,7 @@ struct PrivacySettings: View {
|
||||
Text("Send delivery receipts to")
|
||||
} footer: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("These settings are for your current profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.")
|
||||
Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.")
|
||||
Text("They can be overridden in contact and group settings.")
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -183,6 +184,16 @@ 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) {
|
||||
contactReceiptsOverrides = m.chats.reduce(0) { count, chat in
|
||||
let sendRcpts = chat.chatInfo.contact?.chatSettings.sendRcpts
|
||||
@@ -345,7 +356,7 @@ struct SimplexLockView: View {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
let laDelays: [Int] = [10, 30, 60, 180, 0]
|
||||
let laDelays: [Int] = [10, 30, 60, 180, 600, 0]
|
||||
|
||||
func laDelayText(_ t: Int) -> LocalizedStringKey {
|
||||
let m = t / 60
|
||||
@@ -367,6 +378,7 @@ struct SimplexLockView: View {
|
||||
Text(mode.text)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
if performLA {
|
||||
Picker("Lock after", selection: $laLockDelay) {
|
||||
let delays = laDelays.contains(laLockDelay) ? laDelays : [laLockDelay] + laDelays
|
||||
@@ -374,6 +386,7 @@ struct SimplexLockView: View {
|
||||
Text(laDelayText(t))
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
if showChangePassword && laMode == .passcode {
|
||||
Button("Change passcode") {
|
||||
changeLAPassword()
|
||||
|
||||
@@ -93,7 +93,7 @@ enum SimpleXLinkMode: String, Identifiable {
|
||||
case full
|
||||
case browser
|
||||
|
||||
static var values: [SimpleXLinkMode] = [.description, .full, .browser]
|
||||
static var values: [SimpleXLinkMode] = [.description, .full]
|
||||
|
||||
public var id: Self { self }
|
||||
|
||||
@@ -381,7 +381,9 @@ struct ProfilePreview: View {
|
||||
Text(profileOf.displayName)
|
||||
.fontWeight(.bold)
|
||||
.font(.title2)
|
||||
Text(profileOf.fullName)
|
||||
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
|
||||
Text(profileOf.fullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ struct UserAddressView: View {
|
||||
|
||||
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
|
||||
Section {
|
||||
MutableQRCode(uri: Binding.constant(userAddress.connReqContact))
|
||||
MutableQRCode(uri: Binding.constant(simplexChatLink(userAddress.connReqContact)))
|
||||
shareQRCodeButton(userAddress)
|
||||
if MFMailComposeViewController.canSendMail() {
|
||||
shareViaEmailButton(userAddress)
|
||||
@@ -248,7 +248,7 @@ struct UserAddressView: View {
|
||||
|
||||
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
|
||||
Button {
|
||||
showShareSheet(items: [userAddress.connReqContact])
|
||||
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
|
||||
} label: {
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Text("Share address")
|
||||
|
||||
@@ -17,6 +17,8 @@ struct UserProfile: View {
|
||||
@State private var showImagePicker = false
|
||||
@State private var showTakePhoto = false
|
||||
@State private var chosenImage: UIImage? = nil
|
||||
@State private var alert: UserProfileAlert?
|
||||
@FocusState private var focusDisplayName
|
||||
|
||||
var body: some View {
|
||||
let user: User = chatModel.currentUser!
|
||||
@@ -47,18 +49,27 @@ struct UserProfile: View {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
ZStack(alignment: .leading) {
|
||||
if !validDisplayName(profile.displayName) {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.foregroundColor(.red)
|
||||
.padding(.bottom, 10)
|
||||
if !validNewProfileName(user) {
|
||||
Button {
|
||||
alert = .invalidNameError(validName: mkValidName(profile.displayName))
|
||||
} label: {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
||||
}
|
||||
profileNameTextEdit("Display name", $profile.displayName)
|
||||
profileNameTextEdit("Profile 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) {
|
||||
Button("Cancel") { editProfile = false }
|
||||
Button("Save (and notify contacts)") { saveProfile() }
|
||||
.disabled(profile.displayName == "" || !validDisplayName(profile.displayName))
|
||||
.disabled(!canSaveProfile(user))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
||||
@@ -74,11 +85,14 @@ struct UserProfile: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
profileNameView("Display name:", user.profile.displayName)
|
||||
profileNameView("Full name:", user.profile.fullName)
|
||||
profileNameView("Profile name:", user.profile.displayName)
|
||||
if showFullName(user) {
|
||||
profileNameView("Full name:", user.profile.fullName)
|
||||
}
|
||||
Button("Edit") {
|
||||
profile = fromLocalProfile(user.profile)
|
||||
editProfile = true
|
||||
focusDisplayName = true
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
||||
@@ -117,14 +131,12 @@ struct UserProfile: View {
|
||||
profile.image = nil
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
||||
}
|
||||
|
||||
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
|
||||
TextField(label, text: name)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.padding(.bottom)
|
||||
.padding(.leading, 28)
|
||||
.padding(.leading, 32)
|
||||
}
|
||||
|
||||
func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View {
|
||||
@@ -141,19 +153,34 @@ struct UserProfile: View {
|
||||
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() {
|
||||
Task {
|
||||
do {
|
||||
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
|
||||
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
|
||||
DispatchQueue.main.async {
|
||||
chatModel.updateCurrentUser(newProfile)
|
||||
profile = newProfile
|
||||
}
|
||||
editProfile = false
|
||||
} else {
|
||||
alert = .duplicateUserError
|
||||
}
|
||||
} catch {
|
||||
logger.error("UserProfile apiUpdateProfile error: \(responseError(error))")
|
||||
}
|
||||
editProfile = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"locale" : "bg"
|
||||
}
|
||||
],
|
||||
"properties" : {
|
||||
"localizable" : true
|
||||
},
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"red" : "0.000",
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "0.533"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"properties" : {
|
||||
"localizable" : true
|
||||
},
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* Bundle display name */
|
||||
"CFBundleDisplayName" = "SimpleX NSE";
|
||||
/* Bundle name */
|
||||
"CFBundleName" = "SimpleX NSE";
|
||||
/* Copyright (human-readable) */
|
||||
"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved.";
|
||||
@@ -0,0 +1,30 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"`a + b`" = "\\`a + b`";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"~strike~" = "\\~strike~";
|
||||
|
||||
/* call status */
|
||||
"connecting call" = "connecting call…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting server…" = "Connecting to server…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting server… (error: %@)" = "Connecting to server… (error: %@)";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"member connected" = "connected";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/* Bundle name */
|
||||
"CFBundleName" = "SimpleX";
|
||||
/* Privacy - Camera Usage Description */
|
||||
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
|
||||
/* Privacy - Face ID Usage Description */
|
||||
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
|
||||
/* Privacy - Microphone Usage Description */
|
||||
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
|
||||
/* Privacy - Photo Library Additions Usage Description */
|
||||
"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media";
|
||||
12
apps/ios/SimpleX Localizations/bg.xcloc/contents.json
Normal file
12
apps/ios/SimpleX Localizations/bg.xcloc/contents.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"developmentRegion" : "en",
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "bg",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,10 @@
|
||||
<target>%lld minut</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld vteřin</target>
|
||||
@@ -327,6 +331,12 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +710,10 @@
|
||||
<target>Sestavení aplikace: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>Ikona aplikace</target>
|
||||
@@ -835,6 +849,10 @@
|
||||
<target>Hlasové zprávy můžete posílat vy i váš kontakt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
@@ -1231,6 +1249,10 @@
|
||||
<target>Vytvořit odkaz</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Vytvořit jednorázovou pozvánku</target>
|
||||
@@ -1684,6 +1706,10 @@
|
||||
<target>Odpojit</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Zobrazované jméno</target>
|
||||
@@ -1823,6 +1849,10 @@
|
||||
<source>Encrypt local files</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<target>Zašifrovaná databáze</target>
|
||||
@@ -1948,6 +1978,10 @@
|
||||
<target>Chyba při vytváření odkazu skupiny</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>Chyba při vytváření profilu!</target>
|
||||
@@ -2077,6 +2111,10 @@
|
||||
<target>Chyba odesílání e-mailu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Chyba při odesílání zprávy</target>
|
||||
@@ -3090,6 +3128,10 @@
|
||||
<target>Archiv nové databáze</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>Nově zobrazované jméno</target>
|
||||
@@ -3303,6 +3345,10 @@
|
||||
<target>Hlasové zprávy může odesílat pouze váš kontakt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>Otevřít nastavení</target>
|
||||
@@ -4037,6 +4083,10 @@
|
||||
<target>Odeslat přímou zprávu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Poslat mizící zprávu</target>
|
||||
@@ -4334,6 +4384,10 @@
|
||||
<target>Jednorázová pozvánka SimpleX</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Přeskočit</target>
|
||||
@@ -4726,6 +4780,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target>
|
||||
<target>Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>Izolace transportu</target>
|
||||
@@ -5571,6 +5629,10 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>připojeno</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>připojování</target>
|
||||
@@ -6030,6 +6092,10 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>bezpečnostní kód změněn</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>začíná…</target>
|
||||
@@ -6174,7 +6240,7 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6206,7 +6272,7 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "cs",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,10 @@
|
||||
<target>%lld Minuten</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld Sekunde(n)</target>
|
||||
@@ -327,6 +331,12 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +710,10 @@
|
||||
<target>App Build: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>App-Icon</target>
|
||||
@@ -835,6 +849,10 @@
|
||||
<target>Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
@@ -1231,6 +1249,10 @@
|
||||
<target>Link erzeugen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Einmal-Einladungslink erstellen</target>
|
||||
@@ -1684,6 +1706,10 @@
|
||||
<target>Trennen</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Angezeigter Name</target>
|
||||
@@ -1821,6 +1847,11 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve">
|
||||
<source>Encrypt local files</source>
|
||||
<target>Lokale Dateien verschlüsseln</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
@@ -1948,6 +1979,10 @@
|
||||
<target>Fehler beim Erzeugen des Gruppen-Links</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>Fehler beim Erstellen des Profils!</target>
|
||||
@@ -1955,6 +1990,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error decrypting file" xml:space="preserve">
|
||||
<source>Error decrypting file</source>
|
||||
<target>Fehler beim Entschlüsseln der Datei</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
@@ -2077,6 +2113,10 @@
|
||||
<target>Fehler beim Senden der eMail</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Fehler beim Senden der Nachricht</target>
|
||||
@@ -3090,6 +3130,10 @@
|
||||
<target>Neues Datenbankarchiv</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>Neuer Anzeigename</target>
|
||||
@@ -3304,6 +3348,10 @@
|
||||
<target>Nur Ihr Kontakt kann Sprachnachrichten versenden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>Geräte-Einstellungen öffnen</target>
|
||||
@@ -4039,6 +4087,10 @@
|
||||
<target>Direktnachricht senden</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Verschwindende Nachricht senden</target>
|
||||
@@ -4339,6 +4391,10 @@
|
||||
<target>SimpleX-Einmal-Einladung</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Überspringen</target>
|
||||
@@ -4733,6 +4789,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
|
||||
<target>Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>Transport-Isolation</target>
|
||||
@@ -5581,6 +5641,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Verbunden</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>verbinde</target>
|
||||
@@ -6042,6 +6106,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Sicherheitscode wurde geändert</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>Verbindung wird gestartet…</target>
|
||||
@@ -6186,7 +6254,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6218,7 +6286,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "de",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="en" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,11 @@
|
||||
<target>%lld minutes</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<target>%lld new interface languages</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld second(s)</target>
|
||||
@@ -327,6 +332,15 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<target>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +714,11 @@
|
||||
<target>App build: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target>App encrypts new local files (except videos).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>App icon</target>
|
||||
@@ -835,6 +854,11 @@
|
||||
<target>Both you and your contact can send voice messages.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<target>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
@@ -1231,6 +1255,11 @@
|
||||
<target>Create link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Create one-time invitation link</target>
|
||||
@@ -1684,6 +1713,11 @@
|
||||
<target>Disconnect</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<target>Discover and join groups</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Display name</target>
|
||||
@@ -1824,6 +1858,11 @@
|
||||
<target>Encrypt local files</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<target>Encrypt stored files & media</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<target>Encrypted database</target>
|
||||
@@ -1949,6 +1988,11 @@
|
||||
<target>Error creating group link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<target>Error creating member contact</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>Error creating profile!</target>
|
||||
@@ -2079,6 +2123,11 @@
|
||||
<target>Error sending email</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<target>Error sending member contact invitation</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Error sending message</target>
|
||||
@@ -3092,6 +3141,11 @@
|
||||
<target>New database archive</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<target>New desktop app!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>New display name</target>
|
||||
@@ -3306,6 +3360,11 @@
|
||||
<target>Only your contact can send voice messages.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<target>Open</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>Open Settings</target>
|
||||
@@ -4041,6 +4100,11 @@
|
||||
<target>Send direct message</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<target>Send direct message to connect</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Send disappearing message</target>
|
||||
@@ -4341,6 +4405,11 @@
|
||||
<target>SimpleX one-time invitation</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<target>Simplified incognito mode</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Skip</target>
|
||||
@@ -4735,6 +4804,11 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<target>Toggle incognito when connecting.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>Transport isolation</target>
|
||||
@@ -5583,6 +5657,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>connected</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<target>connected directly</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>connecting</target>
|
||||
@@ -6044,6 +6123,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>security code changed</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<target>send direct message</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>starting…</target>
|
||||
@@ -6188,7 +6272,7 @@ SimpleX servers cannot see your profile.</target>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6220,7 +6304,7 @@ SimpleX servers cannot see your profile.</target>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "en",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="es" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,10 @@
|
||||
<target>%lld minutos</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld segundo(s)</target>
|
||||
@@ -327,6 +331,12 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +710,10 @@
|
||||
<target>Compilación app: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>Icono aplicación</target>
|
||||
@@ -835,6 +849,10 @@
|
||||
<target>Tanto tú como tu contacto podéis enviar mensajes de voz.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>Mediante perfil (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
@@ -1231,6 +1249,10 @@
|
||||
<target>Crear enlace</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Crea enlace de invitación de un uso</target>
|
||||
@@ -1636,7 +1658,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable (keep overrides)" xml:space="preserve">
|
||||
<source>Disable (keep overrides)</source>
|
||||
<target>Desactivar (conservar anulaciones)</target>
|
||||
<target>Desactivar (conservando anulaciones)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
@@ -1684,6 +1706,10 @@
|
||||
<target>Desconectar</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Nombre mostrado</target>
|
||||
@@ -1823,6 +1849,10 @@
|
||||
<source>Encrypt local files</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<target>Base de datos cifrada</target>
|
||||
@@ -1948,6 +1978,10 @@
|
||||
<target>Error al crear enlace de grupo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>¡Error al crear perfil!</target>
|
||||
@@ -2077,6 +2111,10 @@
|
||||
<target>Error al enviar email</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Error al enviar mensaje</target>
|
||||
@@ -3090,6 +3128,10 @@
|
||||
<target>Nuevo archivo de bases de datos</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>Nuevo nombre mostrado</target>
|
||||
@@ -3304,6 +3346,10 @@
|
||||
<target>Sólo tu contacto puede enviar mensajes de voz.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>Abrir Configuración</target>
|
||||
@@ -4039,6 +4085,10 @@
|
||||
<target>Enviar mensaje directo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Enviar mensaje temporal</target>
|
||||
@@ -4339,6 +4389,10 @@
|
||||
<target>Invitación SimpleX de un uso</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Omitir</target>
|
||||
@@ -4733,6 +4787,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t
|
||||
<target>Para comprobar el cifrado de extremo a extremo con tu contacto compara (o escanea) el código en tus dispositivos.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>Aislamiento de transporte</target>
|
||||
@@ -5582,6 +5640,10 @@ Los servidores de SimpleX no pueden ver tu perfil.</target>
|
||||
<target>conectado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>conectando</target>
|
||||
@@ -5779,6 +5841,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="event happened" xml:space="preserve">
|
||||
<source>event happened</source>
|
||||
<target>evento ocurrido</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="group deleted" xml:space="preserve">
|
||||
@@ -6042,6 +6105,10 @@ Los servidores de SimpleX no pueden ver tu perfil.</target>
|
||||
<target>código de seguridad cambiado</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>inicializando…</target>
|
||||
@@ -6186,7 +6253,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="es" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6218,7 +6285,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="es" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "es",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "fi",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="fr" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,11 @@
|
||||
<target>%lld minutes</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<target>%lld nouvelles langues d'interface</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld seconde·s</target>
|
||||
@@ -327,6 +332,15 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<target>- connexion au [service d'annuaire](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA) !
|
||||
- les accusés de réception (jusqu'à 20 membres).
|
||||
- plus rapide et plus stable.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +714,11 @@
|
||||
<target>Build de l'app : %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target>L'application chiffre les nouveaux fichiers locaux (sauf les vidéos).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>Icône de l'app</target>
|
||||
@@ -835,6 +854,11 @@
|
||||
<target>Vous et votre contact êtes tous deux en mesure d'envoyer des messages vocaux.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<target>Bulgare, finnois, thaïlandais et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) !</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>Par profil de chat (par défaut) ou [par connexion](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
@@ -1231,6 +1255,11 @@
|
||||
<target>Créer un lien</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>Créer un nouveau profil sur [l'application de bureau](https://simplex.chat/downloads/). 💻</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Créer un lien d'invitation unique</target>
|
||||
@@ -1684,6 +1713,11 @@
|
||||
<target>Se déconnecter</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<target>Découvrir et rejoindre des groupes</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Nom affiché</target>
|
||||
@@ -1821,6 +1855,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve">
|
||||
<source>Encrypt local files</source>
|
||||
<target>Chiffrer les fichiers locaux</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<target>Chiffrement des fichiers et des médias stockés</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
@@ -1948,6 +1988,10 @@
|
||||
<target>Erreur lors de la création du lien du groupe</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>Erreur lors de la création du profil !</target>
|
||||
@@ -1955,6 +1999,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error decrypting file" xml:space="preserve">
|
||||
<source>Error decrypting file</source>
|
||||
<target>Erreur lors du déchiffrement du fichier</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
@@ -2077,6 +2122,10 @@
|
||||
<target>Erreur lors de l'envoi de l'e-mail</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Erreur lors de l'envoi du message</target>
|
||||
@@ -3090,6 +3139,11 @@
|
||||
<target>Nouvelle archive de base de données</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<target>Nouvelle application de bureau !</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>Nouveau nom d'affichage</target>
|
||||
@@ -3304,6 +3358,10 @@
|
||||
<target>Seul votre contact peut envoyer des messages vocaux.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>Ouvrir les Paramètres</target>
|
||||
@@ -4039,6 +4097,10 @@
|
||||
<target>Envoi de message direct</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Envoyer un message éphémère</target>
|
||||
@@ -4339,6 +4401,11 @@
|
||||
<target>Invitation unique SimpleX</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<target>Mode incognito simplifié</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Passer</target>
|
||||
@@ -4733,6 +4800,11 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s
|
||||
<target>Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<target>Basculer en mode incognito lors de la connexion.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>Transport isolé</target>
|
||||
@@ -5581,6 +5653,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
|
||||
<target>connecté</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>connexion</target>
|
||||
@@ -6042,6 +6118,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
|
||||
<target>code de sécurité modifié</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>lancement…</target>
|
||||
@@ -6186,7 +6266,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="fr" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6218,7 +6298,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="fr" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "fr",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="it" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,11 @@
|
||||
<target>%lld minuti</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<target>%lld nuove lingue dell'interfaccia</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld secondo/i</target>
|
||||
@@ -327,6 +332,15 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<target>- connessione al [servizio directory](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- ricevute di consegna (fino a 20 membri).
|
||||
- più veloce e più stabile.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +714,11 @@
|
||||
<target>Build dell'app: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target>L'app cripta i nuovi file locali (eccetto i video).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>Icona app</target>
|
||||
@@ -835,6 +854,11 @@
|
||||
<target>Sia tu che il tuo contatto potete inviare messaggi vocali.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<target>Bulgaro, finlandese, tailandese e ucraino - grazie agli utenti e a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
@@ -1231,6 +1255,11 @@
|
||||
<target>Crea link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>Crea un nuovo profilo nell'[app desktop](https://simplex.chat/downloads/). 💻</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Crea link di invito una tantum</target>
|
||||
@@ -1684,6 +1713,11 @@
|
||||
<target>Disconnetti</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<target>Scopri ed unisciti ai gruppi</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Nome da mostrare</target>
|
||||
@@ -1821,6 +1855,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve">
|
||||
<source>Encrypt local files</source>
|
||||
<target>Cripta i file locali</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<target>Crittografia di file e media memorizzati</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
@@ -1948,6 +1988,10 @@
|
||||
<target>Errore nella creazione del link del gruppo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>Errore nella creazione del profilo!</target>
|
||||
@@ -1955,6 +1999,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error decrypting file" xml:space="preserve">
|
||||
<source>Error decrypting file</source>
|
||||
<target>Errore decifrando il file</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
@@ -2077,6 +2122,10 @@
|
||||
<target>Errore nell'invio dell'email</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Errore nell'invio del messaggio</target>
|
||||
@@ -3090,6 +3139,11 @@
|
||||
<target>Nuovo archivio database</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<target>Nuova app desktop!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>Nuovo nome da mostrare</target>
|
||||
@@ -3304,6 +3358,10 @@
|
||||
<target>Solo il tuo contatto può inviare messaggi vocali.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>Apri le impostazioni</target>
|
||||
@@ -4039,6 +4097,10 @@
|
||||
<target>Invia messaggio diretto</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Invia messaggio a tempo</target>
|
||||
@@ -4339,6 +4401,11 @@
|
||||
<target>Invito SimpleX una tantum</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<target>Modalità incognito semplificata</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Salta</target>
|
||||
@@ -4733,6 +4800,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
|
||||
<target>Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<target>Attiva/disattiva l'incognito quando ti colleghi.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>Isolamento del trasporto</target>
|
||||
@@ -5581,6 +5653,10 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
|
||||
<target>connesso/a</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>in connessione</target>
|
||||
@@ -6042,6 +6118,10 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
|
||||
<target>codice di sicurezza modificato</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>avvio…</target>
|
||||
@@ -6186,7 +6266,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="it" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6218,7 +6298,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="it" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "it",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="ja" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,10 @@
|
||||
<target>%lld 分</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld 秒</target>
|
||||
@@ -327,6 +331,12 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +710,10 @@
|
||||
<target>アプリのビルド: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>アプリのアイコン</target>
|
||||
@@ -835,6 +849,10 @@
|
||||
<target>あなたと連絡相手が音声メッセージを送信できます。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>チャット プロファイル経由 (デフォルト) または [接続経由](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
@@ -1231,6 +1249,10 @@
|
||||
<target>リンクを生成する</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>使い捨ての招待リンクを生成する</target>
|
||||
@@ -1683,6 +1705,10 @@
|
||||
<target>切断</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>表示名</target>
|
||||
@@ -1820,6 +1846,11 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve">
|
||||
<source>Encrypt local files</source>
|
||||
<target>ローカルファイルを暗号化する</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
@@ -1947,6 +1978,10 @@
|
||||
<target>グループリンク生成にエラー発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>プロフィール作成にエラー発生!</target>
|
||||
@@ -1954,6 +1989,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error decrypting file" xml:space="preserve">
|
||||
<source>Error decrypting file</source>
|
||||
<target>ファイルの復号エラー</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
@@ -2075,6 +2111,10 @@
|
||||
<target>メールの送信にエラー発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>メッセージ送信にエラー発生</target>
|
||||
@@ -3086,6 +3126,10 @@
|
||||
<target>新しいデータベースのアーカイブ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>新たな表示名</target>
|
||||
@@ -3300,6 +3344,10 @@
|
||||
<target>音声メッセージを送れるのはあなたの連絡相手だけです。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>設定を開く</target>
|
||||
@@ -4033,6 +4081,10 @@
|
||||
<target>ダイレクトメッセージを送信</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>消えるメッセージを送信</target>
|
||||
@@ -4326,6 +4378,10 @@
|
||||
<target>SimpleX使い捨て招待リンク</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>スキップ</target>
|
||||
@@ -4719,6 +4775,10 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>トランスポート隔離</target>
|
||||
@@ -5567,6 +5627,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<target>接続中</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>接続待ち</target>
|
||||
@@ -6028,6 +6092,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<target>セキュリティコードが変更されました</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>接続中…</target>
|
||||
@@ -6172,7 +6240,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6204,7 +6272,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "ja",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="nl" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,11 @@
|
||||
<target>%lld minuten</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<target>%lld nieuwe interface-talen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld seconde(n)</target>
|
||||
@@ -269,7 +274,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<target>**Maak een link / QR-code aan** die uw contactpersoon kan gebruiken.</target>
|
||||
<target>**Maak een link / QR-code aan** die uw contact kan gebruiken.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
||||
@@ -299,7 +304,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
|
||||
<target>**Scan QR-code**: om persoonlijk of via een video gesprek verbinding te maken met uw contactpersoon.</target>
|
||||
<target>**Scan QR-code**: om persoonlijk of via een video gesprek verbinding te maken met uw contact.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
|
||||
@@ -327,6 +332,15 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<target>- verbinding maken met [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- ontvangst bevestiging(tot 20 leden).
|
||||
- sneller en stabieler.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -482,7 +496,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept connection request?" xml:space="preserve">
|
||||
<source>Accept connection request?</source>
|
||||
<target>Accepteer contactpersoon</target>
|
||||
<target>Accepteer contact</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept contact request from %@?" xml:space="preserve">
|
||||
@@ -592,22 +606,22 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow calls only if your contact allows them." xml:space="preserve">
|
||||
<source>Allow calls only if your contact allows them.</source>
|
||||
<target>Sta oproepen alleen toe als uw contact persoon dit toestaat.</target>
|
||||
<target>Sta oproepen alleen toe als uw contact dit toestaat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve">
|
||||
<source>Allow disappearing messages only if your contact allows it to you.</source>
|
||||
<target>Sta verdwijnende berichten alleen toe als uw contactpersoon dit toestaat.</target>
|
||||
<target>Sta verdwijnende berichten alleen toe als uw contact dit toestaat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve">
|
||||
<source>Allow irreversible message deletion only if your contact allows it to you.</source>
|
||||
<target>Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contactpersoon dit toestaat.</target>
|
||||
<target>Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve">
|
||||
<source>Allow message reactions only if your contact allows them.</source>
|
||||
<target>Sta berichtreacties alleen toe als uw contactpersoon dit toestaat.</target>
|
||||
<target>Sta berichtreacties alleen toe als uw contact dit toestaat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow message reactions." xml:space="preserve">
|
||||
@@ -642,7 +656,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve">
|
||||
<source>Allow voice messages only if your contact allows them.</source>
|
||||
<target>Sta spraak berichten alleen toe als uw contactpersoon ze toestaat.</target>
|
||||
<target>Sta spraak berichten alleen toe als uw contact ze toestaat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow voice messages?" xml:space="preserve">
|
||||
@@ -700,6 +714,11 @@
|
||||
<target>App build: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target>App versleutelt nieuwe lokale bestanden (behalve video's).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>App icon</target>
|
||||
@@ -812,27 +831,32 @@
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>Zowel u als uw contactpersoon kunnen berichtreacties toevoegen.</target>
|
||||
<target>Zowel u als uw contact kunnen berichtreacties toevoegen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve">
|
||||
<source>Both you and your contact can irreversibly delete sent messages.</source>
|
||||
<target>Zowel jij als je contactpersoon kunnen verzonden berichten onherroepelijk verwijderen.</target>
|
||||
<target>Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can make calls." xml:space="preserve">
|
||||
<source>Both you and your contact can make calls.</source>
|
||||
<target>Zowel u als uw contact persoon kunnen bellen.</target>
|
||||
<target>Zowel u als uw contact kunnen bellen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve">
|
||||
<source>Both you and your contact can send disappearing messages.</source>
|
||||
<target>Zowel jij als je contactpersoon kunnen verdwijnende berichten sturen.</target>
|
||||
<target>Zowel jij als je contact kunnen verdwijnende berichten sturen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can send voice messages." xml:space="preserve">
|
||||
<source>Both you and your contact can send voice messages.</source>
|
||||
<target>Zowel jij als je contactpersoon kunnen spraak berichten verzenden.</target>
|
||||
<target>Zowel jij als je contact kunnen spraak berichten verzenden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<target>Bulgaars, Fins, Thais en Oekraïens - dankzij de gebruikers en [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
@@ -1231,6 +1255,11 @@
|
||||
<target>Maak link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>Maak een nieuw profiel aan in [desktop-app](https://simplex.chat/downloads/). 💻</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Maak een eenmalige uitnodiging link</target>
|
||||
@@ -1684,6 +1713,11 @@
|
||||
<target>verbinding verbreken</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<target>Ontdek en sluit je aan bij groepen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Weergavenaam</target>
|
||||
@@ -1821,6 +1855,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve">
|
||||
<source>Encrypt local files</source>
|
||||
<target>Versleutel lokale bestanden</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<target>Versleutel opgeslagen bestanden en media</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
@@ -1948,6 +1988,10 @@
|
||||
<target>Fout bij maken van groep link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>Fout bij aanmaken van profiel!</target>
|
||||
@@ -1955,6 +1999,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error decrypting file" xml:space="preserve">
|
||||
<source>Error decrypting file</source>
|
||||
<target>Fout bij het ontsleutelen van bestand</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
@@ -2077,6 +2122,10 @@
|
||||
<target>Fout bij het verzenden van e-mail</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Fout bij verzenden van bericht</target>
|
||||
@@ -2199,12 +2248,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve">
|
||||
<source>File will be received when your contact completes uploading it.</source>
|
||||
<target>Het bestand wordt gedownload wanneer uw contactpersoon het uploaden heeft voltooid.</target>
|
||||
<target>Het bestand wordt gedownload wanneer uw contact het uploaden heeft voltooid.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve">
|
||||
<source>File will be received when your contact is online, please wait or check later!</source>
|
||||
<target>Het bestand wordt ontvangen wanneer uw contact persoon online is, even geduld a.u.b. of controleer later!</target>
|
||||
<target>Het bestand wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of controleer later!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="File: %@" xml:space="preserve">
|
||||
@@ -2514,7 +2563,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
|
||||
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
|
||||
<target>Als u elkaar niet persoonlijk kunt ontmoeten, kunt u **de QR-code scannen in het video gesprek**, of uw contactpersoon kan een uitnodiging link delen.</target>
|
||||
<target>Als u elkaar niet persoonlijk kunt ontmoeten, kunt u **de QR-code scannen in het video gesprek**, of uw contact kan een uitnodiging link delen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
|
||||
@@ -2539,7 +2588,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve">
|
||||
<source>Image will be received when your contact completes uploading it.</source>
|
||||
<target>De afbeelding wordt gedownload wanneer uw contactpersoon het uploaden heeft voltooid.</target>
|
||||
<target>De afbeelding wordt gedownload wanneer uw contact het uploaden heeft voltooid.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve">
|
||||
@@ -2731,7 +2780,7 @@
|
||||
3. The connection was compromised.</source>
|
||||
<target>Het kan gebeuren wanneer:
|
||||
1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server.
|
||||
2. Decodering van het bericht is mislukt, omdat u of uw contactpersoon een oude databaseback-up heeft gebruikt.
|
||||
2. Decodering van het bericht is mislukt, omdat u of uw contact een oude databaseback-up heeft gebruikt.
|
||||
3. De verbinding is verbroken.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -3090,6 +3139,11 @@
|
||||
<target>Nieuw database archief</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<target>Nieuwe desktop app!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>Nieuwe weergavenaam</target>
|
||||
@@ -3261,7 +3315,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve">
|
||||
<source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source>
|
||||
<target>Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering).</target>
|
||||
<target>Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only you can make calls." xml:space="preserve">
|
||||
@@ -3281,12 +3335,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Only your contact can add message reactions." xml:space="preserve">
|
||||
<source>Only your contact can add message reactions.</source>
|
||||
<target>Alleen uw contactpersoon kan berichtreacties toevoegen.</target>
|
||||
<target>Alleen uw contact kan berichtreacties toevoegen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve">
|
||||
<source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source>
|
||||
<target>Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</target>
|
||||
<target>Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only your contact can make calls." xml:space="preserve">
|
||||
@@ -3296,12 +3350,16 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Only your contact can send disappearing messages." xml:space="preserve">
|
||||
<source>Only your contact can send disappearing messages.</source>
|
||||
<target>Alleen uw contactpersoon kan verdwijnende berichten verzenden.</target>
|
||||
<target>Alleen uw contact kan verdwijnende berichten verzenden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only your contact can send voice messages." xml:space="preserve">
|
||||
<source>Only your contact can send voice messages.</source>
|
||||
<target>Alleen uw contactpersoon kan spraak berichten verzenden.</target>
|
||||
<target>Alleen uw contact kan spraak berichten verzenden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
@@ -3396,7 +3454,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
|
||||
<source>Paste the link you received to connect with your contact.</source>
|
||||
<target>Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contactpersoon.</target>
|
||||
<target>Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contact.</target>
|
||||
<note>placeholder</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
@@ -3416,12 +3474,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve">
|
||||
<source>Please ask your contact to enable sending voice messages.</source>
|
||||
<target>Vraag uw contactpersoon om het verzenden van spraak berichten in te schakelen.</target>
|
||||
<target>Vraag uw contact om het verzenden van spraak berichten in te schakelen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve">
|
||||
<source>Please check that you used the correct link or ask your contact to send you another one.</source>
|
||||
<target>Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen.</target>
|
||||
<target>Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve">
|
||||
@@ -3966,7 +4024,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan security code from your contact's app." xml:space="preserve">
|
||||
<source>Scan security code from your contact's app.</source>
|
||||
<target>Scan de beveiligingscode van de app van uw contactpersoon.</target>
|
||||
<target>Scan de beveiligingscode van de app van uw contact.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan server QR code" xml:space="preserve">
|
||||
@@ -4039,6 +4097,10 @@
|
||||
<target>Direct bericht sturen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Stuur een verdwijnend bericht</target>
|
||||
@@ -4339,6 +4401,11 @@
|
||||
<target>Eenmalige SimpleX uitnodiging</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<target>Vereenvoudigde incognitomodus</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Overslaan</target>
|
||||
@@ -4688,7 +4755,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve">
|
||||
<source>To connect, your contact can scan QR code or use the link in the app.</source>
|
||||
<target>Om verbinding te maken, kan uw contact persoon de QR-code scannen of de link in de app gebruiken.</target>
|
||||
<target>Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To make a new connection" xml:space="preserve">
|
||||
@@ -4730,7 +4797,12 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
|
||||
</trans-unit>
|
||||
<trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve">
|
||||
<source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source>
|
||||
<target>Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contactpersoon te verifiëren.</target>
|
||||
<target>Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<target>Schakel incognito in tijdens het verbinden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
@@ -4826,8 +4898,8 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
|
||||
<trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve">
|
||||
<source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.
|
||||
To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source>
|
||||
<target>Tenzij uw contactpersoon de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.
|
||||
Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</target>
|
||||
<target>Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.
|
||||
Om verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unlock" xml:space="preserve">
|
||||
@@ -4972,7 +5044,7 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link
|
||||
</trans-unit>
|
||||
<trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve">
|
||||
<source>Video will be received when your contact completes uploading it.</source>
|
||||
<target>De video wordt gedownload wanneer uw contactpersoon het uploaden heeft voltooid.</target>
|
||||
<target>De video wordt gedownload wanneer uw contact het uploaden heeft voltooid.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve">
|
||||
@@ -5222,7 +5294,7 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link
|
||||
</trans-unit>
|
||||
<trans-unit id="You invited a contact" xml:space="preserve">
|
||||
<source>You invited a contact</source>
|
||||
<target>Je hebt je contactpersoon uitgenodigd</target>
|
||||
<target>Je hebt je contact uitgenodigd</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You joined this group" xml:space="preserve">
|
||||
@@ -5358,13 +5430,13 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link
|
||||
<trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve">
|
||||
<source>Your contact needs to be online for the connection to complete.
|
||||
You can cancel this connection and remove the contact (and try later with a new link).</source>
|
||||
<target>Uw contactpersoon moet online zijn om de verbinding te voltooien.
|
||||
U kunt deze verbinding verbreken en het contact verwijderen (en later proberen met een nieuwe link).</target>
|
||||
<target>Uw contact moet online zijn om de verbinding te voltooien.
|
||||
U kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
|
||||
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
|
||||
<target>Uw contactpersoon heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@).</target>
|
||||
<target>Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contacts can allow full message deletion." xml:space="preserve">
|
||||
@@ -5581,6 +5653,10 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
||||
<target>verbonden</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>Verbinden</target>
|
||||
@@ -5808,7 +5884,7 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via contact address link" xml:space="preserve">
|
||||
<source>incognito via contact address link</source>
|
||||
<target>incognito via contact adres link</target>
|
||||
<target>incognito via contactadres link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via group link" xml:space="preserve">
|
||||
@@ -6042,6 +6118,10 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
||||
<target>beveiligingscode gewijzigd</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>beginnen…</target>
|
||||
@@ -6074,7 +6154,7 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="via contact address link" xml:space="preserve">
|
||||
<source>via contact address link</source>
|
||||
<target>via contact adres link</target>
|
||||
<target>via contactadres link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via group link" xml:space="preserve">
|
||||
@@ -6186,7 +6266,7 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="nl" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6218,7 +6298,7 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="nl" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "nl",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="pl" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,11 @@
|
||||
<target>%lld minut</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<target>%lld nowe języki interfejsu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld sekund(y)</target>
|
||||
@@ -327,6 +332,15 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<target>- połącz do [serwera katalogowego](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- potwierdzenie dostarczenia (do 20 członków).
|
||||
- szybszy i bardziej stabilny.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +714,11 @@
|
||||
<target>Kompilacja aplikacji: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target>Aplikacja szyfruje nowe lokalne pliki (bez filmów).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>Ikona aplikacji</target>
|
||||
@@ -835,6 +854,11 @@
|
||||
<target>Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<target>Bułgarski, fiński, tajski i ukraiński – dzięki użytkownikom i [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>Według profilu czatu (domyślnie) lub [według połączenia](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target>
|
||||
@@ -1231,6 +1255,11 @@
|
||||
<target>Utwórz link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>Utwórz nowy profil w [aplikacji desktopowej](https://simplex.chat/downloads/). 💻</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Utwórz jednorazowy link do zaproszenia</target>
|
||||
@@ -1684,6 +1713,11 @@
|
||||
<target>Rozłącz</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<target>Odkrywaj i dołączaj do grup</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Wyświetlana nazwa</target>
|
||||
@@ -1821,6 +1855,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve">
|
||||
<source>Encrypt local files</source>
|
||||
<target>Zaszyfruj lokalne pliki</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<target>Szyfruj przechowywane pliki i media</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
@@ -1948,6 +1988,10 @@
|
||||
<target>Błąd tworzenia linku grupy</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>Błąd tworzenia profilu!</target>
|
||||
@@ -1955,6 +1999,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error decrypting file" xml:space="preserve">
|
||||
<source>Error decrypting file</source>
|
||||
<target>Błąd odszyfrowania pliku</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
@@ -2077,6 +2122,10 @@
|
||||
<target>Błąd wysyłania e-mail</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Błąd wysyłania wiadomości</target>
|
||||
@@ -3090,6 +3139,11 @@
|
||||
<target>Nowe archiwum bazy danych</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<target>Nowa aplikacja desktopowa!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>Nowa wyświetlana nazwa</target>
|
||||
@@ -3304,6 +3358,10 @@
|
||||
<target>Tylko Twój kontakt może wysyłać wiadomości głosowe.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>Otwórz Ustawienia</target>
|
||||
@@ -4039,6 +4097,10 @@
|
||||
<target>Wyślij wiadomość bezpośrednią</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Wyślij znikającą wiadomość</target>
|
||||
@@ -4339,6 +4401,11 @@
|
||||
<target>Zaproszenie jednorazowe SimpleX</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<target>Uproszczony tryb incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Pomiń</target>
|
||||
@@ -4733,6 +4800,11 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
|
||||
<target>Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<target>Przełącz incognito przy połączeniu.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>Izolacja transportu</target>
|
||||
@@ -5581,6 +5653,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
<target>połączony</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>łączenie</target>
|
||||
@@ -6042,6 +6118,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
<target>kod bezpieczeństwa zmieniony</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>uruchamianie…</target>
|
||||
@@ -6186,7 +6266,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pl" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6218,7 +6298,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="pl" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "pl",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="ru" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -197,6 +197,11 @@
|
||||
<target>%lld минуты</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<target>%lld новых языков интерфейса</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld секунд</target>
|
||||
@@ -327,6 +332,15 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<target>- соединиться с [каталогом групп](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- отчеты о доставке (до 20 членов).
|
||||
- быстрее и стабильнее.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -700,6 +714,11 @@
|
||||
<target>Сборка приложения: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target>Приложение шифрует новые локальные файлы (кроме видео).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>Иконка</target>
|
||||
@@ -835,6 +854,11 @@
|
||||
<target>Вы и Ваш контакт можете отправлять голосовые сообщения.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<target>Болгарский, финский, тайский и украинский - благодаря пользователям и [Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА).</target>
|
||||
@@ -1231,6 +1255,11 @@
|
||||
<target>Создать ссылку</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>Создайте новый профиль в [приложении для компьютера](https://simplex.chat/downloads/). 💻</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Создать ссылку-приглашение</target>
|
||||
@@ -1684,6 +1713,11 @@
|
||||
<target>Разрыв соединения</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</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>
|
||||
@@ -1821,6 +1855,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve">
|
||||
<source>Encrypt local files</source>
|
||||
<target>Шифровать локальные файлы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<target>Шифруйте сохраненные файлы и медиа</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
@@ -1948,6 +1988,10 @@
|
||||
<target>Ошибка при создании ссылки группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>Ошибка создания профиля!</target>
|
||||
@@ -1955,6 +1999,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error decrypting file" xml:space="preserve">
|
||||
<source>Error decrypting file</source>
|
||||
<target>Ошибка расшифровки файла</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
@@ -2077,6 +2122,10 @@
|
||||
<target>Ошибка отправки email</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>Ошибка при отправке сообщения</target>
|
||||
@@ -3090,6 +3139,11 @@
|
||||
<target>Новый архив чата</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<target>Приложение для компьютера!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>Новое имя</target>
|
||||
@@ -3304,6 +3358,10 @@
|
||||
<target>Только Ваш контакт может отправлять голосовые сообщения.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>Открыть Настройки</target>
|
||||
@@ -4039,6 +4097,10 @@
|
||||
<target>Отправить сообщение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>Отправить исчезающее сообщение</target>
|
||||
@@ -4339,6 +4401,11 @@
|
||||
<target>SimpleX одноразовая ссылка</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<target>Упрощенный режим Инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>Пропустить</target>
|
||||
@@ -4733,6 +4800,11 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>Чтобы подтвердить end-to-end шифрование с Вашим контактом сравните (или сканируйте) код безопасности на Ваших устройствах.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<target>Установите режим Инкогнито при соединении.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>Отдельные сессии для</target>
|
||||
@@ -5581,6 +5653,10 @@ SimpleX серверы не могут получить доступ к Ваше
|
||||
<target>соединение установлено</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>соединяется</target>
|
||||
@@ -6042,6 +6118,10 @@ SimpleX серверы не могут получить доступ к Ваше
|
||||
<target>код безопасности изменился</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>инициализация…</target>
|
||||
@@ -6186,7 +6266,7 @@ SimpleX серверы не могут получить доступ к Ваше
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6218,7 +6298,7 @@ SimpleX серверы не могут получить доступ к Ваше
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "ru",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="en.lproj/Localizable.strings" source-language="en" target-language="th" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
@@ -192,6 +192,10 @@
|
||||
<target>%lld นาที</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve">
|
||||
<source>%lld new interface languages</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<source>%lld second(s)</source>
|
||||
<target>%lld วินาที</target>
|
||||
@@ -322,6 +326,12 @@
|
||||
<target>, </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve">
|
||||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
|
||||
- delivery receipts (up to 20 members).
|
||||
- faster and more stable.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
@@ -693,6 +703,10 @@
|
||||
<target>รุ่นแอป: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<source>App icon</source>
|
||||
<target>ไอคอนแอป</target>
|
||||
@@ -828,6 +842,10 @@
|
||||
<target>ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve">
|
||||
<source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source>
|
||||
<target>ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (เบต้า)</target>
|
||||
@@ -1220,6 +1238,10 @@
|
||||
<target>สร้างลิงค์</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>สร้างลิงก์เชิญแบบใช้ครั้งเดียว</target>
|
||||
@@ -1672,6 +1694,10 @@
|
||||
<target>ตัดการเชื่อมต่อ</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Discover and join groups" xml:space="preserve">
|
||||
<source>Discover and join groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>ชื่อที่แสดง</target>
|
||||
@@ -1811,6 +1837,10 @@
|
||||
<source>Encrypt local files</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt stored files & media" xml:space="preserve">
|
||||
<source>Encrypt stored files & media</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted database" xml:space="preserve">
|
||||
<source>Encrypted database</source>
|
||||
<target>Encrypt ฐานข้อมูลเรียบร้อยแล้ว</target>
|
||||
@@ -1936,6 +1966,10 @@
|
||||
<target>เกิดข้อผิดพลาดในการสร้างลิงก์กลุ่ม</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating member contact" xml:space="preserve">
|
||||
<source>Error creating member contact</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||
<source>Error creating profile!</source>
|
||||
<target>เกิดข้อผิดพลาดในการสร้างโปรไฟล์!</target>
|
||||
@@ -2065,6 +2099,10 @@
|
||||
<target>เกิดข้อผิดพลาดในการส่งอีเมล</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending member contact invitation" xml:space="preserve">
|
||||
<source>Error sending member contact invitation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
<source>Error sending message</source>
|
||||
<target>เกิดข้อผิดพลาดในการส่งข้อความ</target>
|
||||
@@ -3075,6 +3113,10 @@
|
||||
<target>ฐานข้อมูลใหม่สำหรับการเก็บถาวร</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New desktop app!" xml:space="preserve">
|
||||
<source>New desktop app!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>ชื่อที่แสดงใหม่</target>
|
||||
@@ -3288,6 +3330,10 @@
|
||||
<target>ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open" xml:space="preserve">
|
||||
<source>Open</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open Settings" xml:space="preserve">
|
||||
<source>Open Settings</source>
|
||||
<target>เปิดการตั้งค่า</target>
|
||||
@@ -4020,6 +4066,10 @@
|
||||
<target>ส่งข้อความโดยตรง</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message to connect" xml:space="preserve">
|
||||
<source>Send direct message to connect</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>ส่งข้อความแบบที่หายไป</target>
|
||||
@@ -4317,6 +4367,10 @@
|
||||
<target>คำเชิญ SimpleX แบบครั้งเดียว</target>
|
||||
<note>simplex link type</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Simplified incognito mode" xml:space="preserve">
|
||||
<source>Simplified incognito mode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Skip" xml:space="preserve">
|
||||
<source>Skip</source>
|
||||
<target>ข้าม</target>
|
||||
@@ -4709,6 +4763,10 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
<source>Toggle incognito when connecting.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transport isolation" xml:space="preserve">
|
||||
<source>Transport isolation</source>
|
||||
<target>การแยกการขนส่ง</target>
|
||||
@@ -5553,6 +5611,10 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target>เชื่อมต่อสำเร็จ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connected directly" xml:space="preserve">
|
||||
<source>connected directly</source>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting" xml:space="preserve">
|
||||
<source>connecting</source>
|
||||
<target>กำลังเชื่อมต่อ</target>
|
||||
@@ -6012,6 +6074,10 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target>เปลี่ยนรหัสความปลอดภัยแล้ว</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="send direct message" xml:space="preserve">
|
||||
<source>send direct message</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>กำลังเริ่มต้น…</target>
|
||||
@@ -6156,7 +6222,7 @@ SimpleX servers cannot see your profile.</source>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="th" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
@@ -6188,7 +6254,7 @@ SimpleX servers cannot see your profile.</source>
|
||||
</file>
|
||||
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="th" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A5219j"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "th",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A5219j",
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user